mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
130
packages/server/src/modules/Accounts/Account.transformer.ts
Normal file
130
packages/server/src/modules/Accounts/Account.transformer.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Transformer } from '../Transformer/Transformer';
|
||||
import { Account } from './models/Account.model';
|
||||
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
|
||||
import { assocDepthLevelToObjectTree } from '@/utils/assoc-depth-level-to-object-tree';
|
||||
import { nestedArrayToFlatten } from '@/utils/nested-array-to-flatten';
|
||||
import { IAccountsStructureType } from './Accounts.types';
|
||||
|
||||
export class AccountTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'accountTypeLabel',
|
||||
'accountNormalFormatted',
|
||||
'formattedAmount',
|
||||
'flattenName',
|
||||
'bankBalanceFormatted',
|
||||
'lastFeedsUpdatedAtFormatted',
|
||||
'isFeedsPaused',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['plaidItem'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the flatten name with all dependants accounts names.
|
||||
* @param {IAccount} account -
|
||||
* @returns {string}
|
||||
*/
|
||||
public flattenName = (account: Account): string => {
|
||||
const parentDependantsIds = this.options.accountsGraph.dependantsOf(
|
||||
account.id,
|
||||
);
|
||||
const prefixAccounts = parentDependantsIds.map((dependId) => {
|
||||
const node = this.options.accountsGraph.getNodeData(dependId);
|
||||
return `${node.name}: `;
|
||||
});
|
||||
return `${prefixAccounts}${account.name}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted account amount.
|
||||
* @param {IAccount} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (account: Account): string => {
|
||||
return this.formatNumber(account.amount, {
|
||||
currencyCode: account.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted bank balance.
|
||||
* @param {Account} account
|
||||
* @returns {string}
|
||||
*/
|
||||
protected bankBalanceFormatted = (account: Account): string => {
|
||||
return this.formatNumber(account.bankBalance, {
|
||||
currencyCode: account.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted last feeds update at.
|
||||
* @param {IAccount} account
|
||||
* @returns {string}
|
||||
*/
|
||||
protected lastFeedsUpdatedAtFormatted = (account: Account): string => {
|
||||
return account.lastFeedsUpdatedAt
|
||||
? this.formatDate(account.lastFeedsUpdatedAt)
|
||||
: '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the bank account connection is paused.
|
||||
* @param account
|
||||
* @returns {boolean}
|
||||
*/
|
||||
protected isFeedsPaused = (account: Account): boolean => {
|
||||
// return account.plaidItem?.isPaused || false;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted account type label.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected accountTypeLabel = (account: Account): string => {
|
||||
return this.context.i18n.t(account.accountTypeLabel);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted account normal.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected accountNormalFormatted = (account: Account): string => {
|
||||
return this.context.i18n.t(account.accountNormalFormatted);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the accounts collection to flat or nested array.
|
||||
* @param {IAccount[]}
|
||||
* @returns {IAccount[]}
|
||||
*/
|
||||
protected postCollectionTransform = (accounts: Account[]) => {
|
||||
// Transfom the flatten to accounts tree.
|
||||
const transformed = flatToNestedArray(accounts, {
|
||||
id: 'id',
|
||||
parentId: 'parentAccountId',
|
||||
});
|
||||
// Associate `accountLevel` attr to indicate object depth.
|
||||
const transformed2 = assocDepthLevelToObjectTree(
|
||||
transformed,
|
||||
1,
|
||||
'accountLevel',
|
||||
);
|
||||
return this.options.structure === IAccountsStructureType.Flat
|
||||
? nestedArrayToFlatten(transformed2)
|
||||
: transformed2;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Transformer } from '../Transformer/Transformer';
|
||||
import { AccountTransaction } from './models/AccountTransaction.model';
|
||||
|
||||
export class AccountTransactionTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'date',
|
||||
'formattedDate',
|
||||
'transactionType',
|
||||
'transactionId',
|
||||
'transactionTypeFormatted',
|
||||
'credit',
|
||||
'debit',
|
||||
'formattedCredit',
|
||||
'formattedDebit',
|
||||
'fcCredit',
|
||||
'fcDebit',
|
||||
'formattedFcCredit',
|
||||
'formattedFcDebit',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes of the model.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted date.
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedDate(transaction: AccountTransaction) {
|
||||
return this.formatDate(transaction.date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted transaction type.
|
||||
* @returns {string}
|
||||
*/
|
||||
public transactionTypeFormatted(transaction: AccountTransaction) {
|
||||
return this.context.i18n.t(transaction.referenceTypeFormatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tranasction type.
|
||||
* @returns {string}
|
||||
*/
|
||||
public transactionType(transaction: AccountTransaction) {
|
||||
return transaction.referenceType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the transaction id.
|
||||
* @returns {number}
|
||||
*/
|
||||
public transactionId(transaction: AccountTransaction) {
|
||||
return transaction.referenceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the credit amount.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCredit(transaction: AccountTransaction) {
|
||||
return this.formatMoney(transaction.credit, {
|
||||
excerptZero: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the credit amount.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDebit(transaction: AccountTransaction) {
|
||||
return this.formatMoney(transaction.debit, {
|
||||
excerptZero: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the foreign credit amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
protected fcCredit(transaction: AccountTransaction) {
|
||||
return transaction.credit * transaction.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the foreign debit amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
protected fcDebit(transaction: AccountTransaction) {
|
||||
return transaction.debit * transaction.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted foreign credit amount.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedFcCredit(transaction: AccountTransaction) {
|
||||
return this.formatMoney(this.fcCredit(transaction), {
|
||||
currencyCode: transaction.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted foreign debit amount.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedFcDebit(transaction: AccountTransaction) {
|
||||
return this.formatMoney(this.fcDebit(transaction), {
|
||||
currencyCode: transaction.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
637
packages/server/src/modules/Accounts/Accounts.constants.ts
Normal file
637
packages/server/src/modules/Accounts/Accounts.constants.ts
Normal file
@@ -0,0 +1,637 @@
|
||||
export const OtherExpensesAccount = {
|
||||
name: 'Other Expenses',
|
||||
slug: 'other-expenses',
|
||||
accountType: 'other-expense',
|
||||
code: '40011',
|
||||
description: '',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const TaxPayableAccount = {
|
||||
name: 'Tax Payable',
|
||||
slug: 'tax-payable',
|
||||
accountType: 'tax-payable',
|
||||
code: '20006',
|
||||
description: '',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const UnearnedRevenueAccount = {
|
||||
name: 'Unearned Revenue',
|
||||
slug: 'unearned-revenue',
|
||||
accountType: 'other-current-liability',
|
||||
parentAccountId: null,
|
||||
code: '50005',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const PrepardExpenses = {
|
||||
name: 'Prepaid Expenses',
|
||||
slug: 'prepaid-expenses',
|
||||
accountType: 'other-current-asset',
|
||||
parentAccountId: null,
|
||||
code: '100010',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const StripeClearingAccount = {
|
||||
name: 'Stripe Clearing',
|
||||
slug: 'stripe-clearing',
|
||||
accountType: 'other-current-asset',
|
||||
parentAccountId: null,
|
||||
code: '100020',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const DiscountExpenseAccount = {
|
||||
name: 'Discount',
|
||||
slug: 'discount',
|
||||
accountType: 'other-income',
|
||||
code: '40008',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const PurchaseDiscountAccount = {
|
||||
name: 'Purchase Discount',
|
||||
slug: 'purchase-discount',
|
||||
accountType: 'other-expense',
|
||||
code: '40009',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const OtherChargesAccount = {
|
||||
name: 'Other Charges',
|
||||
slug: 'other-charges',
|
||||
accountType: 'other-income',
|
||||
code: '40010',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const SeedAccounts = [
|
||||
{
|
||||
name: 'Bank Account',
|
||||
slug: 'bank-account',
|
||||
accountType: 'bank',
|
||||
code: '10001',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: 'Saving Bank Account',
|
||||
slug: 'saving-bank-account',
|
||||
account_type: 'bank',
|
||||
code: '10002',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name: 'Undeposited Funds',
|
||||
slug: 'undeposited-funds',
|
||||
account_type: 'cash',
|
||||
code: '10003',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: 'Petty Cash',
|
||||
slug: 'petty-cash',
|
||||
account_type: 'cash',
|
||||
code: '10004',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: 'Computer Equipment',
|
||||
slug: 'computer-equipment',
|
||||
code: '10005',
|
||||
account_type: 'fixed-asset',
|
||||
predefined: 0,
|
||||
parent_account_id: null,
|
||||
index: 1,
|
||||
active: 1,
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Office Equipment',
|
||||
slug: 'office-equipment',
|
||||
code: '10006',
|
||||
account_type: 'fixed-asset',
|
||||
predefined: 0,
|
||||
parent_account_id: null,
|
||||
index: 1,
|
||||
active: 1,
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Accounts Receivable (A/R)',
|
||||
slug: 'accounts-receivable',
|
||||
account_type: 'accounts-receivable',
|
||||
code: '10007',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: 'Inventory Asset',
|
||||
slug: 'inventory-asset',
|
||||
code: '10008',
|
||||
account_type: 'inventory',
|
||||
predefined: 1,
|
||||
parent_account_id: null,
|
||||
index: 1,
|
||||
active: 1,
|
||||
description:
|
||||
'An account that holds valuation of products or goods that available for sale.',
|
||||
},
|
||||
|
||||
// Libilities
|
||||
{
|
||||
name: 'Accounts Payable (A/P)',
|
||||
slug: 'accounts-payable',
|
||||
account_type: 'accounts-payable',
|
||||
parent_account_id: null,
|
||||
code: '20001',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: 'Owner A Drawings',
|
||||
slug: 'owner-drawings',
|
||||
account_type: 'other-current-liability',
|
||||
parent_account_id: null,
|
||||
code: '20002',
|
||||
description: 'Withdrawals by the owners.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name: 'Loan',
|
||||
slug: 'owner-drawings',
|
||||
account_type: 'other-current-liability',
|
||||
code: '20003',
|
||||
description: 'Money that has been borrowed from a creditor.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name: 'Opening Balance Liabilities',
|
||||
slug: 'opening-balance-liabilities',
|
||||
account_type: 'other-current-liability',
|
||||
code: '20004',
|
||||
description:
|
||||
'This account will hold the difference in the debits and credits entered during the opening balance..',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name: 'Revenue Received in Advance',
|
||||
slug: 'revenue-received-in-advance',
|
||||
account_type: 'other-current-liability',
|
||||
parent_account_id: null,
|
||||
code: '20005',
|
||||
description: 'When customers pay in advance for products/services.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
TaxPayableAccount,
|
||||
|
||||
// Equity
|
||||
{
|
||||
name: 'Retained Earnings',
|
||||
slug: 'retained-earnings',
|
||||
account_type: 'equity',
|
||||
code: '30001',
|
||||
description:
|
||||
'Retained earnings tracks net income from previous fiscal years.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: 'Opening Balance Equity',
|
||||
slug: 'opening-balance-equity',
|
||||
account_type: 'equity',
|
||||
code: '30002',
|
||||
description:
|
||||
'When you enter opening balances to the accounts, the amounts enter in Opening balance equity. This ensures that you have a correct trial balance sheet for your company, without even specific the second credit or debit entry.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: "Owner's Equity",
|
||||
slug: 'owner-equity',
|
||||
account_type: 'equity',
|
||||
code: '30003',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: `Drawings`,
|
||||
slug: 'drawings',
|
||||
account_type: 'equity',
|
||||
code: '30003',
|
||||
description:
|
||||
'Goods purchased with the intention of selling these to customers',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
|
||||
// Expenses
|
||||
OtherExpensesAccount,
|
||||
{
|
||||
name: 'Other Expenses',
|
||||
slug: 'other-expenses',
|
||||
account_type: 'other-expense',
|
||||
parent_account_id: null,
|
||||
code: '40001',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: 'Cost of Goods Sold',
|
||||
slug: 'cost-of-goods-sold',
|
||||
account_type: 'cost-of-goods-sold',
|
||||
parent_account_id: null,
|
||||
code: '40002',
|
||||
description: 'Tracks the direct cost of the goods sold.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: 'Office expenses',
|
||||
slug: 'office-expenses',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
code: '40003',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name: 'Rent',
|
||||
slug: 'rent',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
code: '40004',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name: 'Exchange Gain or Loss',
|
||||
slug: 'exchange-grain-loss',
|
||||
account_type: 'other-expense',
|
||||
parent_account_id: null,
|
||||
code: '40005',
|
||||
description: 'Tracks the gain and losses of the exchange differences.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: 'Bank Fees and Charges',
|
||||
slug: 'bank-fees-and-charges',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
code: '40006',
|
||||
description:
|
||||
'Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name: 'Depreciation Expense',
|
||||
slug: 'depreciation-expense',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
code: '40007',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
|
||||
// Income
|
||||
{
|
||||
name: 'Sales of Product Income',
|
||||
slug: 'sales-of-product-income',
|
||||
account_type: 'income',
|
||||
predefined: 1,
|
||||
parent_account_id: null,
|
||||
code: '50001',
|
||||
index: 1,
|
||||
active: 1,
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Sales of Service Income',
|
||||
slug: 'sales-of-service-income',
|
||||
account_type: 'income',
|
||||
predefined: 0,
|
||||
parent_account_id: null,
|
||||
code: '50002',
|
||||
index: 1,
|
||||
active: 1,
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Uncategorized Income',
|
||||
slug: 'uncategorized-income',
|
||||
account_type: 'income',
|
||||
parent_account_id: null,
|
||||
code: '50003',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name: 'Other Income',
|
||||
slug: 'other-income',
|
||||
account_type: 'other-income',
|
||||
parent_account_id: null,
|
||||
code: '50004',
|
||||
description:
|
||||
'The income activities are not associated to the core business.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
UnearnedRevenueAccount,
|
||||
PrepardExpenses,
|
||||
DiscountExpenseAccount,
|
||||
PurchaseDiscountAccount,
|
||||
OtherChargesAccount,
|
||||
];
|
||||
|
||||
export const ACCOUNT_TYPE = {
|
||||
CASH: 'cash',
|
||||
BANK: 'bank',
|
||||
ACCOUNTS_RECEIVABLE: 'accounts-receivable',
|
||||
INVENTORY: 'inventory',
|
||||
OTHER_CURRENT_ASSET: 'other-current-asset',
|
||||
FIXED_ASSET: 'fixed-asset',
|
||||
NON_CURRENT_ASSET: 'none-current-asset',
|
||||
|
||||
ACCOUNTS_PAYABLE: 'accounts-payable',
|
||||
CREDIT_CARD: 'credit-card',
|
||||
TAX_PAYABLE: 'tax-payable',
|
||||
OTHER_CURRENT_LIABILITY: 'other-current-liability',
|
||||
LOGN_TERM_LIABILITY: 'long-term-liability',
|
||||
NON_CURRENT_LIABILITY: 'non-current-liability',
|
||||
|
||||
EQUITY: 'equity',
|
||||
INCOME: 'income',
|
||||
OTHER_INCOME: 'other-income',
|
||||
COST_OF_GOODS_SOLD: 'cost-of-goods-sold',
|
||||
EXPENSE: 'expense',
|
||||
OTHER_EXPENSE: 'other-expense',
|
||||
};
|
||||
|
||||
export const ACCOUNT_PARENT_TYPE = {
|
||||
CURRENT_ASSET: 'current-asset',
|
||||
FIXED_ASSET: 'fixed-asset',
|
||||
NON_CURRENT_ASSET: 'non-current-asset',
|
||||
|
||||
CURRENT_LIABILITY: 'current-liability',
|
||||
LOGN_TERM_LIABILITY: 'long-term-liability',
|
||||
NON_CURRENT_LIABILITY: 'non-current-liability',
|
||||
|
||||
EQUITY: 'equity',
|
||||
EXPENSE: 'expense',
|
||||
INCOME: 'income',
|
||||
};
|
||||
|
||||
export const ACCOUNT_ROOT_TYPE = {
|
||||
ASSET: 'asset',
|
||||
LIABILITY: 'liability',
|
||||
EQUITY: 'equity',
|
||||
EXPENSE: 'expense',
|
||||
INCOME: 'income',
|
||||
};
|
||||
|
||||
export const ACCOUNT_NORMAL = {
|
||||
CREDIT: 'credit',
|
||||
DEBIT: 'debit',
|
||||
};
|
||||
export const ACCOUNT_TYPES = [
|
||||
{
|
||||
label: 'Cash',
|
||||
key: ACCOUNT_TYPE.CASH,
|
||||
normal: ACCOUNT_NORMAL.DEBIT,
|
||||
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
|
||||
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||
multiCurrency: true,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Bank',
|
||||
key: ACCOUNT_TYPE.BANK,
|
||||
normal: ACCOUNT_NORMAL.DEBIT,
|
||||
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
|
||||
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||
multiCurrency: true,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Accounts Receivable',
|
||||
key: ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE,
|
||||
normal: ACCOUNT_NORMAL.DEBIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Inventory',
|
||||
key: ACCOUNT_TYPE.INVENTORY,
|
||||
normal: ACCOUNT_NORMAL.DEBIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Other Current Asset',
|
||||
key: ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
|
||||
normal: ACCOUNT_NORMAL.DEBIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Fixed Asset',
|
||||
key: ACCOUNT_TYPE.FIXED_ASSET,
|
||||
normal: ACCOUNT_NORMAL.DEBIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||
parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Non-Current Asset',
|
||||
key: ACCOUNT_TYPE.NON_CURRENT_ASSET,
|
||||
normal: ACCOUNT_NORMAL.DEBIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||
parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Accounts Payable',
|
||||
key: ACCOUNT_TYPE.ACCOUNTS_PAYABLE,
|
||||
normal: ACCOUNT_NORMAL.CREDIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Credit Card',
|
||||
key: ACCOUNT_TYPE.CREDIT_CARD,
|
||||
normal: ACCOUNT_NORMAL.CREDIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
|
||||
multiCurrency: true,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Tax Payable',
|
||||
key: ACCOUNT_TYPE.TAX_PAYABLE,
|
||||
normal: ACCOUNT_NORMAL.CREDIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Other Current Liability',
|
||||
key: ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY,
|
||||
normal: ACCOUNT_NORMAL.CREDIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
|
||||
balanceSheet: false,
|
||||
incomeSheet: true,
|
||||
},
|
||||
{
|
||||
label: 'Long Term Liability',
|
||||
key: ACCOUNT_TYPE.LOGN_TERM_LIABILITY,
|
||||
normal: ACCOUNT_NORMAL.CREDIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||
parentType: ACCOUNT_PARENT_TYPE.LOGN_TERM_LIABILITY,
|
||||
balanceSheet: false,
|
||||
incomeSheet: true,
|
||||
},
|
||||
{
|
||||
label: 'Non-Current Liability',
|
||||
key: ACCOUNT_TYPE.NON_CURRENT_LIABILITY,
|
||||
normal: ACCOUNT_NORMAL.CREDIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||
parentType: ACCOUNT_PARENT_TYPE.NON_CURRENT_LIABILITY,
|
||||
balanceSheet: false,
|
||||
incomeSheet: true,
|
||||
},
|
||||
{
|
||||
label: 'Equity',
|
||||
key: ACCOUNT_TYPE.EQUITY,
|
||||
normal: ACCOUNT_NORMAL.CREDIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.EQUITY,
|
||||
parentType: ACCOUNT_PARENT_TYPE.EQUITY,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
{
|
||||
label: 'Income',
|
||||
key: ACCOUNT_TYPE.INCOME,
|
||||
normal: ACCOUNT_NORMAL.CREDIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.INCOME,
|
||||
parentType: ACCOUNT_PARENT_TYPE.INCOME,
|
||||
balanceSheet: false,
|
||||
incomeSheet: true,
|
||||
},
|
||||
{
|
||||
label: 'Other Income',
|
||||
key: ACCOUNT_TYPE.OTHER_INCOME,
|
||||
normal: ACCOUNT_NORMAL.CREDIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.INCOME,
|
||||
parentType: ACCOUNT_PARENT_TYPE.INCOME,
|
||||
balanceSheet: false,
|
||||
incomeSheet: true,
|
||||
},
|
||||
{
|
||||
label: 'Cost of Goods Sold',
|
||||
key: ACCOUNT_TYPE.COST_OF_GOODS_SOLD,
|
||||
normal: ACCOUNT_NORMAL.DEBIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
|
||||
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
|
||||
balanceSheet: false,
|
||||
incomeSheet: true,
|
||||
},
|
||||
{
|
||||
label: 'Expense',
|
||||
key: ACCOUNT_TYPE.EXPENSE,
|
||||
normal: ACCOUNT_NORMAL.DEBIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
|
||||
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
|
||||
balanceSheet: false,
|
||||
incomeSheet: true,
|
||||
},
|
||||
{
|
||||
label: 'Other Expense',
|
||||
key: ACCOUNT_TYPE.OTHER_EXPENSE,
|
||||
normal: ACCOUNT_NORMAL.DEBIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
|
||||
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
|
||||
balanceSheet: false,
|
||||
incomeSheet: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const getAccountsSupportsMultiCurrency = () => {
|
||||
return ACCOUNT_TYPES.filter((account) => account.multiCurrency);
|
||||
};
|
||||
149
packages/server/src/modules/Accounts/Accounts.controller.ts
Normal file
149
packages/server/src/modules/Accounts/Accounts.controller.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Delete,
|
||||
Get,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { AccountsApplication } from './AccountsApplication.service';
|
||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
import { EditAccountDTO } from './EditAccount.dto';
|
||||
import { IAccountsFilter, IAccountsTransactionsFilter } from './Accounts.types';
|
||||
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@Controller('accounts')
|
||||
@ApiTags('accounts')
|
||||
export class AccountsController {
|
||||
constructor(private readonly accountsApplication: AccountsApplication) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create an account' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The account has been successfully created.',
|
||||
})
|
||||
async createAccount(@Body() accountDTO: CreateAccountDTO) {
|
||||
return this.accountsApplication.createAccount(accountDTO);
|
||||
}
|
||||
|
||||
@Post(':id')
|
||||
@ApiOperation({ summary: 'Edit the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The account has been successfully updated.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The account not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The account id',
|
||||
})
|
||||
async editAccount(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() accountDTO: EditAccountDTO,
|
||||
) {
|
||||
return this.accountsApplication.editAccount(id, accountDTO);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The account has been successfully deleted.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The account not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The account id',
|
||||
})
|
||||
async deleteAccount(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.accountsApplication.deleteAccount(id);
|
||||
}
|
||||
|
||||
@Post(':id/activate')
|
||||
@ApiOperation({ summary: 'Activate the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The account has been successfully activated.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The account not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The account id',
|
||||
})
|
||||
async activateAccount(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.accountsApplication.activateAccount(id);
|
||||
}
|
||||
|
||||
@Post(':id/inactivate')
|
||||
@ApiOperation({ summary: 'Inactivate the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The account has been successfully inactivated.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The account not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The account id',
|
||||
})
|
||||
async inactivateAccount(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.accountsApplication.inactivateAccount(id);
|
||||
}
|
||||
|
||||
@Get('types')
|
||||
@ApiOperation({ summary: 'Retrieves the account types.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The account types have been successfully retrieved.',
|
||||
})
|
||||
async getAccountTypes() {
|
||||
return this.accountsApplication.getAccountTypes();
|
||||
}
|
||||
|
||||
@Get('transactions')
|
||||
@ApiOperation({ summary: 'Retrieves the account transactions.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The account transactions have been successfully retrieved.',
|
||||
})
|
||||
async getAccountTransactions(@Query() filter: IAccountsTransactionsFilter) {
|
||||
return this.accountsApplication.getAccountsTransactions(filter);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Retrieves the account details.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The account details have been successfully retrieved.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The account not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The account id',
|
||||
})
|
||||
async getAccount(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.accountsApplication.getAccount(id);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Retrieves the accounts.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The accounts have been successfully retrieved.',
|
||||
})
|
||||
async getAccounts(@Query() filter: IAccountsFilter) {
|
||||
return this.accountsApplication.getAccounts(filter);
|
||||
}
|
||||
}
|
||||
44
packages/server/src/modules/Accounts/Accounts.module.ts
Normal file
44
packages/server/src/modules/Accounts/Accounts.module.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||
import { AccountsController } from './Accounts.controller';
|
||||
import { AccountsApplication } from './AccountsApplication.service';
|
||||
import { CreateAccountService } from './CreateAccount.service';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
||||
import { AccountRepository } from './repositories/Account.repository';
|
||||
import { EditAccount } from './EditAccount.service';
|
||||
import { DeleteAccount } from './DeleteAccount.service';
|
||||
import { GetAccount } from './GetAccount.service';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { ActivateAccount } from './ActivateAccount.service';
|
||||
import { GetAccountTypesService } from './GetAccountTypes.service';
|
||||
import { GetAccountTransactionsService } from './GetAccountTransactions.service';
|
||||
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
|
||||
import { BankAccount } from '../BankingTransactions/models/BankAccount';
|
||||
import { GetAccountsService } from './GetAccounts.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
// import { GetAccountsService } from './GetAccounts.service';
|
||||
|
||||
const models = [RegisterTenancyModel(BankAccount)];
|
||||
|
||||
@Module({
|
||||
imports: [TenancyDatabaseModule, DynamicListModule, ...models],
|
||||
controllers: [AccountsController],
|
||||
providers: [
|
||||
AccountsApplication,
|
||||
CreateAccountService,
|
||||
TenancyContext,
|
||||
CommandAccountValidators,
|
||||
AccountRepository,
|
||||
EditAccount,
|
||||
DeleteAccount,
|
||||
GetAccount,
|
||||
TransformerInjectable,
|
||||
ActivateAccount,
|
||||
GetAccountTypesService,
|
||||
GetAccountTransactionsService,
|
||||
GetAccountsService,
|
||||
],
|
||||
exports: [AccountRepository, CreateAccountService, ...models],
|
||||
})
|
||||
export class AccountsModule {}
|
||||
92
packages/server/src/modules/Accounts/Accounts.types.ts
Normal file
92
packages/server/src/modules/Accounts/Accounts.types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Account } from './models/Account.model';
|
||||
// import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
|
||||
|
||||
export enum AccountNormal {
|
||||
DEBIT = 'debit',
|
||||
CREDIT = 'credit',
|
||||
}
|
||||
|
||||
export interface IAccountsTransactionsFilter {
|
||||
accountId?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export enum IAccountsStructureType {
|
||||
Tree = 'tree',
|
||||
Flat = 'flat',
|
||||
}
|
||||
|
||||
// export interface IAccountsFilter extends IDynamicListFilterDTO {
|
||||
// }
|
||||
export interface IAccountsFilter {
|
||||
onlyInactive: boolean;
|
||||
structure?: IAccountsStructureType;
|
||||
}
|
||||
export interface IAccountType {
|
||||
label: string;
|
||||
key: string;
|
||||
normal: string;
|
||||
rootType: string;
|
||||
childType: string;
|
||||
balanceSheet: boolean;
|
||||
incomeSheet: boolean;
|
||||
}
|
||||
|
||||
export interface IAccountsTypesService {
|
||||
getAccountsTypes(): Promise<IAccountType>;
|
||||
}
|
||||
|
||||
export interface IAccountEventCreatingPayload {
|
||||
accountDTO: any;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IAccountEventCreatedPayload {
|
||||
account: Account;
|
||||
accountId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IAccountEventEditedPayload {
|
||||
account: Account;
|
||||
oldAccount: Account;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IAccountEventDeletedPayload {
|
||||
accountId: number;
|
||||
oldAccount: Account;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IAccountEventDeletePayload {
|
||||
trx: Knex.Transaction;
|
||||
oldAccount: Account;
|
||||
}
|
||||
|
||||
export interface IAccountEventActivatedPayload {
|
||||
accountId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export enum AccountAction {
|
||||
CREATE = 'Create',
|
||||
EDIT = 'Edit',
|
||||
DELETE = 'Delete',
|
||||
VIEW = 'View',
|
||||
TransactionsLocking = 'TransactionsLocking',
|
||||
}
|
||||
|
||||
export enum TaxRateAction {
|
||||
CREATE = 'Create',
|
||||
EDIT = 'Edit',
|
||||
DELETE = 'Delete',
|
||||
VIEW = 'View',
|
||||
}
|
||||
|
||||
export interface CreateAccountParams {
|
||||
ignoreUniqueName: boolean;
|
||||
}
|
||||
|
||||
export interface IGetAccountTransactionPOJO {}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateAccountService } from './CreateAccount.service';
|
||||
import { DeleteAccount } from './DeleteAccount.service';
|
||||
import { EditAccount } from './EditAccount.service';
|
||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
import { Account } from './models/Account.model';
|
||||
import { EditAccountDTO } from './EditAccount.dto';
|
||||
import { GetAccount } from './GetAccount.service';
|
||||
import { ActivateAccount } from './ActivateAccount.service';
|
||||
import { GetAccountTypesService } from './GetAccountTypes.service';
|
||||
import { GetAccountTransactionsService } from './GetAccountTransactions.service';
|
||||
import {
|
||||
IAccountsFilter,
|
||||
IAccountsTransactionsFilter,
|
||||
IGetAccountTransactionPOJO,
|
||||
} from './Accounts.types';
|
||||
import { GetAccountsService } from './GetAccounts.service';
|
||||
import { IFilterMeta } from '@/interfaces/Model';
|
||||
|
||||
@Injectable()
|
||||
export class AccountsApplication {
|
||||
/**
|
||||
* @param {CreateAccountService} createAccountService - The create account service.
|
||||
* @param {EditAccount} editAccountService - The edit account service.
|
||||
* @param {DeleteAccount} deleteAccountService - The delete account service.
|
||||
* @param {ActivateAccount} activateAccountService - The activate account service.
|
||||
* @param {GetAccountTypesService} getAccountTypesService - The get account types service.
|
||||
* @param {GetAccount} getAccountService - The get account service.
|
||||
* @param {GetAccountTransactionsService} getAccountTransactionsService - The get account transactions service.
|
||||
* @param {GetAccountsService} getAccountsService - The get accounts service.
|
||||
*/
|
||||
constructor(
|
||||
private readonly createAccountService: CreateAccountService,
|
||||
private readonly editAccountService: EditAccount,
|
||||
private readonly deleteAccountService: DeleteAccount,
|
||||
private readonly activateAccountService: ActivateAccount,
|
||||
private readonly getAccountTypesService: GetAccountTypesService,
|
||||
private readonly getAccountService: GetAccount,
|
||||
private readonly getAccountTransactionsService: GetAccountTransactionsService,
|
||||
private readonly getAccountsService: GetAccountsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new account.
|
||||
* @param {number} tenantId
|
||||
* @param {IAccountCreateDTO} accountDTO
|
||||
* @returns {Promise<IAccount>}
|
||||
*/
|
||||
public createAccount = (
|
||||
accountDTO: CreateAccountDTO,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<Account> => {
|
||||
return this.createAccountService.createAccount(accountDTO, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given account.
|
||||
* @param {number} tenantId
|
||||
* @param {number} accountId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteAccount = (accountId: number) => {
|
||||
return this.deleteAccountService.deleteAccount(accountId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits the given account.
|
||||
* @param {number} tenantId
|
||||
* @param {number} accountId
|
||||
* @param {IAccountEditDTO} accountDTO
|
||||
* @returns
|
||||
*/
|
||||
public editAccount = (accountId: number, accountDTO: EditAccountDTO) => {
|
||||
return this.editAccountService.editAccount(accountId, accountDTO);
|
||||
};
|
||||
|
||||
/**
|
||||
* Activate the given account.
|
||||
* @param {number} accountId - Account id.
|
||||
*/
|
||||
public activateAccount = (accountId: number) => {
|
||||
return this.activateAccountService.activateAccount(accountId, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inactivate the given account.
|
||||
* @param {number} accountId - Account id.
|
||||
*/
|
||||
public inactivateAccount = (accountId: number) => {
|
||||
return this.activateAccountService.activateAccount(accountId, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the account details.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} accountId - Account id.
|
||||
* @returns {Promise<IAccount>}
|
||||
*/
|
||||
public getAccount = (accountId: number) => {
|
||||
return this.getAccountService.getAccount(accountId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves all account types.
|
||||
* @returns {Promise<IAccountType[]>}
|
||||
*/
|
||||
public getAccountTypes = () => {
|
||||
return this.getAccountTypesService.getAccountsTypes();
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the accounts list.
|
||||
* @param {IAccountsFilter} filterDTO - Filter DTO.
|
||||
* @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
|
||||
*/
|
||||
public getAccounts = (
|
||||
filterDTO: IAccountsFilter,
|
||||
): Promise<{ accounts: Account[]; filterMeta: IFilterMeta }> => {
|
||||
return this.getAccountsService.getAccountsList(filterDTO);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the given account transactions.
|
||||
* @param {IAccountsTransactionsFilter} filter
|
||||
* @returns {Promise<IGetAccountTransactionPOJO[]>}
|
||||
*/
|
||||
public getAccountsTransactions = (
|
||||
filter: IAccountsTransactionsFilter,
|
||||
): Promise<IGetAccountTransactionPOJO[]> => {
|
||||
return this.getAccountTransactionsService.getAccountsTransactions(filter);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AccountsApplication } from './AccountsApplication.service';
|
||||
import { Exportable } from '../Export/Exportable';
|
||||
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||
import { IAccountsFilter, IAccountsStructureType } from './Accounts.types';
|
||||
|
||||
export class AccountsExportable extends Exportable {
|
||||
constructor(private readonly accountsApplication: AccountsApplication) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the accounts data to exportable sheet.
|
||||
*/
|
||||
public exportable(query: IAccountsFilter) {
|
||||
const parsedQuery = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
inactiveMode: false,
|
||||
...query,
|
||||
structure: IAccountsStructureType.Flat,
|
||||
pageSize: EXPORT_SIZE_LIMIT,
|
||||
page: 1,
|
||||
} as IAccountsFilter;
|
||||
|
||||
return this.accountsApplication
|
||||
.getAccounts(parsedQuery)
|
||||
.then((output) => output.accounts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
export const AccountsSampleData = [
|
||||
{
|
||||
'Account Name': 'Utilities Expense',
|
||||
'Account Code': 9000,
|
||||
Type: 'Expense',
|
||||
Description: 'Omnis voluptatum consequatur.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
{
|
||||
'Account Name': 'Unearned Revenue',
|
||||
'Account Code': 9010,
|
||||
Type: 'Long Term Liability',
|
||||
Description: 'Autem odit voluptas nihil unde.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
{
|
||||
'Account Name': 'Long-Term Debt',
|
||||
'Account Code': 9020,
|
||||
Type: 'Long Term Liability',
|
||||
Description: 'In voluptas cumque exercitationem.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
{
|
||||
'Account Name': 'Salaries and Wages Expense',
|
||||
'Account Code': 9030,
|
||||
Type: 'Expense',
|
||||
Description: 'Assumenda aspernatur soluta aliquid perspiciatis quasi.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
{
|
||||
'Account Name': 'Rental Income',
|
||||
'Account Code': 9040,
|
||||
Type: 'Income',
|
||||
Description: 'Omnis possimus amet occaecati inventore.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
{
|
||||
'Account Name': 'Paypal',
|
||||
'Account Code': 9050,
|
||||
Type: 'Bank',
|
||||
Description: 'In voluptas cumque exercitationem.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,45 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { Knex } from 'knex';
|
||||
// import { IAccountCreateDTO } from '@/interfaces';
|
||||
// import { CreateAccount } from './CreateAccount.service';
|
||||
// import { Importable } from '../Import/Importable';
|
||||
// import { AccountsSampleData } from './AccountsImportable.SampleData';
|
||||
|
||||
// @Service()
|
||||
// export class AccountsImportable extends Importable {
|
||||
// @Inject()
|
||||
// private createAccountService: CreateAccount;
|
||||
|
||||
// /**
|
||||
// * Importing to account service.
|
||||
// * @param {number} tenantId
|
||||
// * @param {IAccountCreateDTO} createAccountDTO
|
||||
// * @returns
|
||||
// */
|
||||
// public importable(
|
||||
// tenantId: number,
|
||||
// createAccountDTO: IAccountCreateDTO,
|
||||
// trx?: Knex.Transaction
|
||||
// ) {
|
||||
// return this.createAccountService.createAccount(
|
||||
// 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 AccountsSampleData;
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { IAccountEventActivatedPayload } from './Accounts.types';
|
||||
import { Account } from './models/Account.model';
|
||||
import { AccountRepository } from './repositories/Account.repository';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class ActivateAccount {
|
||||
/**
|
||||
* @param {EventEmitter2} eventEmitter - The event emitter.
|
||||
* @param {UnitOfWork} uow - The unit of work.
|
||||
* @param {AccountRepository} accountRepository - The account repository.
|
||||
* @param {TenantModelProxy<typeof Account>} accountModel - The account model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Activates/Inactivates the given account.
|
||||
* @param {number} accountId - The account id.
|
||||
* @param {boolean} activate - Activate or inactivate the account.
|
||||
*/
|
||||
public activateAccount = async (accountId: number, activate?: boolean) => {
|
||||
// Retrieve the given account or throw not found error.
|
||||
const oldAccount = await this.accountModel()
|
||||
.query()
|
||||
.findById(accountId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Get all children accounts.
|
||||
const accountsGraph = await this.accountRepository.getDependencyGraph();
|
||||
const dependenciesAccounts = accountsGraph.dependenciesOf(accountId);
|
||||
|
||||
const patchAccountsIds = [...dependenciesAccounts, accountId];
|
||||
|
||||
// Activate account and associated transactions under unit-of-work environment.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Activate and inactivate the given accounts ids.
|
||||
activate
|
||||
? await this.accountRepository.activateByIds(patchAccountsIds, trx)
|
||||
: await this.accountRepository.inactivateByIds(patchAccountsIds, trx);
|
||||
|
||||
// Triggers `onAccountActivated` event.
|
||||
this.eventEmitter.emitAsync(events.accounts.onActivated, {
|
||||
accountId,
|
||||
trx,
|
||||
} as IAccountEventActivatedPayload);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// @ts-nocheck
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
// import { IAccountDTO, IAccount, IAccountCreateDTO } from './Accounts.types';
|
||||
// import AccountTypesUtils from '@/lib/AccountTypes';
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
import { ERRORS, MAX_ACCOUNTS_CHART_DEPTH } from './constants';
|
||||
import { Account } from './models/Account.model';
|
||||
import { AccountRepository } from './repositories/Account.repository';
|
||||
import { AccountTypesUtils } from './utils/AccountType.utils';
|
||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
import { EditAccountDTO } from './EditAccount.dto';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class CommandAccountValidators {
|
||||
constructor(
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Throws error if the account was prefined.
|
||||
* @param {Account} account
|
||||
*/
|
||||
public throwErrorIfAccountPredefined(account: Account) {
|
||||
if (account.predefined) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_PREDEFINED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff account type between new and old account, throw service error
|
||||
* if they have different account type.
|
||||
* @param {Account|CreateAccountDTO|EditAccountDTO} oldAccount
|
||||
* @param {Account|CreateAccountDTO|EditAccountDTO} newAccount
|
||||
*/
|
||||
public async isAccountTypeChangedOrThrowError(
|
||||
oldAccount: Account | CreateAccountDTO | EditAccountDTO,
|
||||
newAccount: Account | CreateAccountDTO | EditAccountDTO,
|
||||
) {
|
||||
if (oldAccount.accountType !== newAccount.accountType) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account type or throws service error.
|
||||
* @param {string} accountTypeKey -
|
||||
* @return {IAccountType}
|
||||
*/
|
||||
public getAccountTypeOrThrowError(accountTypeKey: string) {
|
||||
const accountType = AccountTypesUtils.getType(accountTypeKey);
|
||||
|
||||
if (!accountType) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_FOUND);
|
||||
}
|
||||
return accountType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve parent account or throw service error.
|
||||
* @param {number} accountId - Account id.
|
||||
* @param {number} notAccountId - Ignore the account id.
|
||||
*/
|
||||
public async getParentAccountOrThrowError(
|
||||
accountId: number,
|
||||
notAccountId?: number,
|
||||
) {
|
||||
const parentAccount = await this.accountModel()
|
||||
.query()
|
||||
.findById(accountId)
|
||||
.onBuild((query) => {
|
||||
if (notAccountId) {
|
||||
query.whereNot('id', notAccountId);
|
||||
}
|
||||
});
|
||||
if (!parentAccount) {
|
||||
throw new ServiceError(ERRORS.PARENT_ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
return parentAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if the account type was not unique on the storage.
|
||||
* @param {string} accountCode - Account code.
|
||||
* @param {number} notAccountId - Ignore the account id.
|
||||
*/
|
||||
public async isAccountCodeUniqueOrThrowError(
|
||||
accountCode: string,
|
||||
notAccountId?: number,
|
||||
) {
|
||||
const account = await this.accountModel()
|
||||
.query()
|
||||
.where('code', accountCode)
|
||||
.onBuild((query) => {
|
||||
if (notAccountId) {
|
||||
query.whereNot('id', notAccountId);
|
||||
}
|
||||
});
|
||||
if (account.length > 0) {
|
||||
throw new ServiceError(
|
||||
ERRORS.ACCOUNT_CODE_NOT_UNIQUE,
|
||||
'Account code is not unique.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the account name uniquiness.
|
||||
* @param {string} accountName - Account name.
|
||||
* @param {number} notAccountId - Ignore the account id.
|
||||
*/
|
||||
public async validateAccountNameUniquiness(
|
||||
accountName: string,
|
||||
notAccountId?: number,
|
||||
) {
|
||||
const foundAccount = await this.accountModel
|
||||
.query()
|
||||
.findOne('name', accountName)
|
||||
.onBuild((query) => {
|
||||
if (notAccountId) {
|
||||
query.whereNot('id', notAccountId);
|
||||
}
|
||||
});
|
||||
if (foundAccount) {
|
||||
throw new ServiceError(
|
||||
ERRORS.ACCOUNT_NAME_NOT_UNIQUE,
|
||||
'Account name is not unique.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given account type supports multi-currency.
|
||||
* @param {CreateAccountDTO | EditAccountDTO} accountDTO -
|
||||
*/
|
||||
public validateAccountTypeSupportCurrency = (
|
||||
accountDTO: CreateAccountDTO | EditAccountDTO,
|
||||
baseCurrency: string,
|
||||
) => {
|
||||
// Can't continue to validate the type has multi-currency feature
|
||||
// if the given currency equals the base currency or not assigned.
|
||||
if (accountDTO.currencyCode === baseCurrency || !accountDTO.currencyCode) {
|
||||
return;
|
||||
}
|
||||
const meta = AccountTypesUtils.getType(accountDTO.accountType);
|
||||
|
||||
// Throw error if the account type does not support multi-currency.
|
||||
if (!meta?.multiCurrency) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the account DTO currency code whether equals the currency code of
|
||||
* parent account.
|
||||
* @param {CreateAccountDTO | EditAccountDTO} accountDTO
|
||||
* @param {Account} parentAccount
|
||||
* @param {string} baseCurrency -
|
||||
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
|
||||
*/
|
||||
public validateCurrentSameParentAccount = (
|
||||
accountDTO: CreateAccountDTO | EditAccountDTO,
|
||||
parentAccount: Account,
|
||||
baseCurrency: string,
|
||||
) => {
|
||||
// If the account DTO currency not assigned and the parent account has no base currency.
|
||||
if (
|
||||
!accountDTO.currencyCode &&
|
||||
parentAccount.currencyCode !== baseCurrency
|
||||
) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT);
|
||||
}
|
||||
// If the account DTO is assigned and not equals the currency code of parent account.
|
||||
if (
|
||||
accountDTO.currencyCode &&
|
||||
parentAccount.currencyCode !== accountDTO.currencyCode
|
||||
) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws service error if parent account has different type.
|
||||
* @param {IAccountDTO} accountDTO
|
||||
* @param {IAccount} parentAccount
|
||||
*/
|
||||
public throwErrorIfParentHasDiffType(
|
||||
accountDTO: CreateAccountDTO | EditAccountDTO,
|
||||
parentAccount: Account,
|
||||
) {
|
||||
if (accountDTO.accountType !== parentAccount.accountType) {
|
||||
throw new ServiceError(ERRORS.PARENT_ACCOUNT_HAS_DIFFERENT_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account of throw service error in case account not found.
|
||||
* @param {number} accountId
|
||||
* @return {IAccount}
|
||||
*/
|
||||
public async getAccountOrThrowError(accountId: number) {
|
||||
const account = await this.accountRepository.findOneById(accountId);
|
||||
|
||||
if (!account) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_NOT_FOUND);
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the max depth level of accounts chart.
|
||||
* @param {number} parentAccountId - Parent account id.
|
||||
*/
|
||||
public async validateMaxParentAccountDepthLevels(parentAccountId: number) {
|
||||
const accountsGraph = await this.accountRepository.getDependencyGraph();
|
||||
const parentDependantsIds = accountsGraph.dependantsOf(parentAccountId);
|
||||
|
||||
if (parentDependantsIds.length >= MAX_ACCOUNTS_CHART_DEPTH) {
|
||||
throw new ServiceError(ERRORS.PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL);
|
||||
}
|
||||
}
|
||||
}
|
||||
103
packages/server/src/modules/Accounts/CreateAccount.dto.ts
Normal file
103
packages/server/src/modules/Accounts/CreateAccount.dto.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsInt,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
IsBoolean,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateAccountDTO {
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@MaxLength(255) // Assuming DATATYPES_LENGTH.STRING is 255
|
||||
@ApiProperty({
|
||||
description: 'Account name',
|
||||
example: 'Cash Account',
|
||||
minLength: 3,
|
||||
maxLength: 255,
|
||||
})
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@MaxLength(6)
|
||||
@ApiProperty({
|
||||
description: 'Account code',
|
||||
example: 'CA001',
|
||||
required: false,
|
||||
minLength: 3,
|
||||
maxLength: 6,
|
||||
})
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Currency code for the account',
|
||||
example: 'USD',
|
||||
required: false,
|
||||
})
|
||||
currencyCode?: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@MaxLength(255)
|
||||
@ApiProperty({
|
||||
description: 'Type of account',
|
||||
example: 'asset',
|
||||
minLength: 3,
|
||||
maxLength: 255,
|
||||
})
|
||||
accountType: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(65535)
|
||||
@ApiProperty({
|
||||
description: 'Account description',
|
||||
example: 'Main cash account for daily operations',
|
||||
required: false,
|
||||
maxLength: 65535,
|
||||
})
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({
|
||||
description: 'ID of the parent account',
|
||||
example: 1,
|
||||
required: false,
|
||||
})
|
||||
parentAccountId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiProperty({
|
||||
description: 'Whether the account is active',
|
||||
example: true,
|
||||
required: false,
|
||||
default: true,
|
||||
})
|
||||
active?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Plaid account ID for syncing',
|
||||
example: 'plaid_account_123456',
|
||||
required: false,
|
||||
})
|
||||
plaidAccountId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Plaid item ID for syncing',
|
||||
example: 'plaid_item_123456',
|
||||
required: false,
|
||||
})
|
||||
plaidItemId?: string;
|
||||
}
|
||||
147
packages/server/src/modules/Accounts/CreateAccount.service.ts
Normal file
147
packages/server/src/modules/Accounts/CreateAccount.service.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { kebabCase } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
IAccountEventCreatingPayload,
|
||||
CreateAccountParams,
|
||||
IAccountEventCreatedPayload,
|
||||
} from './Accounts.types';
|
||||
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
||||
import { Account } from './models/Account.model';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
import { PartialModelObject } from 'objection';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class CreateAccountService {
|
||||
/**
|
||||
* @param {TenantModelProxy<typeof Account>} accountModel - The account model proxy.
|
||||
* @param {EventEmitter2} eventEmitter - The event emitter.
|
||||
* @param {UnitOfWork} uow - The unit of work.
|
||||
* @param {CommandAccountValidators} validator - The command account validators.
|
||||
* @param {TenancyContext} tenancyContext - The tenancy context.
|
||||
*/
|
||||
constructor(
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandAccountValidators,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize the account creation.
|
||||
* @param {CreateAccountDTO} accountDTO
|
||||
*/
|
||||
private authorize = async (
|
||||
accountDTO: CreateAccountDTO,
|
||||
baseCurrency: string,
|
||||
params?: CreateAccountParams,
|
||||
) => {
|
||||
// Validate account name uniquiness.
|
||||
if (!params.ignoreUniqueName) {
|
||||
await this.validator.validateAccountNameUniquiness(accountDTO.name);
|
||||
}
|
||||
// Validate the account code uniquiness.
|
||||
if (accountDTO.code) {
|
||||
await this.validator.isAccountCodeUniqueOrThrowError(accountDTO.code);
|
||||
}
|
||||
// Retrieve the account type meta or throw service error if not found.
|
||||
this.validator.getAccountTypeOrThrowError(accountDTO.accountType);
|
||||
|
||||
// Ingore the parent account validation if not presented.
|
||||
if (accountDTO.parentAccountId) {
|
||||
const parentAccount = await this.validator.getParentAccountOrThrowError(
|
||||
accountDTO.parentAccountId,
|
||||
);
|
||||
this.validator.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
|
||||
|
||||
// Inherit active status from parent account.
|
||||
accountDTO.active = parentAccount.active;
|
||||
|
||||
// Validate should currency code be the same currency of parent account.
|
||||
this.validator.validateCurrentSameParentAccount(
|
||||
accountDTO,
|
||||
parentAccount,
|
||||
baseCurrency,
|
||||
);
|
||||
// Validates the max depth level of accounts chart.
|
||||
await this.validator.validateMaxParentAccountDepthLevels(
|
||||
accountDTO.parentAccountId,
|
||||
);
|
||||
}
|
||||
// Validates the given account type supports the multi-currency.
|
||||
this.validator.validateAccountTypeSupportCurrency(accountDTO, baseCurrency);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the create account DTO to input model.
|
||||
* @param {IAccountCreateDTO} createAccountDTO
|
||||
*/
|
||||
private transformDTOToModel = (
|
||||
createAccountDTO: CreateAccountDTO,
|
||||
baseCurrency: string,
|
||||
): PartialModelObject<Account> => {
|
||||
return {
|
||||
...createAccountDTO,
|
||||
slug: kebabCase(createAccountDTO.name),
|
||||
currencyCode: createAccountDTO.currencyCode || baseCurrency,
|
||||
|
||||
// Mark the account is Plaid owner since Plaid item/account is defined on creating.
|
||||
isSyncingOwner: Boolean(
|
||||
createAccountDTO.plaidAccountId || createAccountDTO.plaidItemId,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new account on the storage.
|
||||
* @param {IAccountCreateDTO} accountDTO
|
||||
* @returns {Promise<IAccount>}
|
||||
*/
|
||||
public createAccount = async (
|
||||
accountDTO: CreateAccountDTO,
|
||||
trx?: Knex.Transaction,
|
||||
params: CreateAccountParams = { ignoreUniqueName: false },
|
||||
): Promise<Account> => {
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenant = await this.tenancyContext.getTenant(true);
|
||||
|
||||
// Authorize the account creation.
|
||||
await this.authorize(accountDTO, tenant.metadata.baseCurrency, params);
|
||||
|
||||
// Transformes the DTO to model.
|
||||
const accountInputModel = this.transformDTOToModel(
|
||||
accountDTO,
|
||||
tenant.metadata.baseCurrency,
|
||||
);
|
||||
// Creates a new account with associated transactions under unit-of-work envirement.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onAccountCreating` event.
|
||||
await this.eventEmitter.emitAsync(events.accounts.onCreating, {
|
||||
accountDTO,
|
||||
trx,
|
||||
} as IAccountEventCreatingPayload);
|
||||
|
||||
// Inserts account to the storage.
|
||||
const account = await this.accountModel()
|
||||
.query()
|
||||
.insert({
|
||||
...accountInputModel,
|
||||
});
|
||||
// Triggers `onAccountCreated` event.
|
||||
await this.eventEmitter.emitAsync(events.accounts.onCreated, {
|
||||
account,
|
||||
accountId: account.id,
|
||||
trx,
|
||||
} as IAccountEventCreatedPayload);
|
||||
|
||||
return account;
|
||||
}, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
// import { IAccountEventDeletedPayload } from '@/interfaces';
|
||||
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
||||
import { Account } from './models/Account.model';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { IAccountEventDeletedPayload } from './Accounts.types';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteAccount {
|
||||
constructor(
|
||||
@Inject(Account.name)
|
||||
private accountModel: TenantModelProxy<typeof Account>,
|
||||
|
||||
private eventEmitter: EventEmitter2,
|
||||
private uow: UnitOfWork,
|
||||
private validator: CommandAccountValidators,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize account delete.
|
||||
* @param {number} accountId - Account id.
|
||||
*/
|
||||
private authorize = async (accountId: number, oldAccount: Account) => {
|
||||
// Throw error if the account was predefined.
|
||||
this.validator.throwErrorIfAccountPredefined(oldAccount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unlink the given parent account with children accounts.
|
||||
* @param {number|number[]} parentAccountId -
|
||||
*/
|
||||
private async unassociateChildrenAccountsFromParent(
|
||||
parentAccountId: number | number[],
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const accountsIds = Array.isArray(parentAccountId)
|
||||
? parentAccountId
|
||||
: [parentAccountId];
|
||||
|
||||
await this.accountModel()
|
||||
.query(trx)
|
||||
.whereIn('parent_account_id', accountsIds)
|
||||
.patch({ parentAccountId: null });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the account from the storage.
|
||||
* @param {number} accountId
|
||||
*/
|
||||
public deleteAccount = async (accountId: number): Promise<void> => {
|
||||
// Retrieve account or not found service error.
|
||||
const oldAccount = await this.accountModel().query().findById(accountId);
|
||||
|
||||
// Authorize before delete account.
|
||||
await this.authorize(accountId, oldAccount);
|
||||
|
||||
// Deletes the account and associated transactions under UOW environment.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onAccountDelete` event.
|
||||
await this.eventEmitter.emitAsync(events.accounts.onDelete, {
|
||||
trx,
|
||||
oldAccount,
|
||||
} as IAccountEventDeletedPayload);
|
||||
|
||||
// Unlink the parent account from children accounts.
|
||||
await this.unassociateChildrenAccountsFromParent(accountId, trx);
|
||||
|
||||
// Deletes account by the given id.
|
||||
await this.accountModel().query(trx).deleteById(accountId);
|
||||
|
||||
// Triggers `onAccountDeleted` event.
|
||||
await this.eventEmitter.emitAsync(events.accounts.onDeleted, {
|
||||
accountId,
|
||||
oldAccount,
|
||||
trx,
|
||||
} as IAccountEventDeletedPayload);
|
||||
});
|
||||
};
|
||||
}
|
||||
54
packages/server/src/modules/Accounts/EditAccount.dto.ts
Normal file
54
packages/server/src/modules/Accounts/EditAccount.dto.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsInt,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class EditAccountDTO {
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@MaxLength(255)
|
||||
@ApiProperty({
|
||||
description: 'The name of the account',
|
||||
example: 'Bank Account',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@MaxLength(6)
|
||||
@ApiProperty({
|
||||
description: 'The code of the account',
|
||||
example: '123456',
|
||||
})
|
||||
code?: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@MaxLength(255)
|
||||
@ApiProperty({
|
||||
description: 'The type of the account',
|
||||
example: 'Bank Account',
|
||||
})
|
||||
accountType: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'The description of the account',
|
||||
example: 'This is a description',
|
||||
})
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({
|
||||
description: 'The parent account ID of the account',
|
||||
example: 1,
|
||||
})
|
||||
parentAccountId?: number;
|
||||
}
|
||||
101
packages/server/src/modules/Accounts/EditAccount.service.ts
Normal file
101
packages/server/src/modules/Accounts/EditAccount.service.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { CommandAccountValidators } from './CommandAccountValidators.service';
|
||||
import { Account } from './models/Account.model';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { EditAccountDTO } from './EditAccount.dto';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class EditAccount {
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandAccountValidators,
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize the account editing.
|
||||
* @param {number} accountId
|
||||
* @param {IAccountEditDTO} accountDTO
|
||||
* @param {IAccount} oldAccount -
|
||||
*/
|
||||
private authorize = async (
|
||||
accountId: number,
|
||||
accountDTO: EditAccountDTO,
|
||||
oldAccount: Account,
|
||||
) => {
|
||||
// Validate account name uniquiness.
|
||||
await this.validator.validateAccountNameUniquiness(
|
||||
accountDTO.name,
|
||||
accountId,
|
||||
);
|
||||
// Validate the account type should be not mutated.
|
||||
await this.validator.isAccountTypeChangedOrThrowError(
|
||||
oldAccount,
|
||||
accountDTO,
|
||||
);
|
||||
// Validate the account code not exists on the storage.
|
||||
if (accountDTO.code && accountDTO.code !== oldAccount.code) {
|
||||
await this.validator.isAccountCodeUniqueOrThrowError(
|
||||
accountDTO.code,
|
||||
oldAccount.id,
|
||||
);
|
||||
}
|
||||
// Retrieve the parent account of throw not found service error.
|
||||
if (accountDTO.parentAccountId) {
|
||||
const parentAccount = await this.validator.getParentAccountOrThrowError(
|
||||
accountDTO.parentAccountId,
|
||||
oldAccount.id,
|
||||
);
|
||||
this.validator.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits details of the given account.
|
||||
* @param {number} accountId
|
||||
* @param {IAccountDTO} accountDTO
|
||||
*/
|
||||
public async editAccount(
|
||||
accountId: number,
|
||||
accountDTO: EditAccountDTO,
|
||||
): Promise<Account> {
|
||||
// Retrieve the old account or throw not found service error.
|
||||
const oldAccount = await this.accountModel()
|
||||
.query()
|
||||
.findById(accountId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Authorize the account editing.
|
||||
await this.authorize(accountId, accountDTO, oldAccount);
|
||||
|
||||
// Edits account and associated transactions under unit-of-work environment.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onAccountEditing` event.
|
||||
await this.eventEmitter.emitAsync(events.accounts.onEditing, {
|
||||
oldAccount,
|
||||
accountDTO,
|
||||
});
|
||||
// Update the account on the storage.
|
||||
const account = await this.accountModel()
|
||||
.query(trx)
|
||||
.findById(accountId)
|
||||
.updateAndFetch({ ...accountDTO });
|
||||
|
||||
// Triggers `onAccountEdited` event.
|
||||
// await this.eventEmitter.emitAsync(events.accounts.onEdited, {
|
||||
// account,
|
||||
// oldAccount,
|
||||
// trx,
|
||||
// } as IAccountEventEditedPayload);
|
||||
|
||||
return account;
|
||||
});
|
||||
}
|
||||
}
|
||||
47
packages/server/src/modules/Accounts/GetAccount.service.ts
Normal file
47
packages/server/src/modules/Accounts/GetAccount.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { AccountTransformer } from './Account.transformer';
|
||||
import { Account } from './models/Account.model';
|
||||
import { AccountRepository } from './repositories/Account.repository';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetAccount {
|
||||
constructor(
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the given account details.
|
||||
* @param {number} accountId
|
||||
*/
|
||||
public getAccount = async (accountId: number) => {
|
||||
// Find the given account or throw not found error.
|
||||
const account = await this.accountModel()
|
||||
.query()
|
||||
.findById(accountId)
|
||||
.withGraphFetched('plaidItem')
|
||||
.throwIfNotFound();
|
||||
|
||||
const accountsGraph = await this.accountRepository.getDependencyGraph();
|
||||
|
||||
// Transforms the account model to POJO.
|
||||
const transformed = await this.transformer.transform(
|
||||
account,
|
||||
new AccountTransformer(),
|
||||
{ accountsGraph },
|
||||
);
|
||||
const eventPayload = { accountId };
|
||||
|
||||
// Triggers `onAccountViewed` event.
|
||||
await this.eventEmitter.emitAsync(events.accounts.onViewed, eventPayload);
|
||||
|
||||
return transformed;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
IAccountsTransactionsFilter,
|
||||
IGetAccountTransactionPOJO,
|
||||
} from './Accounts.types';
|
||||
import { AccountTransactionTransformer } from './AccountTransaction.transformer';
|
||||
import { AccountTransaction } from './models/AccountTransaction.model';
|
||||
import { Account } from './models/Account.model';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetAccountTransactionsService {
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(AccountTransaction.name)
|
||||
private readonly accountTransaction: TenantModelProxy<
|
||||
typeof AccountTransaction
|
||||
>,
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly account: TenantModelProxy<typeof Account>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the accounts transactions.
|
||||
* @param {IAccountsTransactionsFilter} filter -
|
||||
*/
|
||||
public getAccountsTransactions = async (
|
||||
filter: IAccountsTransactionsFilter,
|
||||
): Promise<IGetAccountTransactionPOJO[]> => {
|
||||
// Retrieve the given account or throw not found error.
|
||||
if (filter.accountId) {
|
||||
await this.account().query().findById(filter.accountId).throwIfNotFound();
|
||||
}
|
||||
const transactions = await this.accountTransaction()
|
||||
.query()
|
||||
.onBuild((query) => {
|
||||
query.orderBy('date', 'DESC');
|
||||
|
||||
if (filter.accountId) {
|
||||
query.where('account_id', filter.accountId);
|
||||
}
|
||||
query.withGraphFetched('account');
|
||||
query.withGraphFetched('contact');
|
||||
query.limit(filter.limit || 50);
|
||||
});
|
||||
// Transform the account transaction.
|
||||
return this.transformer.transform(
|
||||
transactions,
|
||||
new AccountTransactionTransformer(),
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// import { IAccountType } from './Accounts.types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AccountTypesUtils } from './utils/AccountType.utils';
|
||||
|
||||
@Injectable()
|
||||
export class GetAccountTypesService {
|
||||
/**
|
||||
* Retrieve all accounts types.
|
||||
* @param {number} tenantId -
|
||||
* @return {IAccountType}
|
||||
*/
|
||||
public getAccountsTypes() {
|
||||
const accountTypes = AccountTypesUtils.getList();
|
||||
|
||||
return accountTypes;
|
||||
}
|
||||
}
|
||||
69
packages/server/src/modules/Accounts/GetAccounts.service.ts
Normal file
69
packages/server/src/modules/Accounts/GetAccounts.service.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as R from 'ramda';
|
||||
import { IAccountsFilter } from './Accounts.types';
|
||||
import { DynamicListService } from '../DynamicListing/DynamicList.service';
|
||||
import { AccountTransformer } from './Account.transformer';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { Account } from './models/Account.model';
|
||||
import { AccountRepository } from './repositories/Account.repository';
|
||||
import { IFilterMeta } from '@/interfaces/Model';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetAccountsService {
|
||||
constructor(
|
||||
private readonly dynamicListService: DynamicListService,
|
||||
private readonly transformerService: TransformerInjectable,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve accounts datatable list.
|
||||
* @param {IAccountsFilter} accountsFilter
|
||||
* @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
|
||||
*/
|
||||
public async getAccountsList(
|
||||
filterDTO: IAccountsFilter,
|
||||
): Promise<{ accounts: Account[]; filterMeta: IFilterMeta }> {
|
||||
// Parses the stringified filter roles.
|
||||
const filter = this.parseListFilterDTO(filterDTO);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicList = await this.dynamicListService.dynamicList(
|
||||
this.accountModel(),
|
||||
filter,
|
||||
);
|
||||
// Retrieve accounts model based on the given query.
|
||||
const accounts = await this.accountModel()
|
||||
.query()
|
||||
.onBuild((builder) => {
|
||||
dynamicList.buildQuery()(builder);
|
||||
builder.modify('inactiveMode', filter.inactiveMode);
|
||||
});
|
||||
const accountsGraph = await this.accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieves the transformed accounts collection.
|
||||
const transformedAccounts = await this.transformerService.transform(
|
||||
accounts,
|
||||
new AccountTransformer(),
|
||||
{ accountsGraph, structure: filterDTO.structure },
|
||||
);
|
||||
|
||||
return {
|
||||
accounts: transformedAccounts,
|
||||
filterMeta: dynamicList.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsees accounts list filter DTO.
|
||||
* @param filterDTO
|
||||
* @returns
|
||||
*/
|
||||
private parseListFilterDTO(filterDTO) {
|
||||
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
// @Service()
|
||||
// export class MutateBaseCurrencyAccounts {
|
||||
// @Inject()
|
||||
// tenancy: HasTenancyService;
|
||||
|
||||
// /**
|
||||
// * Mutates the all accounts or the organziation.
|
||||
// * @param {number} tenantId
|
||||
// * @param {string} currencyCode
|
||||
// */
|
||||
// public mutateAllAccountsCurrency = async (
|
||||
// tenantId: number,
|
||||
// currencyCode: string
|
||||
// ) => {
|
||||
// const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
// await Account.query().update({ currencyCode });
|
||||
// };
|
||||
// }
|
||||
103
packages/server/src/modules/Accounts/constants.ts
Normal file
103
packages/server/src/modules/Accounts/constants.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
export const ERRORS = {
|
||||
ACCOUNT_NOT_FOUND: 'account_not_found',
|
||||
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
|
||||
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
|
||||
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
|
||||
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
|
||||
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
|
||||
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',
|
||||
ACCOUNT_PREDEFINED: 'account_predefined',
|
||||
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
|
||||
PREDEFINED_ACCOUNTS: 'predefined_accounts',
|
||||
ACCOUNTS_HAVE_TRANSACTIONS: 'accounts_have_transactions',
|
||||
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE:
|
||||
'close_account_and_to_account_not_same_type',
|
||||
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
|
||||
ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY:
|
||||
'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY',
|
||||
ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT:
|
||||
'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT',
|
||||
PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL:
|
||||
'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL',
|
||||
};
|
||||
|
||||
// Default views columns.
|
||||
export const DEFAULT_VIEW_COLUMNS = [
|
||||
{ key: 'name', label: 'Account name' },
|
||||
{ key: 'code', label: 'Account code' },
|
||||
{ key: 'account_type_label', label: 'Account type' },
|
||||
{ key: 'account_normal', label: 'Account normal' },
|
||||
{ key: 'amount', label: 'Balance' },
|
||||
{ key: 'currencyCode', label: 'Currency' },
|
||||
];
|
||||
|
||||
export const MAX_ACCOUNTS_CHART_DEPTH = 5;
|
||||
|
||||
// Accounts default views.
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
name: 'Assets',
|
||||
slug: 'assets',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'root_type', comparator: 'equals', value: 'asset' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Liabilities',
|
||||
slug: 'liabilities',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'liability',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Equity',
|
||||
slug: 'equity',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'equity',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Income',
|
||||
slug: 'income',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'income',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Expenses',
|
||||
slug: 'expenses',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'expense',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
];
|
||||
458
packages/server/src/modules/Accounts/models/Account.model.ts
Normal file
458
packages/server/src/modules/Accounts/models/Account.model.ts
Normal file
@@ -0,0 +1,458 @@
|
||||
/* eslint-disable global-require */
|
||||
// import { mixin, Model } from 'objection';
|
||||
import { castArray } from 'lodash';
|
||||
import DependencyGraph from '@/libs/dependency-graph';
|
||||
import {
|
||||
ACCOUNT_TYPES,
|
||||
getAccountsSupportsMultiCurrency,
|
||||
} from '@/constants/accounts';
|
||||
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||
// import { SearchableModel } from '@/modules/Search/SearchableMdel';
|
||||
// import { CustomViewBaseModel } from '@/modules/CustomViews/CustomViewBaseModel';
|
||||
// import { ModelSettings } from '@/modules/Settings/ModelSettings';
|
||||
import { AccountTypesUtils } from '@/libs/accounts-utils/AccountTypesUtils';
|
||||
import { Model } from 'objection';
|
||||
import { PlaidItem } from '@/modules/BankingPlaid/models/PlaidItem';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
|
||||
// import AccountSettings from './Account.Settings';
|
||||
// import { DEFAULT_VIEWS } from '@/modules/Accounts/constants';
|
||||
// import { buildFilterQuery, buildSortColumnQuery } from '@/lib/ViewRolesBuilder';
|
||||
// import { flatToNestedArray } from 'utils';
|
||||
|
||||
|
||||
export class Account extends TenantBaseModel {
|
||||
public name!: string;
|
||||
public slug!: string;
|
||||
public code!: string;
|
||||
public index!: number;
|
||||
public accountType!: string;
|
||||
public parentAccountId!: number | null;
|
||||
public predefined!: boolean;
|
||||
public currencyCode!: string;
|
||||
public active!: boolean;
|
||||
public bankBalance!: number;
|
||||
public lastFeedsUpdatedAt!: string | Date | null;
|
||||
public amount!: number;
|
||||
public plaidItemId!: string;
|
||||
public plaidAccountId!: string | null;
|
||||
public isFeedsActive!: boolean;
|
||||
public isSyncingOwner!: boolean;
|
||||
public plaidItem!: PlaidItem;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'accounts';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'accountTypeLabel',
|
||||
'accountParentType',
|
||||
'accountRootType',
|
||||
'accountNormal',
|
||||
'accountNormalFormatted',
|
||||
'isBalanceSheetAccount',
|
||||
'isPLSheet',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Account normal.
|
||||
*/
|
||||
get accountNormal(): string {
|
||||
return AccountTypesUtils.getType(this.accountType, 'normal');
|
||||
}
|
||||
|
||||
get accountNormalFormatted(): string {
|
||||
const paris = {
|
||||
credit: 'Credit',
|
||||
debit: 'Debit',
|
||||
};
|
||||
return paris[this.accountNormal] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account type label.
|
||||
*/
|
||||
get accountTypeLabel(): string {
|
||||
return AccountTypesUtils.getType(this.accountType, 'label');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account parent type.
|
||||
*/
|
||||
get accountParentType(): string {
|
||||
return AccountTypesUtils.getType(this.accountType, 'parentType');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account root type.
|
||||
*/
|
||||
get accountRootType(): string {
|
||||
return AccountTypesUtils.getType(this.accountType, 'rootType');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve whether the account is balance sheet account.
|
||||
*/
|
||||
get isBalanceSheetAccount(): boolean {
|
||||
return this.isBalanceSheet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve whether the account is profit/loss sheet account.
|
||||
*/
|
||||
get isPLSheet(): boolean {
|
||||
return this.isProfitLossSheet();
|
||||
}
|
||||
/**
|
||||
* Allows to mark model as resourceable to viewable and filterable.
|
||||
*/
|
||||
static get resourceable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
const TABLE_NAME = Account.tableName;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Inactive/Active mode.
|
||||
*/
|
||||
inactiveMode(query, active = false) {
|
||||
query.where('accounts.active', !active);
|
||||
},
|
||||
|
||||
filterAccounts(query, accountIds) {
|
||||
if (accountIds.length > 0) {
|
||||
query.whereIn(`${TABLE_NAME}.id`, accountIds);
|
||||
}
|
||||
},
|
||||
filterAccountTypes(query, typesIds) {
|
||||
if (typesIds.length > 0) {
|
||||
query.whereIn('account_types.account_type_id', typesIds);
|
||||
}
|
||||
},
|
||||
viewRolesBuilder(query, conditionals, expression) {
|
||||
// buildFilterQuery(Account.tableName, conditionals, expression)(query);
|
||||
},
|
||||
sortColumnBuilder(query, columnKey, direction) {
|
||||
// buildSortColumnQuery(Account.tableName, columnKey, direction)(query);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter by root type.
|
||||
*/
|
||||
filterByRootType(query, rootType) {
|
||||
const filterTypes = ACCOUNT_TYPES.filter(
|
||||
(accountType) => accountType.rootType === rootType,
|
||||
).map((accountType) => accountType.key);
|
||||
|
||||
query.whereIn('account_type', filterTypes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter by account normal
|
||||
*/
|
||||
filterByAccountNormal(query, accountNormal) {
|
||||
const filterTypes = ACCOUNT_TYPES.filter(
|
||||
(accountType) => accountType.normal === accountNormal,
|
||||
).map((accountType) => accountType.key);
|
||||
|
||||
query.whereIn('account_type', filterTypes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds account by the given slug.
|
||||
* @param {*} query
|
||||
* @param {*} slug
|
||||
*/
|
||||
findBySlug(query, slug) {
|
||||
query.where('slug', slug).first();
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} query
|
||||
* @param {*} baseCyrrency
|
||||
*/
|
||||
preventMutateBaseCurrency(query) {
|
||||
const accountsTypes = getAccountsSupportsMultiCurrency();
|
||||
const accountsTypesKeys = accountsTypes.map((type) => type.key);
|
||||
|
||||
query
|
||||
.whereIn('accountType', accountsTypesKeys)
|
||||
.where('seededAt', null)
|
||||
.first();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { AccountTransaction } = require('./AccountTransaction.model');
|
||||
const { Item } = require('../../Items/models/Item');
|
||||
// const InventoryAdjustment = require('models/InventoryAdjustment');
|
||||
// const ManualJournalEntry = require('models/ManualJournalEntry');
|
||||
// const Expense = require('models/Expense');
|
||||
// const ExpenseEntry = require('models/ExpenseCategory');
|
||||
// const ItemEntry = require('models/ItemEntry');
|
||||
// const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
|
||||
const { PlaidItem } = require('../../BankingPlaid/models/PlaidItem');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Account model may has many transactions.
|
||||
*/
|
||||
transactions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: AccountTransaction,
|
||||
join: {
|
||||
from: 'accounts.id',
|
||||
to: 'accounts_transactions.accountId',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Account may has many items as cost account.
|
||||
*/
|
||||
itemsCostAccount: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: Item,
|
||||
join: {
|
||||
from: 'accounts.id',
|
||||
to: 'items.costAccountId',
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Account may has many items as sell account.
|
||||
*/
|
||||
itemsSellAccount: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: Item,
|
||||
join: {
|
||||
from: 'accounts.id',
|
||||
to: 'items.sellAccountId',
|
||||
},
|
||||
},
|
||||
// /**
|
||||
// *
|
||||
// */
|
||||
// inventoryAdjustments: {
|
||||
// relation: Model.HasManyRelation,
|
||||
// modelClass: InventoryAdjustment.default,
|
||||
// join: {
|
||||
// from: 'accounts.id',
|
||||
// to: 'inventory_adjustments.adjustmentAccountId',
|
||||
// },
|
||||
// },
|
||||
// /**
|
||||
// *
|
||||
// */
|
||||
// manualJournalEntries: {
|
||||
// relation: Model.HasManyRelation,
|
||||
// modelClass: ManualJournalEntry.default,
|
||||
// join: {
|
||||
// from: 'accounts.id',
|
||||
// to: 'manual_journals_entries.accountId',
|
||||
// },
|
||||
// },
|
||||
// /**
|
||||
// *
|
||||
// */
|
||||
// expensePayments: {
|
||||
// relation: Model.HasManyRelation,
|
||||
// modelClass: Expense.default,
|
||||
// join: {
|
||||
// from: 'accounts.id',
|
||||
// to: 'expenses_transactions.paymentAccountId',
|
||||
// },
|
||||
// },
|
||||
// /**
|
||||
// *
|
||||
// */
|
||||
// expenseEntries: {
|
||||
// relation: Model.HasManyRelation,
|
||||
// modelClass: ExpenseEntry.default,
|
||||
// join: {
|
||||
// from: 'accounts.id',
|
||||
// to: 'expense_transaction_categories.expenseAccountId',
|
||||
// },
|
||||
// },
|
||||
// /**
|
||||
// *
|
||||
// */
|
||||
// entriesCostAccount: {
|
||||
// relation: Model.HasManyRelation,
|
||||
// modelClass: ItemEntry.default,
|
||||
// join: {
|
||||
// from: 'accounts.id',
|
||||
// to: 'items_entries.costAccountId',
|
||||
// },
|
||||
// },
|
||||
// /**
|
||||
// *
|
||||
// */
|
||||
// entriesSellAccount: {
|
||||
// relation: Model.HasManyRelation,
|
||||
// modelClass: ItemEntry.default,
|
||||
// join: {
|
||||
// from: 'accounts.id',
|
||||
// to: 'items_entries.sellAccountId',
|
||||
// },
|
||||
// },
|
||||
// /**
|
||||
// * Associated uncategorized transactions.
|
||||
// */
|
||||
// uncategorizedTransactions: {
|
||||
// relation: Model.HasManyRelation,
|
||||
// modelClass: UncategorizedTransaction.default,
|
||||
// join: {
|
||||
// from: 'accounts.id',
|
||||
// to: 'uncategorized_cashflow_transactions.accountId',
|
||||
// },
|
||||
// filter: (query) => {
|
||||
// query.where('categorized', false);
|
||||
// },
|
||||
// },
|
||||
/**
|
||||
* Account model may belongs to a Plaid item.
|
||||
*/
|
||||
plaidItem: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: PlaidItem,
|
||||
join: {
|
||||
from: 'accounts.plaidItemId',
|
||||
to: 'plaid_items.plaidItemId',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the given type equals the account type.
|
||||
* @param {string} accountType
|
||||
* @return {boolean}
|
||||
*/
|
||||
isAccountType(accountType) {
|
||||
const types = castArray(accountType);
|
||||
return types.indexOf(this.accountType) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the given root type equals the account type.
|
||||
* @param {string} rootType
|
||||
* @return {boolean}
|
||||
*/
|
||||
isRootType(rootType) {
|
||||
return AccountTypesUtils.isRootTypeEqualsKey(this.accountType, rootType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmine whether the given parent type equals the account type.
|
||||
* @param {string} parentType
|
||||
* @return {boolean}
|
||||
*/
|
||||
isParentType(parentType) {
|
||||
return AccountTypesUtils.isParentTypeEqualsKey(
|
||||
this.accountType,
|
||||
parentType,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the account is balance sheet account.
|
||||
* @return {boolean}
|
||||
*/
|
||||
isBalanceSheet() {
|
||||
return AccountTypesUtils.isTypeBalanceSheet(this.accountType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the account is profit/loss account.
|
||||
* @return {boolean}
|
||||
*/
|
||||
isProfitLossSheet() {
|
||||
return AccountTypesUtils.isTypePLSheet(this.accountType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the account is income statement account
|
||||
* @return {boolean}
|
||||
*/
|
||||
isIncomeSheet() {
|
||||
return this.isProfitLossSheet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts flatten accounts list to nested array.
|
||||
* @param {Array} accounts
|
||||
* @param {Object} options
|
||||
*/
|
||||
static toNestedArray(accounts, options = { children: 'children' }) {
|
||||
return flatToNestedArray(accounts, {
|
||||
id: 'id',
|
||||
parentId: 'parentAccountId',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the accounts list to depenedency graph structure.
|
||||
* @param {IAccount[]} accounts
|
||||
*/
|
||||
static toDependencyGraph(accounts) {
|
||||
return DependencyGraph.fromArray(accounts, {
|
||||
itemId: 'id',
|
||||
parentItemId: 'parentAccountId',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Model settings.
|
||||
*/
|
||||
// static get meta() {
|
||||
// return AccountSettings;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Retrieve the default custom views, roles and columns.
|
||||
*/
|
||||
// static get defaultViews() {
|
||||
// return DEFAULT_VIEWS;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Model search roles.
|
||||
*/
|
||||
static get searchRoles() {
|
||||
return [
|
||||
{ condition: 'or', fieldKey: 'name', comparator: 'contains' },
|
||||
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents mutate base currency since the model is not empty.
|
||||
*/
|
||||
static get preventMutateBaseCurrency() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import { Model, raw } from 'objection';
|
||||
import * as moment from 'moment';
|
||||
import { unitOfTime } from 'moment';
|
||||
import { isEmpty, castArray } from 'lodash';
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import { Account } from './Account.model';
|
||||
// import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||
|
||||
export class AccountTransaction extends BaseModel {
|
||||
public readonly referenceType: string;
|
||||
public readonly referenceId: number;
|
||||
public readonly accountId: number;
|
||||
public readonly contactId: number;
|
||||
public readonly contactType: string;
|
||||
public readonly credit: number;
|
||||
public readonly debit: number;
|
||||
public readonly exchangeRate: number;
|
||||
public readonly taxRate: number;
|
||||
public readonly date: Date | string;
|
||||
public readonly transactionType: string;
|
||||
public readonly currencyCode: string;
|
||||
public readonly referenceTypeFormatted: string;
|
||||
public readonly transactionNumber!: string;
|
||||
public readonly referenceNumber!: string;
|
||||
public readonly note!: string;
|
||||
|
||||
public readonly index!: number;
|
||||
public readonly indexGroup!: number;
|
||||
|
||||
public readonly taxRateId!: number;
|
||||
|
||||
public readonly branchId!: number;
|
||||
public readonly userId!: number;
|
||||
public readonly itemId!: number;
|
||||
public readonly projectId!: number;
|
||||
public readonly account: Account;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'accounts_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['referenceTypeFormatted', 'creditLocal', 'debitLocal'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the credit amount in base currency.
|
||||
* @return {number}
|
||||
*/
|
||||
get creditLocal() {
|
||||
return this.credit * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the debit amount in base currency.
|
||||
* @return {number}
|
||||
*/
|
||||
get debitLocal() {
|
||||
return this.debit * this.exchangeRate;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Retrieve formatted reference type.
|
||||
// * @return {string}
|
||||
// */
|
||||
// get referenceTypeFormatted() {
|
||||
// return getTransactionTypeLabel(this.referenceType, this.transactionType);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
/**
|
||||
* Filters accounts by the given ids.
|
||||
* @param {Query} query
|
||||
* @param {number[]} accountsIds
|
||||
*/
|
||||
filterAccounts(query, accountsIds) {
|
||||
if (Array.isArray(accountsIds) && accountsIds.length > 0) {
|
||||
query.whereIn('account_id', accountsIds);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters the transaction types.
|
||||
* @param {Query} query
|
||||
* @param {string[]} types
|
||||
*/
|
||||
filterTransactionTypes(query, types) {
|
||||
if (Array.isArray(types) && types.length > 0) {
|
||||
query.whereIn('reference_type', types);
|
||||
} else if (typeof types === 'string') {
|
||||
query.where('reference_type', types);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters the date range.
|
||||
* @param {Query} query
|
||||
* @param {moment.MomentInput} startDate
|
||||
* @param {moment.MomentInput} endDate
|
||||
* @param {unitOfTime.StartOf} type
|
||||
*/
|
||||
filterDateRange(
|
||||
query,
|
||||
startDate: moment.MomentInput,
|
||||
endDate: moment.MomentInput,
|
||||
type: unitOfTime.StartOf = 'day',
|
||||
) {
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||
|
||||
if (startDate) {
|
||||
query.where('date', '>=', fromDate);
|
||||
}
|
||||
if (endDate) {
|
||||
query.where('date', '<=', toDate);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters the amount range.
|
||||
* @param {Query} query
|
||||
* @param {number} fromAmount
|
||||
* @param {number} toAmount
|
||||
*/
|
||||
filterAmountRange(query, fromAmount, toAmount) {
|
||||
if (fromAmount) {
|
||||
query.andWhere((q) => {
|
||||
q.where('credit', '>=', fromAmount);
|
||||
q.orWhere('debit', '>=', fromAmount);
|
||||
});
|
||||
}
|
||||
if (toAmount) {
|
||||
query.andWhere((q) => {
|
||||
q.where('credit', '<=', toAmount);
|
||||
q.orWhere('debit', '<=', toAmount);
|
||||
});
|
||||
}
|
||||
},
|
||||
sumationCreditDebit(query) {
|
||||
query.select(['accountId']);
|
||||
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
query.groupBy('account_id');
|
||||
},
|
||||
filterContactType(query, contactType) {
|
||||
query.where('contact_type', contactType);
|
||||
},
|
||||
filterContactIds(query, contactIds) {
|
||||
query.whereIn('contact_id', contactIds);
|
||||
},
|
||||
openingBalance(query, fromDate) {
|
||||
query.modify('filterDateRange', null, fromDate);
|
||||
query.modify('sumationCreditDebit');
|
||||
},
|
||||
closingBalance(query, toDate) {
|
||||
query.modify('filterDateRange', null, toDate);
|
||||
query.modify('sumationCreditDebit');
|
||||
},
|
||||
contactsOpeningBalance(
|
||||
query,
|
||||
openingDate,
|
||||
receivableAccounts,
|
||||
customersIds,
|
||||
) {
|
||||
// Filter by date.
|
||||
query.modify('filterDateRange', null, openingDate);
|
||||
|
||||
// Filter by customers.
|
||||
query.whereNot('contactId', null);
|
||||
query.whereIn('accountId', castArray(receivableAccounts));
|
||||
|
||||
if (!isEmpty(customersIds)) {
|
||||
query.whereIn('contactId', castArray(customersIds));
|
||||
}
|
||||
// Group by the contact transactions.
|
||||
query.groupBy('contactId');
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
query.select('contactId');
|
||||
},
|
||||
creditDebitSummation(query) {
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
},
|
||||
groupByDateFormat(query, groupType = 'month') {
|
||||
const groupBy = {
|
||||
day: '%Y-%m-%d',
|
||||
month: '%Y-%m',
|
||||
year: '%Y',
|
||||
};
|
||||
const dateFormat = groupBy[groupType];
|
||||
|
||||
query.select(raw(`DATE_FORMAT(DATE, '${dateFormat}')`).as('date'));
|
||||
query.groupByRaw(`DATE_FORMAT(DATE, '${dateFormat}')`);
|
||||
},
|
||||
|
||||
filterByBranches(query, branchesIds) {
|
||||
const formattedBranchesIds = castArray(branchesIds);
|
||||
|
||||
query.whereIn('branchId', formattedBranchesIds);
|
||||
},
|
||||
|
||||
filterByProjects(query, projectsIds) {
|
||||
const formattedProjectsIds = castArray(projectsIds);
|
||||
|
||||
query.whereIn('projectId', formattedProjectsIds);
|
||||
},
|
||||
|
||||
filterByReference(query, referenceId: number, referenceType: string) {
|
||||
query.where('reference_id', referenceId);
|
||||
query.where('reference_type', referenceType);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { Account } = require('./Account.model');
|
||||
const { Contact } = require('../../Contacts/models/Contact');
|
||||
|
||||
return {
|
||||
account: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account,
|
||||
join: {
|
||||
from: 'accounts_transactions.accountId',
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
contact: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Contact,
|
||||
join: {
|
||||
from: 'accounts_transactions.contactId',
|
||||
to: 'contacts.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents mutate base currency since the model is not empty.
|
||||
*/
|
||||
static get preventMutateBaseCurrency() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { TenantRepository } from '@/common/repository/TenantRepository';
|
||||
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
|
||||
import { Account } from '../models/Account.model';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import {
|
||||
DiscountExpenseAccount,
|
||||
OtherChargesAccount,
|
||||
OtherExpensesAccount,
|
||||
PrepardExpenses,
|
||||
PurchaseDiscountAccount,
|
||||
StripeClearingAccount,
|
||||
TaxPayableAccount,
|
||||
UnearnedRevenueAccount,
|
||||
} from '../Accounts.constants';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class AccountRepository extends TenantRepository {
|
||||
constructor(
|
||||
private readonly i18n: I18nService,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
|
||||
@Inject(TENANCY_DB_CONNECTION)
|
||||
private readonly tenantDBKnex: () => Knex,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the repository's model.
|
||||
*/
|
||||
get model(): typeof Account {
|
||||
return Account.bindKnex(this.tenantDBKnex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts dependency graph.
|
||||
* @param {string} withRelation
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {}
|
||||
*/
|
||||
public async getDependencyGraph(
|
||||
withRelation?: string,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const accounts = await this.all(withRelation, trx);
|
||||
|
||||
return this.model.toDependencyGraph(accounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account by slug.
|
||||
* @param {string} slug
|
||||
* @return {Promise<IAccount>}
|
||||
*/
|
||||
public findBySlug(slug: string) {
|
||||
return this.findOne({ slug });
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Changes account balance.
|
||||
// * @param {number} accountId
|
||||
// * @param {number} amount
|
||||
// * @return {Promise<void>}
|
||||
// */
|
||||
// async balanceChange(accountId: number, amount: number): Promise<void> {
|
||||
// const method: string = amount < 0 ? 'decrement' : 'increment';
|
||||
|
||||
// await this.model.query().where('id', accountId)[method]('amount', amount);
|
||||
// this.flushCache();
|
||||
// }
|
||||
|
||||
/**
|
||||
* Activate user by the given id.
|
||||
* @param {number} userId - User id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
activateById(userId: number): Promise<number> {
|
||||
return super.update({ active: 1 }, { id: userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivate user by the given id.
|
||||
* @param {number} userId - User id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
inactivateById(userId: number): Promise<number> {
|
||||
return super.update({ active: 0 }, { id: userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate user by the given id.
|
||||
* @param {number} userId - User id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async activateByIds(userIds: number[], trx): Promise<number> {
|
||||
const results = await this.model
|
||||
.query(trx)
|
||||
.whereIn('id', userIds)
|
||||
.patch({ active: true });
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivate user by the given id.
|
||||
* @param {number} userId - User id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async inactivateByIds(userIds: number[], trx): Promise<number> {
|
||||
const results = await this.model
|
||||
.query(trx)
|
||||
.whereIn('id', userIds)
|
||||
.patch({ active: false });
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} currencyCode
|
||||
* @param extraAttrs
|
||||
* @param trx
|
||||
* @returns
|
||||
*/
|
||||
findOrCreateAccountReceivable = async (
|
||||
currencyCode: string = '',
|
||||
extraAttrs = {},
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.onBuild((query) => {
|
||||
if (currencyCode) {
|
||||
query.where('currencyCode', currencyCode);
|
||||
}
|
||||
query.where('accountType', 'accounts-receivable');
|
||||
})
|
||||
.first();
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
name: this.i18n.t('account.accounts_receivable.currency', {
|
||||
args: { currency: currencyCode },
|
||||
}),
|
||||
accountType: 'accounts-receivable',
|
||||
currencyCode,
|
||||
active: true,
|
||||
...extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find or create tax payable account.
|
||||
* @param {Record<string, string>}extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
async findOrCreateTaxPayable(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: TaxPayableAccount.slug, ...extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...TaxPayableAccount,
|
||||
...extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
findOrCreateAccountsPayable = async (
|
||||
currencyCode: string = '',
|
||||
extraAttrs = {},
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.onBuild((query) => {
|
||||
if (currencyCode) {
|
||||
query.where('currencyCode', currencyCode);
|
||||
}
|
||||
query.where('accountType', 'accounts-payable');
|
||||
})
|
||||
.first();
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
name: this.i18n.t('account.accounts_payable.currency', {
|
||||
args: { currency: currencyCode },
|
||||
}),
|
||||
accountType: 'accounts-payable',
|
||||
currencyCode,
|
||||
active: true,
|
||||
...extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds or creates the unearned revenue.
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
public async findOrCreateUnearnedRevenue(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const tenantMeta = await this.tenancyContext.getTenantMetadata();
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: UnearnedRevenueAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...UnearnedRevenueAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds or creates the prepard expenses account.
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
public async findOrCreatePrepardExpenses(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const tenantMeta = await this.tenancyContext.getTenantMetadata();
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: PrepardExpenses.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...PrepardExpenses,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds or creates the stripe clearing account.
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
public async findOrCreateStripeClearing(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const tenantMeta = await this.tenancyContext.getTenantMetadata();
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: StripeClearingAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...StripeClearingAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds or creates the discount expense account.
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
public async findOrCreateDiscountAccount(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const tenantMeta = await this.tenancyContext.getTenantMetadata();
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: DiscountExpenseAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...DiscountExpenseAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async findOrCreatePurchaseDiscountAccount(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const tenantMeta = await this.tenancyContext.getTenantMetadata();
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: PurchaseDiscountAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...PurchaseDiscountAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async findOrCreateOtherChargesAccount(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const tenantMeta = await this.tenancyContext.getTenantMetadata();
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: OtherChargesAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...OtherChargesAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async findOrCreateOtherExpensesAccount(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const tenantMeta = await this.tenancyContext.getTenantMetadata();
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: OtherExpensesAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...OtherExpensesAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// import { Service, Inject } from 'typedi';
|
||||
// import events from '@/subscribers/events';
|
||||
// import { MutateBaseCurrencyAccounts } from '../MutateBaseCurrencyAccounts';
|
||||
|
||||
// @Service()
|
||||
// export class MutateBaseCurrencyAccountsSubscriber {
|
||||
// @Inject()
|
||||
// public mutateBaseCurrencyAccounts: MutateBaseCurrencyAccounts;
|
||||
|
||||
// /**
|
||||
// * Attaches the events with handles.
|
||||
// * @param bus
|
||||
// */
|
||||
// attach(bus) {
|
||||
// bus.subscribe(
|
||||
// events.organization.baseCurrencyUpdated,
|
||||
// this.updateAccountsCurrencyOnBaseCurrencyMutated
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Updates the all accounts currency once the base currency
|
||||
// * of the organization is mutated.
|
||||
// */
|
||||
// private updateAccountsCurrencyOnBaseCurrencyMutated = async ({
|
||||
// tenantId,
|
||||
// organizationDTO,
|
||||
// }) => {
|
||||
// await this.mutateBaseCurrencyAccounts.mutateAllAccountsCurrency(
|
||||
// tenantId,
|
||||
// organizationDTO.baseCurrency
|
||||
// );
|
||||
// };
|
||||
// }
|
||||
101
packages/server/src/modules/Accounts/utils/AccountType.utils.ts
Normal file
101
packages/server/src/modules/Accounts/utils/AccountType.utils.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { get } from 'lodash';
|
||||
import { ACCOUNT_TYPES } from '../Accounts.constants';
|
||||
|
||||
export class AccountTypesUtils {
|
||||
/**
|
||||
* Retrieve account types list.
|
||||
*/
|
||||
static getList() {
|
||||
return ACCOUNT_TYPES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts types by the given root type.
|
||||
* @param {string} rootType -
|
||||
* @return {string}
|
||||
*/
|
||||
static getTypesByRootType(rootType: string) {
|
||||
return ACCOUNT_TYPES.filter((type) => type.rootType === rootType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account type by the given account type key.
|
||||
* @param {string} key
|
||||
* @param {string} accessor
|
||||
*/
|
||||
static getType(key: string, accessor?: string) {
|
||||
const type = ACCOUNT_TYPES.find((type) => type.key === key);
|
||||
|
||||
if (accessor) {
|
||||
return get(type, accessor);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts types by the parent account type.
|
||||
* @param {string} parentType
|
||||
*/
|
||||
static getTypesByParentType(parentType: string) {
|
||||
return ACCOUNT_TYPES.filter((type) => type.parentType === parentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts types by the given account normal.
|
||||
* @param {string} normal
|
||||
*/
|
||||
static getTypesByNormal(normal: string) {
|
||||
return ACCOUNT_TYPES.filter((type) => type.normal === normal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the root type equals the account type.
|
||||
* @param {string} key
|
||||
* @param {string} rootType
|
||||
*/
|
||||
static isRootTypeEqualsKey(key: string, rootType: string): boolean {
|
||||
return ACCOUNT_TYPES.some((type) => {
|
||||
const isType = type.key === key;
|
||||
const isRootType = type.rootType === rootType;
|
||||
|
||||
return isType && isRootType;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the parent account type equals the account type key.
|
||||
* @param {string} key - Account type key.
|
||||
* @param {string} parentType - Account parent type.
|
||||
*/
|
||||
static isParentTypeEqualsKey(key: string, parentType: string): boolean {
|
||||
return ACCOUNT_TYPES.some((type) => {
|
||||
const isType = type.key === key;
|
||||
const isParentType = type.parentType === parentType;
|
||||
|
||||
return isType && isParentType;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether account type has balance sheet.
|
||||
* @param {string} key - Account type key.
|
||||
*
|
||||
*/
|
||||
static isTypeBalanceSheet(key: string): boolean {
|
||||
return ACCOUNT_TYPES.some((type) => {
|
||||
const isType = type.key === key;
|
||||
return isType && type.balanceSheet;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether account type has profit/loss sheet.
|
||||
* @param {string} key - Account type key.
|
||||
*/
|
||||
static isTypePLSheet(key: string): boolean {
|
||||
return ACCOUNT_TYPES.some((type) => {
|
||||
const isType = type.key === key;
|
||||
return isType && type.incomeSheet;
|
||||
});
|
||||
}
|
||||
}
|
||||
22
packages/server/src/modules/App/App.controller.spec.ts
Normal file
22
packages/server/src/modules/App/App.controller.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './App.controller';
|
||||
import { AppService } from './App.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
12
packages/server/src/modules/App/App.controller.ts
Normal file
12
packages/server/src/modules/App/App.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './App.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
||||
220
packages/server/src/modules/App/App.module.ts
Normal file
220
packages/server/src/modules/App/App.module.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { join } from 'path';
|
||||
import { RedisModule } from '@liaoliaots/nestjs-redis';
|
||||
import {
|
||||
AcceptLanguageResolver,
|
||||
CookieResolver,
|
||||
HeaderResolver,
|
||||
I18nModule,
|
||||
QueryResolver,
|
||||
} from 'nestjs-i18n';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ClsModule, ClsService } from 'nestjs-cls';
|
||||
import { AppController } from './App.controller';
|
||||
import { AppService } from './App.service';
|
||||
import { ItemsModule } from '../Items/items.module';
|
||||
import { config } from '../../common/config';
|
||||
import { SystemDatabaseModule } from '../System/SystemDB/SystemDB.module';
|
||||
import { SystemModelsModule } from '../System/SystemModels/SystemModels.module';
|
||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||
import { TenancyModelsModule } from '../Tenancy/TenancyModels/Tenancy.module';
|
||||
import { LoggerMiddleware } from '@/middleware/logger.middleware';
|
||||
import { ExcludeNullInterceptor } from '@/interceptors/ExcludeNull.interceptor';
|
||||
import { UserIpInterceptor } from '@/interceptors/user-ip.interceptor';
|
||||
import { TransformerModule } from '../Transformer/Transformer.module';
|
||||
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||
import { ExpensesModule } from '../Expenses/Expenses.module';
|
||||
import { ItemCategoryModule } from '../ItemCategories/ItemCategory.module';
|
||||
import { TaxRatesModule } from '../TaxRates/TaxRate.module';
|
||||
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
|
||||
import { BranchesModule } from '../Branches/Branches.module';
|
||||
import { WarehousesModule } from '../Warehouses/Warehouses.module';
|
||||
import { SerializeInterceptor } from '@/common/interceptors/serialize.interceptor';
|
||||
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
|
||||
import { CustomersModule } from '../Customers/Customers.module';
|
||||
import { VendorsModule } from '../Vendors/Vendors.module';
|
||||
import { SaleEstimatesModule } from '../SaleEstimates/SaleEstimates.module';
|
||||
import { BillsModule } from '../Bills/Bills.module';
|
||||
import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module';
|
||||
import { SaleReceiptsModule } from '../SaleReceipts/SaleReceipts.module';
|
||||
import { ManualJournalsModule } from '../ManualJournals/ManualJournals.module';
|
||||
import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
|
||||
import { VendorCreditsModule } from '../VendorCredit/VendorCredits.module';
|
||||
import { VendorCreditApplyBillsModule } from '../VendorCreditsApplyBills/VendorCreditApplyBills.module';
|
||||
import { VendorCreditsRefundModule } from '../VendorCreditsRefund/VendorCreditsRefund.module';
|
||||
import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.module';
|
||||
import { BillPaymentsModule } from '../BillPayments/BillPayments.module';
|
||||
import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.module';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
import { BankRulesModule } from '../BankRules/BankRules.module';
|
||||
import { BankAccountsModule } from '../BankingAccounts/BankAccounts.module';
|
||||
import { BankingTransactionsExcludeModule } from '../BankingTransactionsExclude/BankingTransactionsExclude.module';
|
||||
import { BankingTransactionsRegonizeModule } from '../BankingTranasctionsRegonize/BankingTransactionsRegonize.module';
|
||||
import { BankingMatchingModule } from '../BankingMatching/BankingMatching.module';
|
||||
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
|
||||
import { TransactionsLockingModule } from '../TransactionsLocking/TransactionsLocking.module';
|
||||
import { SettingsModule } from '../Settings/Settings.module';
|
||||
import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module';
|
||||
import { PostHogModule } from '../EventsTracker/postHog.module';
|
||||
import { EventTrackerModule } from '../EventsTracker/EventTracker.module';
|
||||
import { MailModule } from '../Mail/Mail.module';
|
||||
import { FinancialStatementsModule } from '../FinancialStatements/FinancialStatements.module';
|
||||
import { StripePaymentModule } from '../StripePayment/StripePayment.module';
|
||||
import { FeaturesModule } from '../Features/Features.module';
|
||||
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
|
||||
import { WarehousesTransfersModule } from '../WarehousesTransfers/WarehouseTransfers.module';
|
||||
import { DashboardModule } from '../Dashboard/Dashboard.module';
|
||||
import { PaymentLinksModule } from '../PaymentLinks/PaymentLinks.module';
|
||||
import { RolesModule } from '../Roles/Roles.module';
|
||||
import { SubscriptionModule } from '../Subscription/Subscription.module';
|
||||
import { OrganizationModule } from '../Organization/Organization.module';
|
||||
import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module';
|
||||
import { PaymentServicesModule } from '../PaymentServices/PaymentServices.module';
|
||||
import { AuthModule } from '../Auth/Auth.module';
|
||||
import { TenancyModule } from '../Tenancy/Tenancy.module';
|
||||
import { LoopsModule } from '../Loops/Loops.module';
|
||||
import { AttachmentsModule } from '../Attachments/Attachment.module';
|
||||
import { S3Module } from '../S3/S3.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
envFilePath: '.env',
|
||||
load: config,
|
||||
isGlobal: true,
|
||||
}),
|
||||
SystemDatabaseModule,
|
||||
SystemModelsModule,
|
||||
EventEmitterModule.forRoot(),
|
||||
I18nModule.forRootAsync({
|
||||
useFactory: () => ({
|
||||
fallbackLanguage: 'en',
|
||||
loaderOptions: {
|
||||
path: join(__dirname, '/../../i18n/'),
|
||||
watch: true,
|
||||
},
|
||||
}),
|
||||
resolvers: [
|
||||
new QueryResolver(),
|
||||
new HeaderResolver(),
|
||||
new CookieResolver(),
|
||||
AcceptLanguageResolver,
|
||||
],
|
||||
}),
|
||||
PassportModule,
|
||||
BullModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
connection: {
|
||||
host: configService.get('QUEUE_HOST'),
|
||||
port: configService.get('QUEUE_PORT'),
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
ClsModule.forRoot({
|
||||
global: true,
|
||||
middleware: {
|
||||
mount: true,
|
||||
setup: (cls: ClsService, req: Request, res: Response) => {
|
||||
cls.set('organizationId', req.headers['organization-id']);
|
||||
},
|
||||
generateId: true,
|
||||
saveReq: true,
|
||||
},
|
||||
}),
|
||||
RedisModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
config: {
|
||||
host: configService.get('redis.host') || 'localhost',
|
||||
port: configService.get('redis.port') || 6379,
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
TenancyDatabaseModule,
|
||||
TenancyModelsModule,
|
||||
TenancyModule,
|
||||
ChromiumlyTenancyModule,
|
||||
TransformerModule,
|
||||
MailModule,
|
||||
AuthModule,
|
||||
ItemsModule,
|
||||
ItemCategoryModule,
|
||||
AccountsModule,
|
||||
ExpensesModule,
|
||||
TaxRatesModule,
|
||||
PdfTemplatesModule,
|
||||
BranchesModule,
|
||||
WarehousesModule,
|
||||
WarehousesTransfersModule,
|
||||
CustomersModule,
|
||||
VendorsModule,
|
||||
SaleInvoicesModule,
|
||||
SaleEstimatesModule,
|
||||
SaleReceiptsModule,
|
||||
BillsModule,
|
||||
ManualJournalsModule,
|
||||
CreditNotesModule,
|
||||
VendorCreditsModule,
|
||||
VendorCreditApplyBillsModule,
|
||||
VendorCreditsRefundModule,
|
||||
CreditNoteRefundsModule,
|
||||
BillPaymentsModule,
|
||||
PaymentsReceivedModule,
|
||||
LedgerModule,
|
||||
BankAccountsModule,
|
||||
BankRulesModule,
|
||||
BankingTransactionsModule,
|
||||
BankingTransactionsExcludeModule,
|
||||
BankingTransactionsRegonizeModule,
|
||||
BankingMatchingModule,
|
||||
TransactionsLockingModule,
|
||||
SettingsModule,
|
||||
FeaturesModule,
|
||||
InventoryAdjustmentsModule,
|
||||
InventoryCostModule,
|
||||
PostHogModule,
|
||||
EventTrackerModule,
|
||||
FinancialStatementsModule,
|
||||
StripePaymentModule,
|
||||
DashboardModule,
|
||||
PaymentLinksModule,
|
||||
RolesModule,
|
||||
SubscriptionModule,
|
||||
OrganizationModule,
|
||||
TenantDBManagerModule,
|
||||
PaymentServicesModule,
|
||||
LoopsModule,
|
||||
AttachmentsModule,
|
||||
S3Module
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: SerializeInterceptor,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: UserIpInterceptor,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ExcludeNullInterceptor,
|
||||
},
|
||||
AppService,
|
||||
],
|
||||
})
|
||||
export class AppModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(LoggerMiddleware)
|
||||
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
||||
}
|
||||
}
|
||||
10
packages/server/src/modules/App/App.service.ts
Normal file
10
packages/server/src/modules/App/App.service.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
constructor() {}
|
||||
|
||||
getHello(): string {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
80
packages/server/src/modules/Attachments/Attachment.module.ts
Normal file
80
packages/server/src/modules/Attachments/Attachment.module.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import * as multerS3 from 'multer-s3';
|
||||
import { S3_CLIENT, S3Module } from "../S3/S3.module";
|
||||
import { DeleteAttachment } from "./DeleteAttachment";
|
||||
import { GetAttachment } from "./GetAttachment";
|
||||
import { getAttachmentPresignedUrl } from "./GetAttachmentPresignedUrl";
|
||||
import { LinkAttachment } from "./LinkAttachment";
|
||||
import { UnlinkAttachment } from "./UnlinkAttachment";
|
||||
import { ValidateAttachments } from "./ValidateAttachments";
|
||||
import { AttachmentsOnBillPayments } from "./events/AttachmentsOnPaymentsMade";
|
||||
import { AttachmentsOnBills } from "./events/AttachmentsOnBills";
|
||||
import { AttachmentsOnCreditNote } from "./events/AttachmentsOnCreditNote";
|
||||
import { AttachmentsOnExpenses } from "./events/AttachmentsOnExpenses";
|
||||
import { AttachmentsOnPaymentsReceived } from "./events/AttachmentsOnPaymentsReceived";
|
||||
import { AttachmentsOnManualJournals } from "./events/AttachmentsOnManualJournals";
|
||||
import { AttachmentsOnVendorCredits } from "./events/AttachmentsOnVendorCredits";
|
||||
import { AttachmentsOnSaleInvoiceCreated } from "./events/AttachmentsOnSaleInvoice";
|
||||
import { AttachmentsController } from "./Attachments.controller";
|
||||
import { RegisterTenancyModel } from "../Tenancy/TenancyModels/Tenancy.module";
|
||||
import { DocumentModel } from "./models/Document.model";
|
||||
import { DocumentLinkModel } from "./models/DocumentLink.model";
|
||||
import { AttachmentsApplication } from "./AttachmentsApplication";
|
||||
import { UploadDocument } from "./UploadDocument";
|
||||
import { AttachmentUploadPipeline } from "./S3UploadPipeline";
|
||||
import { MULTER_MODULE_OPTIONS } from "@/common/constants/files.constants";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
const models = [
|
||||
RegisterTenancyModel(DocumentModel),
|
||||
RegisterTenancyModel(DocumentLinkModel),
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [S3Module, ...models],
|
||||
controllers: [AttachmentsController],
|
||||
providers: [
|
||||
DeleteAttachment,
|
||||
GetAttachment,
|
||||
getAttachmentPresignedUrl,
|
||||
LinkAttachment,
|
||||
UnlinkAttachment,
|
||||
ValidateAttachments,
|
||||
AttachmentsOnBillPayments,
|
||||
AttachmentsOnBills,
|
||||
AttachmentsOnCreditNote,
|
||||
AttachmentsOnExpenses,
|
||||
AttachmentsOnPaymentsReceived,
|
||||
AttachmentsOnManualJournals,
|
||||
AttachmentsOnVendorCredits,
|
||||
AttachmentsOnSaleInvoiceCreated,
|
||||
AttachmentsApplication,
|
||||
UploadDocument,
|
||||
AttachmentUploadPipeline,
|
||||
{
|
||||
provide: MULTER_MODULE_OPTIONS,
|
||||
inject: [ConfigService, S3_CLIENT],
|
||||
useFactory: (configService: ConfigService, s3: S3Client) => ({
|
||||
storage: multerS3({
|
||||
s3,
|
||||
bucket: configService.get('s3.bucket'),
|
||||
contentType: multerS3.AUTO_CONTENT_TYPE,
|
||||
metadata: function (req, file, cb) {
|
||||
cb(null, { fieldName: file.fieldname });
|
||||
},
|
||||
key: function (req, file, cb) {
|
||||
cb(null, Date.now().toString());
|
||||
},
|
||||
acl: function(req, file, cb) {
|
||||
// Conditionally set file to public or private based on isPublic flag
|
||||
const aclValue = true ? 'public-read' : 'private';
|
||||
// Set ACL based on the isPublic flag
|
||||
cb(null, aclValue);
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AttachmentsModule {}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Transformer } from "../Transformer/Transformer";
|
||||
|
||||
export class AttachmentTransformer extends Transformer {
|
||||
/**
|
||||
* Exclude attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['id', 'createdAt'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Includeded attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Transformer } from "../Transformer/Transformer";
|
||||
|
||||
export class AttachmentTransformer extends Transformer{
|
||||
/**
|
||||
* Exclude attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['id', 'createdAt'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Includeded attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import mime from 'mime-types';
|
||||
import { Response, NextFunction, Request } from 'express';
|
||||
import {
|
||||
ApiBody,
|
||||
ApiConsumes,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Res,
|
||||
UnauthorizedException,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
LinkAttachmentDto,
|
||||
UnlinkAttachmentDto,
|
||||
UploadAttachmentDto,
|
||||
} from './dtos/Attachment.dto';
|
||||
import { AttachmentsApplication } from './AttachmentsApplication';
|
||||
import { AttachmentUploadPipeline } from './S3UploadPipeline';
|
||||
import { FileInterceptor } from '@/common/interceptors/file.interceptor';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@ApiTags('Attachments')
|
||||
@Controller('/attachments')
|
||||
export class AttachmentsController {
|
||||
/**
|
||||
* @param {AttachmentsApplication} attachmentsApplication - Attachments application.
|
||||
* @param uploadPipelineService
|
||||
*/
|
||||
constructor(
|
||||
private readonly attachmentsApplication: AttachmentsApplication,
|
||||
private readonly uploadPipelineService: AttachmentUploadPipeline,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Uploads the attachments to S3 and store the file metadata to DB.
|
||||
*/
|
||||
@Post()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Upload attachment to S3' })
|
||||
@ApiBody({ description: 'Upload attachment', type: UploadAttachmentDto })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The document has been uploaded successfully',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: 'Unauthorized - File upload failed',
|
||||
})
|
||||
async uploadAttachment(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): Promise<Response | void> {
|
||||
if (!file) {
|
||||
throw new UnauthorizedException({
|
||||
errorType: 'FILE_UPLOAD_FAILED',
|
||||
message: 'Now file uploaded.',
|
||||
});
|
||||
}
|
||||
const data = await this.attachmentsApplication.upload(file);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
message: 'The document has uploaded successfully.',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given attachment key.
|
||||
*/
|
||||
@Get('/:id')
|
||||
@ApiOperation({ summary: 'Get attachment by ID' })
|
||||
@ApiParam({ name: 'id', description: 'Attachment ID' })
|
||||
@ApiResponse({ status: 200, description: 'Returns the attachment file' })
|
||||
async getAttachment(
|
||||
@Res() res: Response,
|
||||
@Param('id') documentId: string,
|
||||
): Promise<Response | void> {
|
||||
const data = await this.attachmentsApplication.get(documentId);
|
||||
|
||||
const byte = await data.Body.transformToByteArray();
|
||||
const extension = mime.extension(data.ContentType);
|
||||
const buffer = Buffer.from(byte);
|
||||
|
||||
res.set('Content-Disposition', `filename="${documentId}.${extension}"`);
|
||||
res.set('Content-Type', data.ContentType);
|
||||
res.send(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given document key.
|
||||
*/
|
||||
@Delete('/:id')
|
||||
@ApiOperation({ summary: 'Delete attachment by ID' })
|
||||
@ApiParam({ name: 'id', description: 'Attachment ID' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The document has been deleted successfully',
|
||||
})
|
||||
async deleteAttachment(
|
||||
@Res() res: Response,
|
||||
@Param('id') documentId: string,
|
||||
): Promise<Response | void> {
|
||||
await this.attachmentsApplication.delete(documentId);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
message: 'The document has been delete successfully.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given document key.
|
||||
*/
|
||||
@Post('/:id/link')
|
||||
@ApiOperation({ summary: 'Link attachment to a model' })
|
||||
@ApiParam({ name: 'id', description: 'Attachment ID' })
|
||||
@ApiBody({ type: LinkAttachmentDto })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The document has been linked successfully',
|
||||
})
|
||||
async linkDocument(
|
||||
@Body() linkDocumentDto: LinkAttachmentDto,
|
||||
@Param('id') documentId: string,
|
||||
@Res() res: Response,
|
||||
): Promise<Response | void> {
|
||||
await this.attachmentsApplication.link(
|
||||
documentId,
|
||||
linkDocumentDto.modelRef,
|
||||
linkDocumentDto.modelId,
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
message: 'The document has been linked successfully.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given document key.
|
||||
*/
|
||||
@Post('/:id/unlink')
|
||||
@ApiOperation({ summary: 'Unlink attachment from a model' })
|
||||
@ApiParam({ name: 'id', description: 'Attachment ID' })
|
||||
@ApiBody({ type: UnlinkAttachmentDto })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The document has been unlinked successfully',
|
||||
})
|
||||
async unlinkDocument(
|
||||
@Body() unlinkDto: UnlinkAttachmentDto,
|
||||
@Param('id') documentId: string,
|
||||
@Res() res: Response,
|
||||
): Promise<Response | void> {
|
||||
await this.attachmentsApplication.link(
|
||||
documentId,
|
||||
unlinkDto.modelRef,
|
||||
unlinkDto.modelId,
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
message: 'The document has been linked successfully.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreives the presigned url of the given attachment key.
|
||||
*/
|
||||
@Get('/:id/presigned-url')
|
||||
@ApiOperation({ summary: 'Get presigned URL for attachment' })
|
||||
@ApiParam({ name: 'id', description: 'Attachment ID' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Returns the presigned URL for the attachment',
|
||||
})
|
||||
async getAttachmentPresignedUrl(
|
||||
@Param('id') documentKey: string,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): Promise<Response | void> {
|
||||
const presignedUrl =
|
||||
await this.attachmentsApplication.getPresignedUrl(documentKey);
|
||||
|
||||
return res.status(200).send({ presignedUrl });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface AttachmentLinkDTO {
|
||||
key: string;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UploadDocument } from './UploadDocument';
|
||||
import { DeleteAttachment } from './DeleteAttachment';
|
||||
import { GetAttachment } from './GetAttachment';
|
||||
import { LinkAttachment } from './LinkAttachment';
|
||||
import { UnlinkAttachment } from './UnlinkAttachment';
|
||||
import { getAttachmentPresignedUrl } from './GetAttachmentPresignedUrl';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsApplication {
|
||||
constructor(
|
||||
private readonly uploadDocumentService: UploadDocument,
|
||||
private readonly deleteDocumentService: DeleteAttachment,
|
||||
private readonly getDocumentService: GetAttachment,
|
||||
private readonly linkDocumentService: LinkAttachment,
|
||||
private readonly unlinkDocumentService: UnlinkAttachment,
|
||||
private readonly getPresignedUrlService: getAttachmentPresignedUrl,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Saves the metadata of uploaded document to S3 on database.
|
||||
* @param {} file
|
||||
* @returns {Promise<Document>}
|
||||
*/
|
||||
public upload(file: any) {
|
||||
return this.uploadDocumentService.upload(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the give file attachment file key.
|
||||
* @param {string} documentKey
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public delete(documentKey: string) {
|
||||
return this.deleteDocumentService.delete(documentKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the document data.
|
||||
* @param {string} documentKey
|
||||
*/
|
||||
public get(documentKey: string) {
|
||||
return this.getDocumentService.getAttachment(documentKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given document to resource model.
|
||||
* @param {string} filekey
|
||||
* @param {string} modelRef
|
||||
* @param {number} modelId
|
||||
* @returns
|
||||
*/
|
||||
public link(filekey: string, modelRef: string, modelId: number) {
|
||||
return this.linkDocumentService.link(filekey, modelRef, modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlinks the given document from resource model.
|
||||
* @param {string} filekey
|
||||
* @param {string} modelRef
|
||||
* @param {number} modelId
|
||||
* @returns
|
||||
*/
|
||||
public unlink(filekey: string, modelRef: string, modelId: number) {
|
||||
return this.unlinkDocumentService.unlink(filekey, modelRef, modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the presigned url of the given attachment key.
|
||||
* @param {string} key
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public getPresignedUrl(key: string): Promise<string> {
|
||||
return this.getPresignedUrlService.getPresignedUrl(key);
|
||||
}
|
||||
}
|
||||
56
packages/server/src/modules/Attachments/DeleteAttachment.ts
Normal file
56
packages/server/src/modules/Attachments/DeleteAttachment.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { Knex } from 'knex';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { S3_CLIENT } from '../S3/S3.module';
|
||||
import { DocumentModel } from './models/Document.model';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { DocumentLinkModel } from './models/DocumentLink.model';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteAttachment {
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly configService: ConfigService,
|
||||
|
||||
@Inject(S3_CLIENT)
|
||||
private readonly s3Client: S3Client,
|
||||
|
||||
@Inject(DocumentModel.name)
|
||||
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
|
||||
|
||||
@Inject(DocumentLinkModel.name)
|
||||
private readonly documentLinkModel: TenantModelProxy<
|
||||
typeof DocumentLinkModel
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Deletes the give file attachment file key.
|
||||
* @param {string} filekey
|
||||
*/
|
||||
async delete(filekey: string): Promise<void> {
|
||||
const params = {
|
||||
Bucket: this.configService.get('s3.bucket'),
|
||||
Key: filekey,
|
||||
};
|
||||
await this.s3Client.send(new DeleteObjectCommand(params));
|
||||
|
||||
const foundDocument = await this.documentModel()
|
||||
.query()
|
||||
.findOne('key', filekey)
|
||||
.throwIfNotFound();
|
||||
|
||||
await this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Delete all document links
|
||||
await this.documentLinkModel()
|
||||
.query(trx)
|
||||
.where('documentId', foundDocument.id)
|
||||
.delete();
|
||||
|
||||
// Delete thedocument.
|
||||
await this.documentModel().query(trx).findById(foundDocument.id).delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
28
packages/server/src/modules/Attachments/GetAttachment.ts
Normal file
28
packages/server/src/modules/Attachments/GetAttachment.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { S3_CLIENT } from '../S3/S3.module';
|
||||
|
||||
@Injectable()
|
||||
export class GetAttachment {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
|
||||
@Inject(S3_CLIENT)
|
||||
private readonly s3: S3Client,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves data of the given document key.
|
||||
* @param {string} filekey
|
||||
*/
|
||||
async getAttachment(filekey: string) {
|
||||
const params = {
|
||||
Bucket: this.configService.get('s3.bucket'),
|
||||
Key: filekey,
|
||||
};
|
||||
const data = await this.s3.send(new GetObjectCommand(params));
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { DocumentModel } from './models/Document.model';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { S3_CLIENT } from '../S3/S3.module';
|
||||
|
||||
@Injectable()
|
||||
export class getAttachmentPresignedUrl {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
|
||||
@Inject(DocumentModel.name)
|
||||
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
|
||||
|
||||
@Inject(S3_CLIENT)
|
||||
private readonly s3Client: S3Client,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the presigned url of the given attachment key with the original filename.
|
||||
* @param {string} key -
|
||||
* @returns {string}
|
||||
*/
|
||||
async getPresignedUrl(key: string) {
|
||||
const foundDocument = await this.documentModel().query().findOne({ key });
|
||||
const config = this.configService.get('s3');
|
||||
|
||||
let ResponseContentDisposition = 'attachment';
|
||||
if (foundDocument && foundDocument.originName) {
|
||||
ResponseContentDisposition += `; filename="${foundDocument.originName}"`;
|
||||
}
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: config.bucket,
|
||||
Key: key,
|
||||
ResponseContentDisposition,
|
||||
});
|
||||
const signedUrl = await getSignedUrl(this.s3Client, command, { expiresIn: 300 });
|
||||
|
||||
return signedUrl;
|
||||
}
|
||||
}
|
||||
93
packages/server/src/modules/Attachments/LinkAttachment.ts
Normal file
93
packages/server/src/modules/Attachments/LinkAttachment.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import bluebird from 'bluebird';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
validateLinkModelEntryExists,
|
||||
validateLinkModelExists,
|
||||
} from './_utils';
|
||||
import { ERRORS } from './constants';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { DocumentLink } from '../ChromiumlyTenancy/models/DocumentLink';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
import { getAttachableModelsMap } from './decorators/InjectAttachable.decorator';
|
||||
import { DocumentModel } from './models/Document.model';
|
||||
import { SaleInvoice } from '../SaleInvoices/models/SaleInvoice';
|
||||
|
||||
@Injectable()
|
||||
export class LinkAttachment {
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DocumentLink.name)
|
||||
private readonly documentLinkModel: TenantModelProxy<typeof DocumentLink>,
|
||||
|
||||
@Inject(DocumentModel.name)
|
||||
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Links the given file key to the given model type and id.
|
||||
* @param {string} filekey - File key.
|
||||
* @param {string} modelRef - Model reference.
|
||||
* @param {number} modelId - Model id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async link(
|
||||
filekey: string,
|
||||
modelRef: string,
|
||||
modelId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const attachmentsAttachableModels = getAttachableModelsMap();
|
||||
const attachableModel = attachmentsAttachableModels.get(modelRef);
|
||||
|
||||
validateLinkModelExists(attachableModel);
|
||||
|
||||
const LinkModel = this.moduleRef.get(modelRef, { strict: false });
|
||||
const foundFile = await this.documentModel()
|
||||
.query(trx)
|
||||
.findOne('key', filekey)
|
||||
.throwIfNotFound();
|
||||
|
||||
const foundLinkModel = await LinkModel().query(trx).findById(modelId);
|
||||
validateLinkModelEntryExists(foundLinkModel);
|
||||
|
||||
const foundLinks = await this.documentLinkModel().query(trx)
|
||||
.where('modelRef', modelRef)
|
||||
.where('modelId', modelId)
|
||||
.where('documentId', foundFile.id);
|
||||
|
||||
if (foundLinks.length > 0) {
|
||||
throw new ServiceError(ERRORS.DOCUMENT_LINK_ALREADY_LINKED);
|
||||
}
|
||||
await this.documentLinkModel().query(trx).insert({
|
||||
modelRef,
|
||||
modelId,
|
||||
documentId: foundFile.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given file keys to the given model type and id.
|
||||
* @param {string[]} filekeys - File keys.
|
||||
* @param {string} modelRef - Model reference.
|
||||
* @param {number} modelId - Model id.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async bulkLink(
|
||||
filekeys: string[],
|
||||
modelRef: string,
|
||||
modelId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
return bluebird.each(filekeys, async (fieldKey: string) => {
|
||||
try {
|
||||
await this.link(fieldKey, modelRef, modelId, trx);
|
||||
} catch {
|
||||
// Ignore catching exceptions in bulk action.
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
36
packages/server/src/modules/Attachments/S3UploadPipeline.ts
Normal file
36
packages/server/src/modules/Attachments/S3UploadPipeline.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentUploadPipeline {
|
||||
constructor(
|
||||
private readonly configService: ConfigService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Middleware to ensure that S3 configuration is properly set before proceeding.
|
||||
* This function checks if the necessary S3 configuration keys are present and throws an error if any are missing.
|
||||
* @param req The HTTP request object.
|
||||
* @param res The HTTP response object.
|
||||
* @param next The callback to pass control to the next middleware function.
|
||||
*/
|
||||
public validateS3Configured(req: Request, res: Response, next: NextFunction) {
|
||||
const config = this.configService.get('s3');
|
||||
|
||||
if (
|
||||
!config.region ||
|
||||
!config.accessKeyId ||
|
||||
!config.secretAccessKey
|
||||
) {
|
||||
const missingKeys = [];
|
||||
if (!config.region) missingKeys.push('region');
|
||||
if (!config.accessKeyId) missingKeys.push('accessKeyId');
|
||||
if (!config.secretAccessKey) missingKeys.push('secretAccessKey');
|
||||
const missing = missingKeys.join(', ');
|
||||
|
||||
throw new Error(`S3 configuration error: Missing ${missing}`);
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
131
packages/server/src/modules/Attachments/UnlinkAttachment.ts
Normal file
131
packages/server/src/modules/Attachments/UnlinkAttachment.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import bluebird from 'bluebird';
|
||||
import { difference } from 'lodash';
|
||||
import {
|
||||
validateLinkModelEntryExists,
|
||||
validateLinkModelExists,
|
||||
} from './_utils';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DocumentLinkModel } from './models/DocumentLink.model';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { DocumentModel } from './models/Document.model';
|
||||
import { getAttachableModelsMap } from './decorators/InjectAttachable.decorator';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
|
||||
@Injectable()
|
||||
export class UnlinkAttachment {
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DocumentModel.name)
|
||||
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
|
||||
|
||||
@Inject(DocumentLinkModel.name)
|
||||
private readonly documentLinkModel: TenantModelProxy<
|
||||
typeof DocumentLinkModel
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Unlink the attachments from the model entry.
|
||||
* @param {string} filekey - File key.
|
||||
* @param {string} modelRef - Model reference.
|
||||
* @param {number} modelId - Model id.
|
||||
*/
|
||||
async unlink(
|
||||
filekey: string,
|
||||
modelRef: string,
|
||||
modelId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const attachmentsAttachableModels = getAttachableModelsMap();
|
||||
const attachableModel = attachmentsAttachableModels.get(modelRef);
|
||||
|
||||
validateLinkModelExists(attachableModel);
|
||||
|
||||
const LinkModel = this.moduleRef.get(modelRef, { strict: false });
|
||||
const foundLinkModel = await LinkModel.query(trx).findById(modelId);
|
||||
validateLinkModelEntryExists(foundLinkModel);
|
||||
|
||||
const document = await this.documentModel().query(trx).findOne('key', filekey);
|
||||
|
||||
// Delete the document link.
|
||||
await this.documentLinkModel().query(trx)
|
||||
.where('modelRef', modelRef)
|
||||
.where('modelId', modelId)
|
||||
.where('documentId', document.id)
|
||||
.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk unlink the attachments from the model entry.
|
||||
* @param {string} fieldkey
|
||||
* @param {string} modelRef
|
||||
* @param {number} modelId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async bulkUnlink(
|
||||
filekeys: string[],
|
||||
modelRef: string,
|
||||
modelId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
await bluebird.each(filekeys, (fieldKey: string) => {
|
||||
try {
|
||||
this.unlink(fieldKey, modelRef, modelId, trx);
|
||||
} catch {
|
||||
// Ignore catching exceptions on bulk action.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all the unpresented keys of the given model type and id.
|
||||
* @param {number} tenantId
|
||||
* @param {string[]} presentedKeys
|
||||
* @param {string} modelRef
|
||||
* @param {number} modelId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
async unlinkUnpresentedKeys(
|
||||
presentedKeys: string[],
|
||||
modelRef: string,
|
||||
modelId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const modelLinks = await this.documentLinkModel()
|
||||
.query(trx)
|
||||
.where('modelRef', modelRef)
|
||||
.where('modelId', modelId)
|
||||
.withGraphFetched('document');
|
||||
|
||||
const modelLinkKeys = modelLinks.map((link) => link.document.key);
|
||||
const unpresentedKeys = difference(modelLinkKeys, presentedKeys);
|
||||
|
||||
await this.bulkUnlink(unpresentedKeys, modelRef, modelId, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments of the given model type and id.
|
||||
* @param {string} modelRef
|
||||
* @param {number} modelId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async unlinkAllModelKeys(
|
||||
modelRef: string,
|
||||
modelId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Get all the keys of the modelRef and modelId.
|
||||
const modelLinks = await this.documentLinkModel()
|
||||
.query(trx)
|
||||
.where('modelRef', modelRef)
|
||||
.where('modelId', modelId)
|
||||
.withGraphFetched('document');
|
||||
|
||||
const modelLinkKeys = modelLinks.map((link) => link.document.key);
|
||||
|
||||
await this.bulkUnlink(modelLinkKeys, modelRef, modelId, trx);
|
||||
}
|
||||
}
|
||||
28
packages/server/src/modules/Attachments/UploadDocument.ts
Normal file
28
packages/server/src/modules/Attachments/UploadDocument.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { DocumentModel } from './models/Document.model';
|
||||
|
||||
@Injectable()
|
||||
export class UploadDocument {
|
||||
constructor(
|
||||
@Inject(DocumentModel.name)
|
||||
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Inserts the document metadata.
|
||||
* @param {number} tenantId
|
||||
* @param {} file
|
||||
* @returns {}
|
||||
*/
|
||||
async upload(file: any) {
|
||||
const insertedDocument = await this.documentModel().query().insert({
|
||||
key: file.key,
|
||||
mimeType: file.mimetype,
|
||||
size: file.size,
|
||||
originName: file.originalname,
|
||||
});
|
||||
return insertedDocument;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { castArray, difference } from 'lodash';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { DocumentModel } from './models/Document.model';
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateAttachments {
|
||||
constructor(
|
||||
@Inject(DocumentModel.name)
|
||||
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the given file keys existance.
|
||||
* @param {string|string[]} key
|
||||
*/
|
||||
async validate(key: string | string[]) {
|
||||
const keys = castArray(key);
|
||||
const documents = await this.documentModel().query().whereIn('key', key);
|
||||
const documentKeys = documents.map((document) => document.key);
|
||||
|
||||
const notFoundKeys = difference(keys, documentKeys);
|
||||
|
||||
if (notFoundKeys.length > 0) {
|
||||
throw new ServiceError('DOCUMENT_KEYS_INVALID');
|
||||
}
|
||||
}
|
||||
}
|
||||
14
packages/server/src/modules/Attachments/_utils.ts
Normal file
14
packages/server/src/modules/Attachments/_utils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
import { ERRORS } from './constants';
|
||||
|
||||
export const validateLinkModelExists = (LinkModel) => {
|
||||
if (!LinkModel) {
|
||||
throw new ServiceError(ERRORS.DOCUMENT_LINK_REF_INVALID);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateLinkModelEntryExists = (foundLinkModel) => {
|
||||
if (!foundLinkModel) {
|
||||
throw new ServiceError(ERRORS.DOCUMENT_LINK_ID_INVALID);
|
||||
}
|
||||
};
|
||||
5
packages/server/src/modules/Attachments/constants.ts
Normal file
5
packages/server/src/modules/Attachments/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum ERRORS {
|
||||
DOCUMENT_LINK_REF_INVALID = 'DOCUMENT_LINK_REF_INVALID',
|
||||
DOCUMENT_LINK_ID_INVALID = 'DOCUMENT_LINK_ID_INVALID',
|
||||
DOCUMENT_LINK_ALREADY_LINKED = 'DOCUMENT_LINK_ALREADY_LINKED'
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
// Array to store attachable model names
|
||||
const attachableModelsMap: Map<string, boolean> = new Map();
|
||||
|
||||
/**
|
||||
* Decorator that marks a class as attachable and adds its name to the attachable models array.
|
||||
* This is used to track which models can have attachments.
|
||||
*/
|
||||
export function InjectAttachable() {
|
||||
return function (target: any) {
|
||||
// Add the model name to the attachable models array
|
||||
attachableModelsMap.set(target.name, true);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attachable model names
|
||||
*/
|
||||
export function getAttachableModelsMap() {
|
||||
return attachableModelsMap;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
|
||||
export class AttachmentLinkDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
key: string;
|
||||
}
|
||||
|
||||
|
||||
export class UnlinkAttachmentDto {
|
||||
@IsNotEmpty()
|
||||
modelRef: string;
|
||||
|
||||
|
||||
@IsNotEmpty()
|
||||
modelId: number;
|
||||
}
|
||||
|
||||
export class LinkAttachmentDto {
|
||||
@IsNotEmpty()
|
||||
modelRef: string;
|
||||
|
||||
|
||||
@IsNotEmpty()
|
||||
modelId: number;
|
||||
}
|
||||
|
||||
export class UploadAttachmentDto {
|
||||
@ApiProperty({ type: 'string', format: 'binary' })
|
||||
file: any;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IBIllEventDeletedPayload,
|
||||
IBillCreatedPayload,
|
||||
IBillCreatingPayload,
|
||||
IBillEditedPayload,
|
||||
} from '@/modules/Bills/Bills.types';
|
||||
import { ValidateAttachments } from '../ValidateAttachments';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UnlinkAttachment } from '../UnlinkAttachment';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsOnBills {
|
||||
/**
|
||||
* @param {LinkAttachment} linkAttachmentService
|
||||
* @param {UnlinkAttachment} unlinkAttachmentService
|
||||
* @param {ValidateAttachments} validateDocuments
|
||||
*/
|
||||
constructor(
|
||||
private readonly linkAttachmentService: LinkAttachment,
|
||||
private readonly unlinkAttachmentService: UnlinkAttachment,
|
||||
private readonly validateDocuments: ValidateAttachments,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating bill.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.bill.onCreating)
|
||||
async validateAttachmentsOnBillCreate({
|
||||
billDTO,
|
||||
}: IBillCreatingPayload): Promise<void> {
|
||||
if (isEmpty(billDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
const documentKeys = billDTO?.attachments?.map((a) => a.key);
|
||||
|
||||
await this.validateDocuments.validate(documentKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created bill.
|
||||
* @param {ISaleInvoiceCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.bill.onCreated)
|
||||
async handleAttachmentsOnBillCreated({
|
||||
bill,
|
||||
billDTO,
|
||||
trx,
|
||||
}: IBillCreatedPayload): Promise<void> {
|
||||
if (isEmpty(billDTO.attachments)) return;
|
||||
|
||||
const keys = billDTO.attachments?.map((attachment) => attachment.key);
|
||||
|
||||
await this.linkAttachmentService.bulkLink(keys, 'Bill', bill.id, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited bill.
|
||||
* @param {IBillEditedPayload}
|
||||
*/
|
||||
@OnEvent(events.bill.onEdited)
|
||||
async handleUnlinkUnpresentedKeysOnBillEdited({
|
||||
billDTO,
|
||||
bill,
|
||||
trx,
|
||||
}: IBillEditedPayload) {
|
||||
const keys = billDTO.attachments?.map((attachment) => attachment.key);
|
||||
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'Bill',
|
||||
bill.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited bill.
|
||||
* @param {ISaleInvoiceEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.bill.onEdited)
|
||||
async handleLinkPresentedKeysOnBillEdited({
|
||||
billDTO,
|
||||
oldBill,
|
||||
trx,
|
||||
}: IBillEditedPayload) {
|
||||
if (isEmpty(billDTO.attachments)) return;
|
||||
|
||||
const keys = billDTO.attachments?.map((attachment) => attachment.key);
|
||||
|
||||
await this.linkAttachmentService.bulkLink(keys, 'Bill', oldBill.id, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments once the bill deleted.
|
||||
* @param {ISaleInvoiceDeletedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.bill.onDeleting)
|
||||
async handleUnlinkAttachmentsOnBillDeleted({
|
||||
oldBill,
|
||||
trx,
|
||||
}: IBIllEventDeletedPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'Bill',
|
||||
oldBill.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
ICreditNoteCreatedPayload,
|
||||
ICreditNoteCreatingPayload,
|
||||
ICreditNoteDeletingPayload,
|
||||
ICreditNoteEditedPayload,
|
||||
} from '@/modules/CreditNotes/types/CreditNotes.types';
|
||||
import { ValidateAttachments } from '../ValidateAttachments';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
import { UnlinkAttachment } from '../UnlinkAttachment';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsOnCreditNote {
|
||||
/**
|
||||
* @param {LinkAttachment} linkAttachmentService -
|
||||
* @param {UnlinkAttachment} unlinkAttachmentService -
|
||||
* @param {ValidateAttachments} validateDocuments -
|
||||
*/
|
||||
constructor(
|
||||
private readonly linkAttachmentService: LinkAttachment,
|
||||
private readonly unlinkAttachmentService: UnlinkAttachment,
|
||||
private readonly validateDocuments: ValidateAttachments,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating credit note.
|
||||
* @param {ICreditNoteCreatingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.creditNote.onCreating)
|
||||
async validateAttachmentsOnCreditNoteCreate({
|
||||
creditNoteDTO,
|
||||
}: ICreditNoteCreatingPayload): Promise<void> {
|
||||
if (isEmpty(creditNoteDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
const documentKeys = creditNoteDTO?.attachments?.map((a) => a.key);
|
||||
|
||||
await this.validateDocuments.validate(documentKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created credit note.
|
||||
* @param {ICreditNoteCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.creditNote.onCreated)
|
||||
async handleAttachmentsOnCreditNoteCreated({
|
||||
creditNote,
|
||||
creditNoteDTO,
|
||||
trx,
|
||||
}: ICreditNoteCreatedPayload): Promise<void> {
|
||||
if (isEmpty(creditNoteDTO.attachments)) return;
|
||||
|
||||
const keys = creditNoteDTO.attachments?.map((attachment) => attachment.key);
|
||||
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'CreditNote',
|
||||
creditNote.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited credit note.
|
||||
* @param {ICreditNoteEditedPayload}
|
||||
*/
|
||||
@OnEvent(events.creditNote.onEdited)
|
||||
async handleUnlinkUnpresentedKeysOnCreditNoteEdited({
|
||||
creditNoteEditDTO,
|
||||
oldCreditNote,
|
||||
trx,
|
||||
}: ICreditNoteEditedPayload) {
|
||||
const keys = creditNoteEditDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'CreditNote',
|
||||
oldCreditNote.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited credit note.
|
||||
* @param {ICreditNoteEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.creditNote.onEdited)
|
||||
async handleLinkPresentedKeysOnCreditNoteEdited({
|
||||
creditNoteEditDTO,
|
||||
oldCreditNote,
|
||||
trx,
|
||||
}: ICreditNoteEditedPayload) {
|
||||
if (isEmpty(creditNoteEditDTO.attachments)) return;
|
||||
|
||||
const keys = creditNoteEditDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'CreditNote',
|
||||
oldCreditNote.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments once the credit note deleted.
|
||||
* @param {ICreditNoteDeletingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.creditNote.onDeleting)
|
||||
async handleUnlinkAttachmentsOnCreditNoteDeleted({
|
||||
oldCreditNote,
|
||||
trx,
|
||||
}: ICreditNoteDeletingPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'CreditNote',
|
||||
oldCreditNote.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IExpenseCreatedPayload,
|
||||
IExpenseCreatingPayload,
|
||||
IExpenseDeletingPayload,
|
||||
IExpenseEventEditPayload,
|
||||
} from '@/modules/Expenses/Expenses.types';
|
||||
import { ValidateAttachments } from '../ValidateAttachments';
|
||||
import { UnlinkAttachment } from '../UnlinkAttachment';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsOnExpenses {
|
||||
constructor(
|
||||
private readonly linkAttachmentService: LinkAttachment,
|
||||
private readonly unlinkAttachmentService: UnlinkAttachment,
|
||||
private readonly validateDocuments: ValidateAttachments,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating expense.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.expenses.onCreating)
|
||||
async validateAttachmentsOnExpenseCreate({
|
||||
expenseDTO,
|
||||
}: IExpenseCreatingPayload): Promise<void> {
|
||||
if (isEmpty(expenseDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
const documentKeys = expenseDTO?.attachments?.map((a) => a.key);
|
||||
|
||||
await this.validateDocuments.validate(documentKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created expense.
|
||||
* @param {ISaleInvoiceCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.expenses.onCreated)
|
||||
async handleAttachmentsOnExpenseCreated({
|
||||
expenseDTO,
|
||||
expense,
|
||||
trx,
|
||||
}: IExpenseCreatedPayload): Promise<void> {
|
||||
if (isEmpty(expenseDTO.attachments)) return;
|
||||
|
||||
const keys = expenseDTO.attachments?.map((attachment) => attachment.key);
|
||||
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'Expense',
|
||||
expense.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited expense.
|
||||
* @param {ISaleInvoiceEditedPayload}
|
||||
*/
|
||||
@OnEvent(events.expenses.onEdited)
|
||||
async handleUnlinkUnpresentedKeysOnExpenseEdited({
|
||||
expenseDTO,
|
||||
expense,
|
||||
trx,
|
||||
}: IExpenseEventEditPayload) {
|
||||
const keys = expenseDTO.attachments?.map((attachment) => attachment.key);
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'Expense',
|
||||
expense.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited expense.
|
||||
* @param {ISaleInvoiceEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.expenses.onEdited)
|
||||
async handleLinkPresentedKeysOnExpenseEdited({
|
||||
expenseDTO,
|
||||
oldExpense,
|
||||
trx,
|
||||
}: IExpenseEventEditPayload) {
|
||||
if (isEmpty(expenseDTO.attachments)) return;
|
||||
|
||||
const keys = expenseDTO.attachments?.map((attachment) => attachment.key);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'Expense',
|
||||
oldExpense.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments once the expense deleted.
|
||||
* @param {ISaleInvoiceDeletedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.expenses.onDeleting)
|
||||
async handleUnlinkAttachmentsOnExpenseDeleted({
|
||||
oldExpense,
|
||||
trx,
|
||||
}: IExpenseDeletingPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'Expense',
|
||||
oldExpense.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IManualJournalCreatingPayload,
|
||||
IManualJournalEventCreatedPayload,
|
||||
IManualJournalEventDeletedPayload,
|
||||
IManualJournalEventEditedPayload,
|
||||
} from '@/modules/ManualJournals/types/ManualJournals.types';
|
||||
import { ValidateAttachments } from '../ValidateAttachments';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { UnlinkAttachment } from '../UnlinkAttachment';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsOnManualJournals {
|
||||
constructor(
|
||||
private readonly linkAttachmentService: LinkAttachment,
|
||||
private readonly unlinkAttachmentService: UnlinkAttachment,
|
||||
private readonly validateDocuments: ValidateAttachments,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating manual journal.
|
||||
* @param {IManualJournalCreatingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.manualJournals.onCreating)
|
||||
async validateAttachmentsOnManualJournalCreate({
|
||||
manualJournalDTO,
|
||||
}: IManualJournalCreatingPayload): Promise<void> {
|
||||
if (isEmpty(manualJournalDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
const documentKeys = manualJournalDTO?.attachments?.map((a) => a.key);
|
||||
|
||||
await this.validateDocuments.validate(documentKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created manual journal.
|
||||
* @param {IManualJournalEventCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.manualJournals.onCreated)
|
||||
async handleAttachmentsOnManualJournalCreated({
|
||||
manualJournalDTO,
|
||||
manualJournal,
|
||||
trx,
|
||||
}: IManualJournalEventCreatedPayload): Promise<void> {
|
||||
if (isEmpty(manualJournalDTO.attachments)) return;
|
||||
|
||||
const keys = manualJournalDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'ManualJournal',
|
||||
manualJournal.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited manual journal.
|
||||
* @param {ISaleInvoiceEditedPayload}
|
||||
*/
|
||||
@OnEvent(events.manualJournals.onEdited)
|
||||
async handleUnlinkUnpresentedKeysOnManualJournalEdited({
|
||||
manualJournalDTO,
|
||||
manualJournal,
|
||||
trx,
|
||||
}: IManualJournalEventEditedPayload) {
|
||||
const keys = manualJournalDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'SaleInvoice',
|
||||
manualJournal.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited manual journal.
|
||||
* @param {ISaleInvoiceEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.manualJournals.onEdited)
|
||||
async handleLinkPresentedKeysOnManualJournalEdited({
|
||||
manualJournalDTO,
|
||||
oldManualJournal,
|
||||
trx,
|
||||
}: IManualJournalEventEditedPayload) {
|
||||
if (isEmpty(manualJournalDTO.attachments)) return;
|
||||
|
||||
const keys = manualJournalDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'ManualJournal',
|
||||
oldManualJournal.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments once the manual journal deleted.
|
||||
* @param {ISaleInvoiceDeletedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.manualJournals.onDeleting)
|
||||
async handleUnlinkAttachmentsOnManualJournalDeleted({
|
||||
oldManualJournal,
|
||||
trx,
|
||||
}: IManualJournalEventDeletedPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'SaleInvoice',
|
||||
oldManualJournal.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IBillPaymentCreatingPayload,
|
||||
IBillPaymentDeletingPayload,
|
||||
IBillPaymentEventCreatedPayload,
|
||||
IBillPaymentEventEditedPayload,
|
||||
} from '@/modules/BillPayments/types/BillPayments.types';
|
||||
import { ValidateAttachments } from '../ValidateAttachments';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
import { UnlinkAttachment } from '../UnlinkAttachment';
|
||||
import { events } from '@/common/events/events';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsOnBillPayments {
|
||||
constructor(
|
||||
private readonly linkAttachmentService: LinkAttachment,
|
||||
private readonly unlinkAttachmentService: UnlinkAttachment,
|
||||
private readonly validateDocuments: ValidateAttachments,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating bill payment.
|
||||
* @param {IBillPaymentCreatingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.billPayment.onCreating)
|
||||
async validateAttachmentsOnBillPaymentCreate({
|
||||
billPaymentDTO,
|
||||
}: IBillPaymentCreatingPayload): Promise<void> {
|
||||
if (isEmpty(billPaymentDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
const documentKeys = billPaymentDTO?.attachments?.map((a) => a.key);
|
||||
|
||||
await this.validateDocuments.validate(documentKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created bill payment.
|
||||
* @param {IBillPaymentEventCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.billPayment.onCreated)
|
||||
async handleAttachmentsOnBillPaymentCreated({
|
||||
billPaymentDTO,
|
||||
billPayment,
|
||||
trx,
|
||||
}: IBillPaymentEventCreatedPayload): Promise<void> {
|
||||
if (isEmpty(billPaymentDTO.attachments)) return;
|
||||
|
||||
const keys = billPaymentDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'BillPayment',
|
||||
billPayment.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited bill payment.
|
||||
* @param {IBillPaymentEventEditedPayload}
|
||||
*/
|
||||
@OnEvent(events.billPayment.onEdited)
|
||||
async handleUnlinkUnpresentedKeysOnBillPaymentEdited({
|
||||
billPaymentDTO,
|
||||
oldBillPayment,
|
||||
trx,
|
||||
}: IBillPaymentEventEditedPayload) {
|
||||
const keys = billPaymentDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'BillPayment',
|
||||
oldBillPayment.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited bill payment.
|
||||
* @param {IBillPaymentEventEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.billPayment.onEdited)
|
||||
async handleLinkPresentedKeysOnBillPaymentEdited({
|
||||
billPaymentDTO,
|
||||
oldBillPayment,
|
||||
trx,
|
||||
}: IBillPaymentEventEditedPayload) {
|
||||
if (isEmpty(billPaymentDTO.attachments)) return;
|
||||
|
||||
const keys = billPaymentDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'BillPayment',
|
||||
oldBillPayment.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments once the bill payment deleted.
|
||||
* @param {IBillPaymentDeletingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.billPayment.onDeleting)
|
||||
async handleUnlinkAttachmentsOnBillPaymentDeleted({
|
||||
oldBillPayment,
|
||||
trx,
|
||||
}: IBillPaymentDeletingPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'BillPayment',
|
||||
oldBillPayment.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IPaymentReceivedCreatedPayload,
|
||||
IPaymentReceivedCreatingPayload,
|
||||
IPaymentReceivedDeletingPayload,
|
||||
IPaymentReceivedEditedPayload,
|
||||
} from '@/modules/PaymentReceived/types/PaymentReceived.types';
|
||||
import { ValidateAttachments } from '../ValidateAttachments';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UnlinkAttachment } from '../UnlinkAttachment';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsOnPaymentsReceived {
|
||||
constructor(
|
||||
private readonly linkAttachmentService: LinkAttachment,
|
||||
private readonly unlinkAttachmentService: UnlinkAttachment,
|
||||
private readonly validateDocuments: ValidateAttachments,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating payment.
|
||||
* @param {IPaymentReceivedCreatingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.paymentReceive.onCreating)
|
||||
async validateAttachmentsOnPaymentCreate({
|
||||
paymentReceiveDTO,
|
||||
}: IPaymentReceivedCreatingPayload): Promise<void> {
|
||||
if (isEmpty(paymentReceiveDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
const documentKeys = paymentReceiveDTO?.attachments?.map((a) => a.key);
|
||||
|
||||
await this.validateDocuments.validate(documentKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created payment.
|
||||
* @param {IPaymentReceivedCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.paymentReceive.onCreated)
|
||||
async handleAttachmentsOnPaymentCreated({
|
||||
paymentReceiveDTO,
|
||||
paymentReceive,
|
||||
trx,
|
||||
}: IPaymentReceivedCreatedPayload): Promise<void> {
|
||||
if (isEmpty(paymentReceiveDTO.attachments)) return;
|
||||
|
||||
const keys = paymentReceiveDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'PaymentReceive',
|
||||
paymentReceive.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited payment.
|
||||
* @param {IPaymentReceivedEditedPayload}
|
||||
*/
|
||||
@OnEvent(events.paymentReceive.onEdited)
|
||||
private async handleUnlinkUnpresentedKeysOnPaymentEdited({
|
||||
paymentReceiveDTO,
|
||||
oldPaymentReceive,
|
||||
trx,
|
||||
}: IPaymentReceivedEditedPayload) {
|
||||
const keys = paymentReceiveDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'PaymentReceive',
|
||||
oldPaymentReceive.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited payment.
|
||||
* @param {IPaymentReceivedEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.paymentReceive.onEdited)
|
||||
async handleLinkPresentedKeysOnPaymentEdited({
|
||||
paymentReceiveDTO,
|
||||
oldPaymentReceive,
|
||||
trx,
|
||||
}: IPaymentReceivedEditedPayload) {
|
||||
if (isEmpty(paymentReceiveDTO.attachments)) return;
|
||||
|
||||
const keys = paymentReceiveDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'PaymentReceive',
|
||||
oldPaymentReceive.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments once the payment deleted.
|
||||
* @param {ISaleInvoiceDeletedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.paymentReceive.onDeleting)
|
||||
async handleUnlinkAttachmentsOnPaymentDelete({
|
||||
oldPaymentReceive,
|
||||
trx,
|
||||
}: IPaymentReceivedDeletingPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'PaymentReceive',
|
||||
oldPaymentReceive.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
ISaleEstimateCreatedPayload,
|
||||
ISaleEstimateCreatingPayload,
|
||||
ISaleEstimateDeletingPayload,
|
||||
ISaleEstimateEditedPayload,
|
||||
} from '@/modules/SaleEstimates/types/SaleEstimates.types';
|
||||
import { ValidateAttachments } from '../ValidateAttachments';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UnlinkAttachment } from '../UnlinkAttachment';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsOnSaleEstimates {
|
||||
constructor(
|
||||
private readonly linkAttachmentService: LinkAttachment,
|
||||
private readonly unlinkAttachmentService: UnlinkAttachment,
|
||||
private readonly validateDocuments: ValidateAttachments,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating sale estimate.
|
||||
* @param {ISaleEstimateCreatingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleEstimate.onCreating)
|
||||
async validateAttachmentsOnSaleEstimateCreated({
|
||||
estimateDTO,
|
||||
}: ISaleEstimateCreatingPayload): Promise<void> {
|
||||
if (isEmpty(estimateDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
const documentKeys = estimateDTO?.attachments?.map((a) => a.key);
|
||||
|
||||
await this.validateDocuments.validate(documentKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created sale estimate.
|
||||
* @param {ISaleEstimateCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleEstimate.onCreated)
|
||||
async handleAttachmentsOnSaleEstimateCreated({
|
||||
saleEstimateDTO,
|
||||
saleEstimate,
|
||||
trx,
|
||||
}: ISaleEstimateCreatedPayload): Promise<void> {
|
||||
if (isEmpty(saleEstimateDTO.attachments)) return;
|
||||
|
||||
const keys = saleEstimateDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'SaleEstimate',
|
||||
saleEstimate.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited sale estimate.
|
||||
* @param {ISaleEstimateEditedPayload}
|
||||
*/
|
||||
@OnEvent(events.saleEstimate.onEdited)
|
||||
async handleUnlinkUnpresentedKeysOnSaleEstimateEdited({
|
||||
estimateDTO,
|
||||
oldSaleEstimate,
|
||||
trx,
|
||||
}: ISaleEstimateEditedPayload) {
|
||||
const keys = estimateDTO.attachments?.map((attachment) => attachment.key);
|
||||
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'SaleEstimate',
|
||||
oldSaleEstimate.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited sale estimate.
|
||||
* @param {ISaleEstimateEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleEstimate.onEdited)
|
||||
async handleLinkPresentedKeysOnSaleEstimateEdited({
|
||||
estimateDTO,
|
||||
oldSaleEstimate,
|
||||
trx,
|
||||
}: ISaleEstimateEditedPayload) {
|
||||
if (isEmpty(estimateDTO.attachments)) return;
|
||||
|
||||
const keys = estimateDTO.attachments?.map((attachment) => attachment.key);
|
||||
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'SaleEstimate',
|
||||
oldSaleEstimate.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments once the estimate deleted.
|
||||
* @param {ISaleEstimateDeletingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleEstimate.onDeleting)
|
||||
async handleUnlinkAttachmentsOnSaleEstimateDelete({
|
||||
oldSaleEstimate,
|
||||
trx,
|
||||
}: ISaleEstimateDeletingPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'SaleEstimate',
|
||||
oldSaleEstimate.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import {
|
||||
ISaleInvoiceCreatedPayload,
|
||||
ISaleInvoiceCreatingPaylaod,
|
||||
ISaleInvoiceDeletingPayload,
|
||||
ISaleInvoiceEditedPayload,
|
||||
} from '@/modules/SaleInvoices/SaleInvoice.types';
|
||||
import { ValidateAttachments } from '../ValidateAttachments';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
import { UnlinkAttachment } from '../UnlinkAttachment';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsOnSaleInvoiceCreated {
|
||||
constructor(
|
||||
private readonly linkAttachmentService: LinkAttachment,
|
||||
private readonly unlinkAttachmentService: UnlinkAttachment,
|
||||
private readonly validateDocuments: ValidateAttachments,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating sale invoice.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onCreating)
|
||||
async validateAttachmentsOnSaleInvoiceCreate({
|
||||
saleInvoiceDTO,
|
||||
}: ISaleInvoiceCreatingPaylaod): Promise<void> {
|
||||
if (isEmpty(saleInvoiceDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
const documentKeys = saleInvoiceDTO?.attachments?.map((a) => a.key);
|
||||
|
||||
await this.validateDocuments.validate(documentKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created sale invoice.
|
||||
* @param {ISaleInvoiceCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onCreated)
|
||||
async handleAttachmentsOnSaleInvoiceCreated({
|
||||
saleInvoiceDTO,
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceCreatedPayload): Promise<void> {
|
||||
if (isEmpty(saleInvoiceDTO.attachments)) return;
|
||||
|
||||
const keys = saleInvoiceDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'SaleInvoice',
|
||||
saleInvoice.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited sale invoice.
|
||||
* @param {ISaleInvoiceEditedPayload}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onEdited)
|
||||
async handleUnlinkUnpresentedKeysOnInvoiceEdited({
|
||||
saleInvoiceDTO,
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceEditedPayload) {
|
||||
// if (isEmpty(saleInvoiceDTO.attachments)) return;
|
||||
|
||||
const keys = saleInvoiceDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'SaleInvoice',
|
||||
saleInvoice.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited sale invoice.
|
||||
* @param {ISaleInvoiceEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onEdited)
|
||||
async handleLinkPresentedKeysOnInvoiceEdited({
|
||||
saleInvoiceDTO,
|
||||
oldSaleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceEditedPayload) {
|
||||
if (isEmpty(saleInvoiceDTO.attachments)) return;
|
||||
|
||||
const keys = saleInvoiceDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'SaleInvoice',
|
||||
oldSaleInvoice.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments once the invoice deleted.
|
||||
* @param {ISaleInvoiceDeletedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleInvoice.onDeleting)
|
||||
async handleUnlinkAttachmentsOnInvoiceDeleted({
|
||||
oldSaleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceDeletingPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'SaleInvoice',
|
||||
oldSaleInvoice.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import {
|
||||
ISaleReceiptCreatedPayload,
|
||||
ISaleReceiptCreatingPayload,
|
||||
ISaleReceiptDeletingPayload,
|
||||
ISaleReceiptEditedPayload,
|
||||
} from '../../SaleReceipts/types/SaleReceipts.types';
|
||||
import { ValidateAttachments } from '../ValidateAttachments';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
import { UnlinkAttachment } from '../UnlinkAttachment';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsOnSaleReceipt {
|
||||
constructor(
|
||||
private readonly linkAttachmentService: LinkAttachment,
|
||||
private readonly unlinkAttachmentService: UnlinkAttachment,
|
||||
private readonly validateDocuments: ValidateAttachments,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating sale receipt.
|
||||
* @param {ISaleReceiptCreatingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleReceipt.onCreating)
|
||||
async validateAttachmentsOnSaleInvoiceCreate({
|
||||
saleReceiptDTO,
|
||||
}: ISaleReceiptCreatingPayload): Promise<void> {
|
||||
if (isEmpty(saleReceiptDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
const documentKeys = saleReceiptDTO?.attachments?.map((a) => a.key);
|
||||
|
||||
await this.validateDocuments.validate(documentKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created sale receipt.
|
||||
* @param {ISaleReceiptCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleReceipt.onCreated)
|
||||
async handleAttachmentsOnSaleInvoiceCreated({
|
||||
saleReceiptDTO,
|
||||
saleReceipt,
|
||||
trx,
|
||||
}: ISaleReceiptCreatedPayload): Promise<void> {
|
||||
if (isEmpty(saleReceiptDTO.attachments)) return;
|
||||
|
||||
const keys = saleReceiptDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'SaleReceipt',
|
||||
saleReceipt.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited sale receipt.
|
||||
* @param {ISaleReceiptEditedPayload}
|
||||
*/
|
||||
@OnEvent(events.saleReceipt.onEdited)
|
||||
async handleUnlinkUnpresentedKeysOnInvoiceEdited({
|
||||
saleReceiptDTO,
|
||||
saleReceipt,
|
||||
trx,
|
||||
}: ISaleReceiptEditedPayload) {
|
||||
const keys = saleReceiptDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'SaleReceipt',
|
||||
saleReceipt.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited sale receipt.
|
||||
* @param {ISaleReceiptEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleReceipt.onEdited)
|
||||
async handleLinkPresentedKeysOnInvoiceEdited({
|
||||
saleReceiptDTO,
|
||||
oldSaleReceipt,
|
||||
trx,
|
||||
}: ISaleReceiptEditedPayload) {
|
||||
if (isEmpty(saleReceiptDTO.attachments)) return;
|
||||
|
||||
const keys = saleReceiptDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'SaleReceipt',
|
||||
oldSaleReceipt.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments once the receipt deleted.
|
||||
* @param {ISaleReceiptDeletingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.saleReceipt.onDeleting)
|
||||
async handleUnlinkAttachmentsOnReceiptDeleted({
|
||||
oldSaleReceipt,
|
||||
trx,
|
||||
}: ISaleReceiptDeletingPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'SaleReceipt',
|
||||
oldSaleReceipt.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IVendorCreditCreatedPayload,
|
||||
IVendorCreditCreatingPayload,
|
||||
IVendorCreditDeletingPayload,
|
||||
IVendorCreditEditedPayload,
|
||||
} from '../../VendorCredit/types/VendorCredit.types';
|
||||
import { ValidateAttachments } from '../ValidateAttachments';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UnlinkAttachment } from '../UnlinkAttachment';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsOnVendorCredits {
|
||||
constructor(
|
||||
private readonly linkAttachmentService: LinkAttachment,
|
||||
private readonly unlinkAttachmentService: UnlinkAttachment,
|
||||
private readonly validateDocuments: ValidateAttachments,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating vendor credit.
|
||||
* @param {IVendorCreditCreatingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.vendorCredit.onCreating)
|
||||
async validateAttachmentsOnVendorCreditCreate({
|
||||
vendorCreditCreateDTO,
|
||||
}: IVendorCreditCreatingPayload): Promise<void> {
|
||||
if (isEmpty(vendorCreditCreateDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
const documentKeys = vendorCreditCreateDTO?.attachments?.map((a) => a.key);
|
||||
|
||||
await this.validateDocuments.validate(documentKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created vendor credit.
|
||||
* @param {IVendorCreditCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.vendorCredit.onCreated)
|
||||
async handleAttachmentsOnVendorCreditCreated({
|
||||
vendorCreditCreateDTO,
|
||||
vendorCredit,
|
||||
trx,
|
||||
}: IVendorCreditCreatedPayload): Promise<void> {
|
||||
if (isEmpty(vendorCreditCreateDTO.attachments)) return;
|
||||
|
||||
const keys = vendorCreditCreateDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'VendorCredit',
|
||||
vendorCredit.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited vendor credit.
|
||||
* @param {IVendorCreditEditedPayload}
|
||||
*/
|
||||
@OnEvent(events.vendorCredit.onEdited)
|
||||
async handleUnlinkUnpresentedKeysOnVendorCreditEdited({
|
||||
vendorCreditDTO,
|
||||
oldVendorCredit,
|
||||
trx,
|
||||
}: IVendorCreditEditedPayload) {
|
||||
const keys = vendorCreditDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'VendorCredit',
|
||||
oldVendorCredit.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited vendor credit.
|
||||
* @param {IVendorCreditEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.vendorCredit.onEdited)
|
||||
async handleLinkPresentedKeysOnVendorCreditEdited({
|
||||
vendorCreditDTO,
|
||||
oldVendorCredit,
|
||||
trx,
|
||||
}: IVendorCreditEditedPayload) {
|
||||
if (isEmpty(vendorCreditDTO.attachments)) return;
|
||||
|
||||
const keys = vendorCreditDTO.attachments?.map(
|
||||
(attachment) => attachment.key,
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'VendorCredit',
|
||||
oldVendorCredit.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink all attachments once the vendor credit deleted.
|
||||
* @param {IVendorCreditDeletingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.vendorCredit.onDeleting)
|
||||
async handleUnlinkAttachmentsOnVendorCreditDeleted({
|
||||
oldVendorCredit,
|
||||
trx,
|
||||
}: IVendorCreditDeletingPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'VendorCredit',
|
||||
oldVendorCredit.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
export class DocumentModel extends TenantBaseModel {
|
||||
originName!: string;
|
||||
size!: number;
|
||||
mimeType!: string;
|
||||
key!: string;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'documents';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Model, mixin } from 'objection';
|
||||
import { DocumentModel } from './Document.model';
|
||||
|
||||
export class DocumentLinkModel extends TenantBaseModel {
|
||||
document!: DocumentModel;
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'document_links';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { DocumentModel } = require('./Document.model');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Sale invoice associated entries.
|
||||
*/
|
||||
document: {
|
||||
relation: Model.HasOneRelation,
|
||||
modelClass: DocumentModel,
|
||||
join: {
|
||||
from: 'document_links.documentId',
|
||||
to: 'documents.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
10
packages/server/src/modules/Attachments/utils.ts
Normal file
10
packages/server/src/modules/Attachments/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import path from 'path';
|
||||
// import config from '@/config';
|
||||
|
||||
export const getUploadedObjectUri = (objectKey: string) => {
|
||||
return '';
|
||||
// return new URL(
|
||||
// path.join(config.s3.bucket, objectKey),
|
||||
// config.s3.endpoint
|
||||
// ).toString();
|
||||
};
|
||||
28
packages/server/src/modules/Auth/Auth.constants.ts
Normal file
28
packages/server/src/modules/Auth/Auth.constants.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const jwtConstants = {
|
||||
secret:
|
||||
'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
|
||||
};
|
||||
|
||||
export const ERRORS = {
|
||||
INVALID_DETAILS: 'INVALID_DETAILS',
|
||||
USER_INACTIVE: 'USER_INACTIVE',
|
||||
EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND',
|
||||
TOKEN_INVALID: 'TOKEN_INVALID',
|
||||
USER_NOT_FOUND: 'USER_NOT_FOUND',
|
||||
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
||||
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
|
||||
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
||||
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
|
||||
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
|
||||
SIGNUP_CONFIRM_TOKEN_INVALID: 'SIGNUP_CONFIRM_TOKEN_INVALID',
|
||||
USER_ALREADY_VERIFIED: 'USER_ALREADY_VERIFIED',
|
||||
};
|
||||
|
||||
export const IS_PUBLIC_ROUTE = 'isPublic';
|
||||
|
||||
export const SendResetPasswordMailQueue = 'SendResetPasswordMailQueue';
|
||||
export const SendResetPasswordMailJob = 'SendResetPasswordMailJob';
|
||||
|
||||
export const SendSignupVerificationMailQueue =
|
||||
'SendSignupVerificationMailQueue';
|
||||
export const SendSignupVerificationMailJob = 'SendSignupVerificationMailJob';
|
||||
96
packages/server/src/modules/Auth/Auth.controller.ts
Normal file
96
packages/server/src/modules/Auth/Auth.controller.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
// @ts-nocheck
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Request,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBody, ApiParam } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard, PublicRoute } from './guards/jwt.guard';
|
||||
import { AuthenticationApplication } from './AuthApplication.sevice';
|
||||
import { AuthSignupDto } from './dtos/AuthSignup.dto';
|
||||
import { AuthSigninDto } from './dtos/AuthSignin.dto';
|
||||
import { LocalAuthGuard } from './guards/local.guard';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AuthSigninService } from './commands/AuthSignin.service';
|
||||
|
||||
@Controller('/auth')
|
||||
@ApiTags('Auth')
|
||||
@PublicRoute()
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private readonly authApp: AuthenticationApplication,
|
||||
private readonly authSignin: AuthSigninService,
|
||||
) {}
|
||||
|
||||
@Post('/signin')
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@ApiOperation({ summary: 'Sign in a user' })
|
||||
@ApiBody({ type: AuthSigninDto })
|
||||
signin(@Request() req: Request, @Body() signinDto: AuthSigninDto) {
|
||||
const { user } = req;
|
||||
return { access_token: this.authSignin.signToken(user) };
|
||||
}
|
||||
|
||||
@Post('/signup')
|
||||
@ApiOperation({ summary: 'Sign up a new user' })
|
||||
@ApiBody({ type: AuthSignupDto })
|
||||
signup(@Request() req: Request, @Body() signupDto: AuthSignupDto) {
|
||||
return this.authApp.signUp(signupDto);
|
||||
}
|
||||
|
||||
@Post('/signup/confirm')
|
||||
@ApiOperation({ summary: 'Confirm user signup' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string', example: 'user@example.com' },
|
||||
token: { type: 'string', example: 'confirmation-token' },
|
||||
},
|
||||
},
|
||||
})
|
||||
signupConfirm(@Body('email') email: string, @Body('token') token: string) {
|
||||
return this.authApp.signUpConfirm(email, token);
|
||||
}
|
||||
|
||||
@Post('/send_reset_password')
|
||||
@ApiOperation({ summary: 'Send reset password email' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string', example: 'user@example.com' },
|
||||
},
|
||||
},
|
||||
})
|
||||
sendResetPassword(@Body('email') email: string) {
|
||||
return this.authApp.sendResetPassword(email);
|
||||
}
|
||||
|
||||
@Post('/reset_password/:token')
|
||||
@ApiOperation({ summary: 'Reset password using token' })
|
||||
@ApiParam({ name: 'token', description: 'Reset password token' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
password: { type: 'string', example: 'new-password' },
|
||||
},
|
||||
},
|
||||
})
|
||||
resetPassword(
|
||||
@Param('token') token: string,
|
||||
@Body('password') password: string,
|
||||
) {
|
||||
return this.authApp.resetPassword(token, password);
|
||||
}
|
||||
|
||||
@Get('/meta')
|
||||
meta() {
|
||||
return this.authApp.getAuthMeta();
|
||||
}
|
||||
}
|
||||
74
packages/server/src/modules/Auth/Auth.interfaces.ts
Normal file
74
packages/server/src/modules/Auth/Auth.interfaces.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { ModelObject } from 'objection';
|
||||
import { SystemUser } from '../System/models/SystemUser';
|
||||
import { TenantModel } from '../System/models/TenantModel';
|
||||
import { AuthSignupDto } from './dtos/AuthSignup.dto';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export interface IAuthSignedInEventPayload {}
|
||||
export interface IAuthSigningInEventPayload {}
|
||||
export interface IAuthSignInPOJO {}
|
||||
|
||||
export interface IAuthSigningInEventPayload {
|
||||
email: string;
|
||||
password: string;
|
||||
user: ModelObject<SystemUser>;
|
||||
}
|
||||
|
||||
export interface IAuthSignedInEventPayload {
|
||||
email: string;
|
||||
password: string;
|
||||
user: ModelObject<SystemUser>;
|
||||
}
|
||||
|
||||
export interface IAuthSigningUpEventPayload {
|
||||
signupDTO: AuthSignupDto;
|
||||
}
|
||||
|
||||
export interface IAuthSignedUpEventPayload {
|
||||
signupDTO: AuthSignupDto;
|
||||
tenant: TenantModel;
|
||||
user: SystemUser;
|
||||
}
|
||||
|
||||
export interface IAuthSignInPOJO {
|
||||
user: ModelObject<SystemUser>;
|
||||
token: string;
|
||||
tenant: ModelObject<TenantModel>;
|
||||
}
|
||||
|
||||
export interface IAuthResetedPasswordEventPayload {
|
||||
user: SystemUser;
|
||||
token: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface IAuthSendingResetPassword {
|
||||
user: SystemUser;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface IAuthSendedResetPassword {
|
||||
user: SystemUser;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface IAuthGetMetaPOJO {
|
||||
signupDisabled: boolean;
|
||||
}
|
||||
|
||||
export interface IAuthSignUpVerifingEventPayload {
|
||||
email: string;
|
||||
verifyToken: string;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export interface IAuthSignUpVerifiedEventPayload {
|
||||
email: string;
|
||||
verifyToken: string;
|
||||
userId: number;
|
||||
}
|
||||
75
packages/server/src/modules/Auth/Auth.module.ts
Normal file
75
packages/server/src/modules/Auth/Auth.module.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthController } from './Auth.controller';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { JwtStrategy } from './strategies/Jwt.strategy';
|
||||
import { AuthenticationApplication } from './AuthApplication.sevice';
|
||||
import { AuthSendResetPasswordService } from './commands/AuthSendResetPassword.service';
|
||||
import { AuthResetPasswordService } from './commands/AuthResetPassword.service';
|
||||
import { AuthSignupConfirmResendService } from './commands/AuthSignupConfirmResend.service';
|
||||
import { AuthSignupConfirmService } from './commands/AuthSignupConfirm.service';
|
||||
import { AuthSignupService } from './commands/AuthSignup.service';
|
||||
import { AuthSigninService } from './commands/AuthSignin.service';
|
||||
import { PasswordReset } from './models/PasswordReset';
|
||||
import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module';
|
||||
import { AuthenticationMailMesssages } from './AuthMailMessages.esrvice';
|
||||
import { LocalStrategy } from './strategies/Local.strategy';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { JwtAuthGuard } from './guards/jwt.guard';
|
||||
import { AuthMailSubscriber } from './Subscribers/AuthMail.subscriber';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import {
|
||||
SendResetPasswordMailQueue,
|
||||
SendSignupVerificationMailQueue,
|
||||
} from './Auth.constants';
|
||||
import { SendResetPasswordMailProcessor } from './processors/SendResetPasswordMail.processor';
|
||||
import { SendSignupVerificationMailProcessor } from './processors/SendSignupVerificationMail.processor';
|
||||
import { MailModule } from '../Mail/Mail.module';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
|
||||
import { GetAuthMetaService } from './queries/GetAuthMeta.service';
|
||||
|
||||
const models = [InjectSystemModel(PasswordReset)];
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
imports: [
|
||||
MailModule,
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret: configService.get('jwt.secret'),
|
||||
signOptions: { expiresIn: '1d', algorithm: 'HS384' },
|
||||
verifyOptions: { algorithms: ['HS384'] },
|
||||
}),
|
||||
}),
|
||||
TenantDBManagerModule,
|
||||
BullModule.registerQueue({ name: SendResetPasswordMailQueue }),
|
||||
BullModule.registerQueue({ name: SendSignupVerificationMailQueue }),
|
||||
|
||||
],
|
||||
exports: [...models],
|
||||
providers: [
|
||||
...models,
|
||||
LocalStrategy,
|
||||
JwtStrategy,
|
||||
AuthenticationApplication,
|
||||
AuthSendResetPasswordService,
|
||||
AuthResetPasswordService,
|
||||
AuthSignupConfirmResendService,
|
||||
AuthSignupConfirmService,
|
||||
AuthSignupService,
|
||||
AuthSigninService,
|
||||
AuthenticationMailMesssages,
|
||||
SendResetPasswordMailProcessor,
|
||||
SendSignupVerificationMailProcessor,
|
||||
GetAuthMetaService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
AuthMailSubscriber,
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
10
packages/server/src/modules/Auth/Auth.utils.ts
Normal file
10
packages/server/src/modules/Auth/Auth.utils.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
export const hashPassword = (password: string): Promise<string> =>
|
||||
new Promise((resolve) => {
|
||||
bcrypt.genSalt(10, (error, salt) => {
|
||||
bcrypt.hash(password, salt, (err, hash: string) => {
|
||||
resolve(hash);
|
||||
});
|
||||
});
|
||||
});
|
||||
86
packages/server/src/modules/Auth/AuthApplication.sevice.ts
Normal file
86
packages/server/src/modules/Auth/AuthApplication.sevice.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthSigninService } from './commands/AuthSignin.service';
|
||||
import { AuthSignupService } from './commands/AuthSignup.service';
|
||||
import { AuthSignupConfirmService } from './commands/AuthSignupConfirm.service';
|
||||
import { AuthSignupConfirmResendService } from './commands/AuthSignupConfirmResend.service';
|
||||
import { AuthSigninDto } from './dtos/AuthSignin.dto';
|
||||
import { AuthSignupDto } from './dtos/AuthSignup.dto';
|
||||
import { AuthSendResetPasswordService } from './commands/AuthSendResetPassword.service';
|
||||
import { AuthResetPasswordService } from './commands/AuthResetPassword.service';
|
||||
import { GetAuthMetaService } from './queries/GetAuthMeta.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticationApplication {
|
||||
constructor(
|
||||
private readonly authSigninService: AuthSigninService,
|
||||
private readonly authSignupService: AuthSignupService,
|
||||
private readonly authSignupConfirmService: AuthSignupConfirmService,
|
||||
private readonly authSignUpConfirmResendService: AuthSignupConfirmResendService,
|
||||
private readonly authResetPasswordService: AuthResetPasswordService,
|
||||
private readonly authSendResetPasswordService: AuthSendResetPasswordService,
|
||||
private readonly authGetMeta: GetAuthMetaService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Signin and generates JWT token.
|
||||
* @param {string} email - Email address.
|
||||
* @param {string} password - Password.
|
||||
*/
|
||||
public async signIn(email: string, password: string) {
|
||||
return this.authSigninService.signin(email, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signup a new user.
|
||||
* @param {IRegisterDTO} signupDTO
|
||||
*/
|
||||
public async signUp(signupDto: AuthSignupDto) {
|
||||
return this.authSignupService.signUp(signupDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifying the provided user's email after signin-up.
|
||||
* @param {string} email - User email.
|
||||
* @param {string} token - Verification token.
|
||||
* @returns {Promise<SystemUser>}
|
||||
*/
|
||||
public async signUpConfirm(email: string, token: string) {
|
||||
return this.authSignupConfirmService.signupConfirm(email, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-sends the confirmation email of the given system user.
|
||||
* @param {number} userId - System user id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async signUpConfirmResend(userId: number) {
|
||||
return this.authSignUpConfirmResendService.signUpConfirmResend(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and retrieve password reset token for the given user email.
|
||||
* @param {string} email
|
||||
* @return {<Promise<IPasswordReset>}
|
||||
*/
|
||||
public async sendResetPassword(email: string) {
|
||||
return this.authSendResetPasswordService.sendResetPassword(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a user password from given token.
|
||||
* @param {string} token - Password reset token.
|
||||
* @param {string} password - New Password.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async resetPassword(token: string, password: string) {
|
||||
return this.authResetPasswordService.resetPassword(token, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the authentication meta for SPA.
|
||||
* @returns {Promise<IAuthGetMetaPOJO>}
|
||||
*/
|
||||
public async getAuthMeta() {
|
||||
return this.authGetMeta.getAuthMeta();
|
||||
}
|
||||
}
|
||||
82
packages/server/src/modules/Auth/AuthMailMessages.esrvice.ts
Normal file
82
packages/server/src/modules/Auth/AuthMailMessages.esrvice.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as path from 'path';
|
||||
import { SystemUser } from '../System/models/SystemUser';
|
||||
import { ModelObject } from 'objection';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Mail } from '../Mail/Mail';
|
||||
import { MailTransporter } from '../Mail/MailTransporter.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticationMailMesssages {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly mailTransporter: MailTransporter,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sends reset password message.
|
||||
* @param {ISystemUser} user - The system user.
|
||||
* @param {string} token - Reset password token.
|
||||
* @returns {Mail}
|
||||
*/
|
||||
resetPasswordMessage(user: ModelObject<SystemUser>, token: string) {
|
||||
const baseURL = this.configService.get('baseURL');
|
||||
|
||||
return new Mail()
|
||||
.setSubject('Bigcapital - Password Reset')
|
||||
.setView('mail/ResetPassword.html')
|
||||
.setTo(user.email)
|
||||
.setAttachments([
|
||||
{
|
||||
filename: 'bigcapital.png',
|
||||
path: path.join(global.__static_dirname, `/images/bigcapital.png`),
|
||||
cid: 'bigcapital_logo',
|
||||
},
|
||||
])
|
||||
.setData({
|
||||
resetPasswordUrl: `${baseURL}/auth/reset_password/${token}`,
|
||||
first_name: user.firstName,
|
||||
last_name: user.lastName,
|
||||
});
|
||||
}
|
||||
|
||||
sendResetPasswordMail(user: ModelObject<SystemUser>, token: string) {
|
||||
const mail = this.resetPasswordMessage(user, token);
|
||||
|
||||
return this.mailTransporter.send(mail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends signup verification mail.
|
||||
* @param {string} email - Email address
|
||||
* @param {string} fullName - User name.
|
||||
* @param {string} token - Verification token.
|
||||
* @returns {Mail}
|
||||
*/
|
||||
signupVerificationMail(email: string, fullName: string, token: string) {
|
||||
const baseURL = this.configService.get('baseURL');
|
||||
const verifyUrl = `${baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
|
||||
|
||||
return new Mail()
|
||||
.setSubject('Bigcapital - Verify your email')
|
||||
.setView('mail/SignupVerifyEmail.html')
|
||||
.setTo(email)
|
||||
.setAttachments([
|
||||
{
|
||||
filename: 'bigcapital.png',
|
||||
path: path.join(global.__static_dirname, `/images/bigcapital.png`),
|
||||
cid: 'bigcapital_logo',
|
||||
},
|
||||
])
|
||||
.setData({ verifyUrl, fullName });
|
||||
}
|
||||
|
||||
sendSignupVerificationMail(email: string, fullName: string, token: string) {
|
||||
const mail = this.signupVerificationMail(
|
||||
email,
|
||||
fullName,
|
||||
token,
|
||||
);
|
||||
return this.mailTransporter.send(mail);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||
import { PasswordReset } from '../models/PasswordReset';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { ERRORS } from '../Auth.constants';
|
||||
import { hashPassword } from '../Auth.utils';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { IAuthResetedPasswordEventPayload } from '../Auth.interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class AuthResetPasswordService {
|
||||
/**
|
||||
* @param {ConfigService} configService - Config service.
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter.
|
||||
* @param {typeof SystemUser} systemUserModel
|
||||
* @param {typeof PasswordReset} passwordResetModel - Reset password model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
|
||||
@Inject(SystemUser.name)
|
||||
private readonly systemUserModel: typeof SystemUser,
|
||||
|
||||
@Inject(PasswordReset.name)
|
||||
private readonly passwordResetModel: typeof PasswordReset,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Resets a user password from given token.
|
||||
* @param {string} token - Password reset token.
|
||||
* @param {string} password - New Password.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async resetPassword(token: string, password: string): Promise<void> {
|
||||
// Finds the password reset token.
|
||||
const tokenModel = await this.passwordResetModel
|
||||
.query()
|
||||
.findOne('token', token);
|
||||
|
||||
// In case the password reset token not found throw token invalid error..
|
||||
if (!tokenModel) {
|
||||
throw new ServiceError(ERRORS.TOKEN_INVALID);
|
||||
}
|
||||
const resetPasswordSeconds = this.configService.get('resetPasswordSeconds');
|
||||
|
||||
// Different between tokne creation datetime and current time.
|
||||
if (moment().diff(tokenModel.createdAt, 'seconds') > resetPasswordSeconds) {
|
||||
// Deletes the expired token by expired token email.
|
||||
await this.deletePasswordResetToken(tokenModel.email);
|
||||
throw new ServiceError(ERRORS.TOKEN_EXPIRED);
|
||||
}
|
||||
const user = await this.systemUserModel
|
||||
.query()
|
||||
.findOne({ email: tokenModel.email });
|
||||
|
||||
if (!user) {
|
||||
throw new ServiceError(ERRORS.USER_NOT_FOUND);
|
||||
}
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
await this.systemUserModel
|
||||
.query()
|
||||
.findById(user.id)
|
||||
.update({ password: hashedPassword });
|
||||
|
||||
// Deletes the used token.
|
||||
await this.deletePasswordResetToken(tokenModel.email);
|
||||
|
||||
// Triggers `onResetPassword` event.
|
||||
await this.eventEmitter.emitAsync(events.auth.resetPassword, {
|
||||
user,
|
||||
token,
|
||||
password,
|
||||
} as IAuthResetedPasswordEventPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the password reset token by the given email.
|
||||
* @param {string} email
|
||||
* @returns {Promise}
|
||||
*/
|
||||
private async deletePasswordResetToken(email: string) {
|
||||
return PasswordReset.query().where('email', email).delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as uniqid from 'uniqid';
|
||||
import {
|
||||
IAuthSendedResetPassword,
|
||||
IAuthSendingResetPassword,
|
||||
} from '../Auth.interfaces';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { PasswordReset } from '../models/PasswordReset';
|
||||
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AuthSendResetPasswordService {
|
||||
/**
|
||||
* @param {EventEmitter2} eventPublisher - Event emitter.
|
||||
* @param {typeof PasswordReset} resetPasswordModel - Password reset model.
|
||||
* @param {typeof SystemUser} systemUserModel - System user model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(PasswordReset.name)
|
||||
private readonly resetPasswordModel: typeof PasswordReset,
|
||||
|
||||
@Inject(SystemUser.name)
|
||||
private readonly systemUserModel: typeof SystemUser,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sends the given email reset password email.
|
||||
* @param {string} email - Email address.
|
||||
*/
|
||||
async sendResetPassword(email: string): Promise<void> {
|
||||
const user = await this.systemUserModel
|
||||
.query()
|
||||
.findOne({ email })
|
||||
.throwIfNotFound();
|
||||
|
||||
const token: string = uniqid();
|
||||
|
||||
// Triggers sending reset password event.
|
||||
await this.eventPublisher.emitAsync(events.auth.sendingResetPassword, {
|
||||
user,
|
||||
token,
|
||||
} as IAuthSendingResetPassword);
|
||||
|
||||
// Delete all stored tokens of reset password that associate to the give email.
|
||||
this.deletePasswordResetToken(email);
|
||||
|
||||
// Creates a new password reset row with unique token.
|
||||
const passwordReset = await this.resetPasswordModel.query().insert({
|
||||
email,
|
||||
token,
|
||||
});
|
||||
// Triggers sent reset password event.
|
||||
await this.eventPublisher.emitAsync(events.auth.sendResetPassword, {
|
||||
user,
|
||||
token,
|
||||
} as IAuthSendedResetPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the password reset token by the given email.
|
||||
* @param {string} email
|
||||
* @returns {Promise}
|
||||
*/
|
||||
private async deletePasswordResetToken(email: string) {
|
||||
return this.resetPasswordModel.query().where('email', email).delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||
import { ModelObject } from 'objection';
|
||||
import { JwtPayload } from '../Auth.interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class AuthSigninService {
|
||||
constructor(
|
||||
@Inject(SystemUser.name)
|
||||
private readonly systemUserModel: typeof SystemUser,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly clsService: ClsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the given email and password.
|
||||
* @param {string} email - Signin email address.
|
||||
* @param {string} password - Signin password.
|
||||
* @returns {Promise<ModelObject<SystemUser>>}
|
||||
*/
|
||||
async signin(
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<ModelObject<SystemUser>> {
|
||||
let user: SystemUser;
|
||||
|
||||
try {
|
||||
user = await this.systemUserModel
|
||||
.query()
|
||||
.findOne({ email })
|
||||
.throwIfNotFound();
|
||||
} catch (err) {
|
||||
throw new UnauthorizedException(
|
||||
`There isn't any user with email: ${email}`,
|
||||
);
|
||||
}
|
||||
if (!(await user.checkPassword(password))) {
|
||||
throw new UnauthorizedException(
|
||||
`Wrong password for user with email: ${email}`,
|
||||
);
|
||||
}
|
||||
if (!user.verified) {
|
||||
throw new UnauthorizedException(
|
||||
`The user is not verified yet, check out your mail inbox.`
|
||||
);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the given jwt payload.
|
||||
* @param {JwtPayload} payload
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async verifyPayload(payload: JwtPayload): Promise<any> {
|
||||
let user: SystemUser;
|
||||
|
||||
try {
|
||||
user = await this.systemUserModel
|
||||
.query()
|
||||
.findOne({ email: payload.sub })
|
||||
.throwIfNotFound();
|
||||
|
||||
this.clsService.set('tenantId', user.tenantId);
|
||||
this.clsService.set('userId', user.id);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException(
|
||||
`There isn't any user with email: ${payload.sub}`,
|
||||
);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {SystemUser} user
|
||||
* @returns {string}
|
||||
*/
|
||||
signToken(user: SystemUser): string {
|
||||
const payload = {
|
||||
sub: user.email,
|
||||
};
|
||||
return this.jwtService.sign(payload);
|
||||
}
|
||||
}
|
||||
130
packages/server/src/modules/Auth/commands/AuthSignup.service.ts
Normal file
130
packages/server/src/modules/Auth/commands/AuthSignup.service.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as moment from 'moment';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||
import { TenantsManagerService } from '@/modules/TenantDBManager/TenantsManager';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { isEmpty } from 'class-validator';
|
||||
import { AuthSignupDto } from '../dtos/AuthSignup.dto';
|
||||
import {
|
||||
IAuthSignedUpEventPayload,
|
||||
IAuthSigningUpEventPayload,
|
||||
} from '../Auth.interfaces';
|
||||
import { defaultTo } from 'ramda';
|
||||
import { ERRORS } from '../Auth.constants';
|
||||
import { hashPassword } from '../Auth.utils';
|
||||
|
||||
@Injectable()
|
||||
export class AuthSignupService {
|
||||
/**
|
||||
* @param {ConfigService} configService - Config service
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter
|
||||
* @param {TenantsManagerService} tenantsManager - Tenants manager
|
||||
* @param {typeof SystemUser} systemUserModel - System user model
|
||||
*/
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly tenantsManager: TenantsManagerService,
|
||||
|
||||
@Inject(SystemUser.name)
|
||||
private readonly systemUserModel: typeof SystemUser,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registers a new tenant with user from user input.
|
||||
* @param {AuthSignupDto} signupDTO
|
||||
*/
|
||||
public async signUp(signupDTO: AuthSignupDto) {
|
||||
// Validates the signup disable restrictions.
|
||||
await this.validateSignupRestrictions(signupDTO.email);
|
||||
|
||||
// Validates the given email uniqiness.
|
||||
await this.validateEmailUniqiness(signupDTO.email);
|
||||
|
||||
const hashedPassword = await hashPassword(signupDTO.password);
|
||||
const signupConfirmation = this.configService.get('signupConfirmation');
|
||||
|
||||
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
|
||||
const verifiedEnabed = defaultTo(signupConfirmation.enabled, false);
|
||||
const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
|
||||
const verified = !verifiedEnabed;
|
||||
|
||||
const inviteAcceptedAt = moment().format('YYYY-MM-DD');
|
||||
|
||||
// Triggers signin up event.
|
||||
await this.eventEmitter.emitAsync(events.auth.signingUp, {
|
||||
signupDTO,
|
||||
} as IAuthSigningUpEventPayload);
|
||||
|
||||
const tenant = await this.tenantsManager.createTenant();
|
||||
const user = await this.systemUserModel.query().insert({
|
||||
...signupDTO,
|
||||
verifyToken,
|
||||
verified,
|
||||
active: true,
|
||||
password: hashedPassword,
|
||||
tenantId: tenant.id,
|
||||
inviteAcceptedAt,
|
||||
});
|
||||
// Triggers signed up event.
|
||||
await this.eventEmitter.emitAsync(events.auth.signUp, {
|
||||
signupDTO,
|
||||
tenant,
|
||||
user,
|
||||
} as IAuthSignedUpEventPayload);
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
tenantId: user.tenantId,
|
||||
organizationId: tenant.organizationId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates email uniqiness on the storage.
|
||||
* @param {string} email - Email address
|
||||
*/
|
||||
private async validateEmailUniqiness(email: string) {
|
||||
const isEmailExists = await this.systemUserModel.query().findOne({ email });
|
||||
|
||||
if (isEmailExists) {
|
||||
throw new ServiceError(ERRORS.EMAIL_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sign-up disable restrictions.
|
||||
* @param {string} email - Signup email address
|
||||
*/
|
||||
private async validateSignupRestrictions(email: string) {
|
||||
const signupRestrictions = this.configService.get('signupRestrictions');
|
||||
|
||||
// Can't continue if the signup is not disabled.
|
||||
if (!signupRestrictions.disabled) return;
|
||||
|
||||
// Validate the allowed email addresses and domains.
|
||||
if (
|
||||
!isEmpty(signupRestrictions.allowedEmails) ||
|
||||
!isEmpty(signupRestrictions.allowedDomains)
|
||||
) {
|
||||
const emailDomain = email.split('@').pop();
|
||||
const isAllowedEmail =
|
||||
signupRestrictions.allowedEmails.indexOf(email) !== -1;
|
||||
|
||||
const isAllowedDomain = signupRestrictions.allowedDomains.some(
|
||||
(domain) => emailDomain === domain,
|
||||
);
|
||||
|
||||
if (!isAllowedEmail && !isAllowedDomain) {
|
||||
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED_NOT_ALLOWED);
|
||||
}
|
||||
// Throw error if the signup is disabled with no exceptions.
|
||||
} else {
|
||||
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ERRORS } from '../Auth.constants';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
IAuthSignUpVerifiedEventPayload,
|
||||
IAuthSignUpVerifingEventPayload,
|
||||
} from '../Auth.interfaces';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class AuthSignupConfirmService {
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(SystemUser.name)
|
||||
private readonly systemUserModel: typeof SystemUser,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Verifies the provided user's email after signing-up.
|
||||
* @throws {ServiceErrors}
|
||||
* @param {IRegisterDTO} signupDTO
|
||||
* @returns {Promise<ISystemUser>}
|
||||
*/
|
||||
public async signupConfirm(
|
||||
email: string,
|
||||
verifyToken: string,
|
||||
): Promise<SystemUser> {
|
||||
const foundUser = await this.systemUserModel
|
||||
.query()
|
||||
.findOne({ email, verifyToken });
|
||||
|
||||
if (!foundUser) {
|
||||
throw new ServiceError(ERRORS.SIGNUP_CONFIRM_TOKEN_INVALID);
|
||||
}
|
||||
const userId = foundUser.id;
|
||||
|
||||
// Triggers `signUpConfirming` event.
|
||||
await this.eventPublisher.emitAsync(events.auth.signUpConfirming, {
|
||||
email,
|
||||
verifyToken,
|
||||
userId,
|
||||
} as IAuthSignUpVerifingEventPayload);
|
||||
|
||||
const updatedUser = await this.systemUserModel
|
||||
.query()
|
||||
.patchAndFetchById(foundUser.id, {
|
||||
verified: true,
|
||||
verifyToken: '',
|
||||
});
|
||||
// Triggers `signUpConfirmed` event.
|
||||
await this.eventPublisher.emitAsync(events.auth.signUpConfirmed, {
|
||||
email,
|
||||
verifyToken,
|
||||
userId,
|
||||
} as IAuthSignUpVerifiedEventPayload);
|
||||
|
||||
return updatedUser as SystemUser;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class AuthSignupConfirmResendService {
|
||||
signUpConfirmResend(userId: number) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
11
packages/server/src/modules/Auth/dtos/AuthSignin.dto.ts
Normal file
11
packages/server/src/modules/Auth/dtos/AuthSignin.dto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class AuthSigninDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
email: string;
|
||||
}
|
||||
20
packages/server/src/modules/Auth/dtos/AuthSignup.dto.ts
Normal file
20
packages/server/src/modules/Auth/dtos/AuthSignup.dto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class AuthSignupDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
firstName: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
lastName: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
5
packages/server/src/modules/Auth/guards/Local.guard.ts
Normal file
5
packages/server/src/modules/Auth/guards/Local.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable, ExecutionContext } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthGuard extends AuthGuard('local') {}
|
||||
28
packages/server/src/modules/Auth/guards/jwt.guard.ts
Normal file
28
packages/server/src/modules/Auth/guards/jwt.guard.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ExecutionContext, Injectable, SetMetadata } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { IS_PUBLIC_ROUTE } from '../Auth.constants';
|
||||
|
||||
export const PublicRoute = () => SetMetadata(IS_PUBLIC_ROUTE, true);
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private readonly cls: ClsService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(
|
||||
IS_PUBLIC_ROUTE,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
||||
21
packages/server/src/modules/Auth/models/PasswordReset.ts
Normal file
21
packages/server/src/modules/Auth/models/PasswordReset.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { SystemModel } from '@/modules/System/models/SystemModel';
|
||||
|
||||
export class PasswordReset extends SystemModel {
|
||||
readonly email: string;
|
||||
readonly token: string;
|
||||
readonly createdAt: Date;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'password_resets';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import {
|
||||
SendResetPasswordMailJob,
|
||||
SendResetPasswordMailQueue,
|
||||
} from '../Auth.constants';
|
||||
import { Process } from '@nestjs/bull';
|
||||
import { Job } from 'bullmq';
|
||||
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
|
||||
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
||||
import { ModelObject } from 'objection';
|
||||
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||
|
||||
@Processor({
|
||||
name: SendResetPasswordMailQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class SendResetPasswordMailProcessor extends WorkerHost {
|
||||
constructor(
|
||||
private readonly authMailMesssages: AuthenticationMailMesssages,
|
||||
private readonly mailTransporter: MailTransporter,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(SendResetPasswordMailJob)
|
||||
async process(job: Job<SendResetPasswordMailJobPayload>) {
|
||||
try {
|
||||
await this.authMailMesssages.sendResetPasswordMail(
|
||||
job.data.user,
|
||||
job.data.token,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('Error occured during send reset password mail', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SendResetPasswordMailJobPayload {
|
||||
user: ModelObject<SystemUser>;
|
||||
token: string;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { Process } from '@nestjs/bull';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import {
|
||||
SendSignupVerificationMailJob,
|
||||
SendSignupVerificationMailQueue,
|
||||
} from '../Auth.constants';
|
||||
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
||||
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
|
||||
|
||||
@Processor({
|
||||
name: SendSignupVerificationMailQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class SendSignupVerificationMailProcessor extends WorkerHost {
|
||||
constructor(
|
||||
private readonly authMailMesssages: AuthenticationMailMesssages,
|
||||
private readonly mailTransporter: MailTransporter,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Process(SendSignupVerificationMailJob)
|
||||
async process(job: Job<SendSignupVerificationMailJobPayload>) {
|
||||
try {
|
||||
await this.authMailMesssages.sendSignupVerificationMail(
|
||||
job.data.email,
|
||||
job.data.fullName,
|
||||
job.data.token,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('Error occured during send signup verification mail', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SendSignupVerificationMailJobPayload {
|
||||
email: string;
|
||||
fullName: string;
|
||||
token: string;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IAuthGetMetaPOJO } from '../Auth.interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class GetAuthMetaService {
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
|
||||
}
|
||||
/**
|
||||
* Retrieves the authentication meta for SPA.
|
||||
* @returns {Promise<IAuthGetMetaPOJO>}
|
||||
*/
|
||||
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
|
||||
return {
|
||||
signupDisabled: this.configService.get('signupRestrictions.disabled'),
|
||||
};
|
||||
}
|
||||
}
|
||||
24
packages/server/src/modules/Auth/strategies/Jwt.strategy.ts
Normal file
24
packages/server/src/modules/Auth/strategies/Jwt.strategy.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { AuthSigninService } from '../commands/AuthSignin.service';
|
||||
import { JwtPayload } from '../Auth.interfaces';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
constructor(
|
||||
private readonly authSigninService: AuthSigninService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get('jwt.secret'),
|
||||
});
|
||||
}
|
||||
|
||||
validate(payload: JwtPayload) {
|
||||
return this.authSigninService.verifyPayload(payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-local';
|
||||
import { AuthSigninService } from '../commands/AuthSignin.service';
|
||||
import { ModelObject } from 'objection';
|
||||
import { SystemUser } from '../../System/models/SystemUser';
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
|
||||
constructor(private readonly authSigninService: AuthSigninService) {
|
||||
super({
|
||||
usernameField: 'email',
|
||||
passReqToCallback: false,
|
||||
session: false,
|
||||
});
|
||||
}
|
||||
|
||||
validate(email: string, password: string): Promise<ModelObject<SystemUser>> {
|
||||
return this.authSigninService.signin(email, password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { events } from '@/common/events/events';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import {
|
||||
IAuthSendedResetPassword,
|
||||
IAuthSignedUpEventPayload,
|
||||
} from '../Auth.interfaces';
|
||||
import { Queue } from 'bullmq';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { SendResetPasswordMailJobPayload } from '../processors/SendResetPasswordMail.processor';
|
||||
import {
|
||||
SendResetPasswordMailJob,
|
||||
SendResetPasswordMailQueue,
|
||||
SendSignupVerificationMailJob,
|
||||
SendSignupVerificationMailQueue,
|
||||
} from '../Auth.constants';
|
||||
import { SendSignupVerificationMailJobPayload } from '../processors/SendSignupVerificationMail.processor';
|
||||
|
||||
@Injectable()
|
||||
export class AuthMailSubscriber {
|
||||
constructor(
|
||||
@InjectQueue(SendResetPasswordMailQueue)
|
||||
private readonly sendResetPasswordMailQueue: Queue,
|
||||
|
||||
@InjectQueue(SendSignupVerificationMailQueue)
|
||||
private readonly sendSignupVerificationMailQueue: Queue,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param {IAuthSignedUpEventPayload} payload
|
||||
*/
|
||||
@OnEvent(events.auth.signUp)
|
||||
async handleSignupSendVerificationMail(payload: IAuthSignedUpEventPayload) {
|
||||
try {
|
||||
const job = await this.sendSignupVerificationMailQueue.add(
|
||||
SendSignupVerificationMailJob,
|
||||
{
|
||||
email: payload.user.email,
|
||||
fullName: payload.user.firstName,
|
||||
token: payload.user.verifyToken,
|
||||
} as SendSignupVerificationMailJobPayload,
|
||||
{
|
||||
delay: 0,
|
||||
},
|
||||
);
|
||||
console.log(job);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IAuthSendedResetPassword} payload
|
||||
*/
|
||||
@OnEvent(events.auth.sendResetPassword)
|
||||
async handleSendResetPasswordMail(payload: IAuthSendedResetPassword) {
|
||||
await this.sendResetPasswordMailQueue.add(
|
||||
SendResetPasswordMailJob,
|
||||
{
|
||||
user: payload.user,
|
||||
token: payload.token,
|
||||
} as SendResetPasswordMailJobPayload,
|
||||
{
|
||||
delay: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||
import { AutoIncrementOrdersService } from './AutoIncrementOrders.service';
|
||||
|
||||
@Module({
|
||||
imports: [TenancyDatabaseModule],
|
||||
controllers: [],
|
||||
providers: [AutoIncrementOrdersService],
|
||||
exports: [AutoIncrementOrdersService],
|
||||
})
|
||||
export class AutoIncrementOrdersModule {}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SettingsStore } from '../Settings/SettingsStore';
|
||||
import { SETTINGS_PROVIDER } from '../Settings/Settings.types';
|
||||
import { transactionIncrement } from '@/utils/transaction-increment';
|
||||
|
||||
/**
|
||||
* Auto increment orders service.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AutoIncrementOrdersService {
|
||||
constructor(
|
||||
@Inject(SETTINGS_PROVIDER)
|
||||
private readonly settingsStore: () => SettingsStore,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Check if the auto increment is enabled for the given settings group.
|
||||
* @param {string} settingsGroup - Settings group.
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
public autoIncrementEnabled = async (
|
||||
settingsGroup: string,
|
||||
): Promise<boolean> => {
|
||||
const settingsStore = await this.settingsStore();
|
||||
const group = settingsGroup;
|
||||
|
||||
// Settings service transaction number and prefix.
|
||||
return settingsStore.get({ group, key: 'auto_increment' }, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the next service transaction number.
|
||||
* @param {string} settingsGroup
|
||||
* @param {Function} getMaxTransactionNo
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
async getNextTransactionNumber(group: string): Promise<string> {
|
||||
const settingsStore = await this.settingsStore();
|
||||
|
||||
// Settings service transaction number and prefix.
|
||||
const autoIncrement = await this.autoIncrementEnabled(group);
|
||||
|
||||
const settingNo = settingsStore.get({ group, key: 'next_number' }, '');
|
||||
const settingPrefix = settingsStore.get(
|
||||
{ group, key: 'number_prefix' },
|
||||
'',
|
||||
);
|
||||
return autoIncrement ? `${settingPrefix}${settingNo}` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment setting next number.
|
||||
* @param {string} orderGroup - Order group.
|
||||
* @param {string} orderNumber -Order number.
|
||||
*/
|
||||
async incrementSettingsNextNumber(group: string) {
|
||||
const settingsStore = await this.settingsStore();
|
||||
|
||||
const settingNo = settingsStore.get({ group, key: 'next_number' });
|
||||
const autoIncrement = settingsStore.get({ group, key: 'auto_increment' });
|
||||
|
||||
// // Can't continue if the auto-increment of the service was disabled.
|
||||
if (!autoIncrement) {
|
||||
return;
|
||||
}
|
||||
settingsStore.set(
|
||||
{ group, key: 'next_number' },
|
||||
transactionIncrement(settingNo),
|
||||
);
|
||||
await settingsStore.save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
} from '@nestjs/common';
|
||||
import { BankRulesApplication } from './BankRulesApplication';
|
||||
import { BankRule } from './models/BankRule';
|
||||
import { CreateBankRuleDto } from './dtos/BankRule.dto';
|
||||
import { EditBankRuleDto } from './dtos/BankRule.dto';
|
||||
|
||||
@Controller('banking/rules')
|
||||
@ApiTags('bank-rules')
|
||||
export class BankRulesController {
|
||||
constructor(private readonly bankRulesApplication: BankRulesApplication) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new bank rule.' })
|
||||
async createBankRule(
|
||||
@Body() createRuleDTO: CreateBankRuleDto,
|
||||
): Promise<BankRule> {
|
||||
return this.bankRulesApplication.createBankRule(createRuleDTO);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Edit the given bank rule.' })
|
||||
async editBankRule(
|
||||
@Param('id') ruleId: number,
|
||||
@Body() editRuleDTO: EditBankRuleDto,
|
||||
): Promise<void> {
|
||||
return this.bankRulesApplication.editBankRule(ruleId, editRuleDTO);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete the given bank rule.' })
|
||||
async deleteBankRule(@Param('id') ruleId: number): Promise<void> {
|
||||
return this.bankRulesApplication.deleteBankRule(ruleId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Retrieves the bank rule details.' })
|
||||
async getBankRule(@Param('id') ruleId: number): Promise<any> {
|
||||
return this.bankRulesApplication.getBankRule(ruleId);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Retrieves the bank rules.' })
|
||||
async getBankRules(): Promise<any> {
|
||||
return this.bankRulesApplication.getBankRules();
|
||||
}
|
||||
}
|
||||
36
packages/server/src/modules/BankRules/BankRules.module.ts
Normal file
36
packages/server/src/modules/BankRules/BankRules.module.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { CreateBankRuleService } from './commands/CreateBankRule.service';
|
||||
import { EditBankRuleService } from './commands/EditBankRule.service';
|
||||
import { DeleteBankRuleService } from './commands/DeleteBankRule.service';
|
||||
import { GetBankRulesService } from './queries/GetBankRules.service';
|
||||
import { GetBankRuleService } from './queries/GetBankRule.service';
|
||||
import { BankRulesApplication } from './BankRulesApplication';
|
||||
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
|
||||
import { BankRuleCondition } from './models/BankRuleCondition';
|
||||
import { BankRule } from './models/BankRule';
|
||||
import { BankRulesController } from './BankRules.controller';
|
||||
import { UnlinkBankRuleOnDeleteBankRuleSubscriber } from './events/UnlinkBankRuleOnDeleteBankRule';
|
||||
import { DeleteBankRulesService } from './commands/DeleteBankRules.service';
|
||||
import { BankingTransactionsRegonizeModule } from '../BankingTranasctionsRegonize/BankingTransactionsRegonize.module';
|
||||
|
||||
const models = [
|
||||
RegisterTenancyModel(BankRule),
|
||||
RegisterTenancyModel(BankRuleCondition),
|
||||
];
|
||||
|
||||
@Module({
|
||||
controllers: [BankRulesController],
|
||||
imports: [forwardRef(() => BankingTransactionsRegonizeModule), ...models],
|
||||
providers: [
|
||||
CreateBankRuleService,
|
||||
EditBankRuleService,
|
||||
DeleteBankRuleService,
|
||||
DeleteBankRulesService,
|
||||
GetBankRuleService,
|
||||
GetBankRulesService,
|
||||
BankRulesApplication,
|
||||
UnlinkBankRuleOnDeleteBankRuleSubscriber,
|
||||
],
|
||||
exports: [...models, DeleteBankRuleService, DeleteBankRulesService],
|
||||
})
|
||||
export class BankRulesModule {}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateBankRuleService } from './commands/CreateBankRule.service';
|
||||
import { DeleteBankRuleService } from './commands/DeleteBankRule.service';
|
||||
import { EditBankRuleService } from './commands/EditBankRule.service';
|
||||
import { GetBankRuleService } from './queries/GetBankRule.service';
|
||||
import { GetBankRulesService } from './queries/GetBankRules.service';
|
||||
import { BankRule } from './models/BankRule';
|
||||
import { CreateBankRuleDto, EditBankRuleDto } from './dtos/BankRule.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BankRulesApplication {
|
||||
constructor(
|
||||
private readonly createBankRuleService: CreateBankRuleService,
|
||||
private readonly editBankRuleService: EditBankRuleService,
|
||||
private readonly deleteBankRuleService: DeleteBankRuleService,
|
||||
private readonly getBankRuleService: GetBankRuleService,
|
||||
private readonly getBankRulesService: GetBankRulesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates new bank rule.
|
||||
* @param {ICreateBankRuleDTO} createRuleDTO - Bank rule data.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public createBankRule(
|
||||
createRuleDTO: CreateBankRuleDto,
|
||||
): Promise<BankRule> {
|
||||
return this.createBankRuleService.createBankRule(createRuleDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given bank rule.
|
||||
* @param {number} ruleId - Bank rule identifier.
|
||||
* @param {EditBankRuleDto} editRuleDTO - Bank rule data.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public editBankRule(
|
||||
ruleId: number,
|
||||
editRuleDTO: EditBankRuleDto,
|
||||
): Promise<void> {
|
||||
return this.editBankRuleService.editBankRule(ruleId, editRuleDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given bank rule.
|
||||
* @param {number} ruleId - Bank rule identifier.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteBankRule(ruleId: number): Promise<void> {
|
||||
return this.deleteBankRuleService.deleteBankRule(ruleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given bank rule.
|
||||
* @param {number} ruleId - Bank rule identifier.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
public getBankRule(ruleId: number): Promise<any> {
|
||||
return this.getBankRuleService.getBankRule(ruleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the bank rules of the given account.
|
||||
* @param {number} accountId - Bank account identifier.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
public getBankRules(): Promise<any> {
|
||||
return this.getBankRulesService.getBankRules();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ModelObject } from 'objection';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
IBankRuleEventCreatedPayload,
|
||||
IBankRuleEventCreatingPayload,
|
||||
} from '../types';
|
||||
import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { BankRule } from '../models/BankRule';
|
||||
import { CreateBankRuleDto } from '../dtos/BankRule.dto';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class CreateBankRuleService {
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(BankRule.name)
|
||||
private readonly bankRuleModel: TenantModelProxy<typeof BankRule>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Transforms the DTO to model.
|
||||
* @param {ICreateBankRuleDTO} createDTO
|
||||
*/
|
||||
private transformDTO(createDTO: CreateBankRuleDto): ModelObject<BankRule> {
|
||||
return {
|
||||
...createDTO,
|
||||
} as ModelObject<BankRule>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new bank rule.
|
||||
* @param {ICreateBankRuleDTO} createRuleDTO
|
||||
* @returns {Promise<BankRule>}
|
||||
*/
|
||||
public async createBankRule(
|
||||
createRuleDTO: CreateBankRuleDto,
|
||||
): Promise<BankRule> {
|
||||
const transformDTO = this.transformDTO(createRuleDTO);
|
||||
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onBankRuleCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.bankRules.onCreating, {
|
||||
createRuleDTO,
|
||||
trx,
|
||||
} as IBankRuleEventCreatingPayload);
|
||||
|
||||
const bankRule = await this.bankRuleModel()
|
||||
.query(trx)
|
||||
.upsertGraphAndFetch({
|
||||
...transformDTO,
|
||||
});
|
||||
// Triggers `onBankRuleCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.bankRules.onCreated, {
|
||||
createRuleDTO,
|
||||
bankRule,
|
||||
trx,
|
||||
} as IBankRuleEventCreatedPayload);
|
||||
|
||||
return bankRule;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IBankRuleEventDeletedPayload,
|
||||
IBankRuleEventDeletingPayload,
|
||||
} from '../types';
|
||||
import { BankRule } from '../models/BankRule';
|
||||
import { BankRuleCondition } from '../models/BankRuleCondition';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteBankRuleService {
|
||||
constructor(
|
||||
@Inject(BankRule.name)
|
||||
private bankRuleModel: TenantModelProxy<typeof BankRule>,
|
||||
|
||||
@Inject(BankRuleCondition.name)
|
||||
private bankRuleConditionModel: TenantModelProxy<typeof BankRuleCondition>,
|
||||
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Deletes the given bank rule.
|
||||
* @param {number} ruleId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async deleteBankRule(
|
||||
ruleId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const oldBankRule = await this.bankRuleModel()
|
||||
.query()
|
||||
.findById(ruleId)
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onBankRuleDeleting` event.
|
||||
await this.eventPublisher.emitAsync(events.bankRules.onDeleting, {
|
||||
oldBankRule,
|
||||
ruleId,
|
||||
trx,
|
||||
} as IBankRuleEventDeletingPayload);
|
||||
|
||||
await this.bankRuleConditionModel()
|
||||
.query(trx)
|
||||
.where('ruleId', ruleId)
|
||||
.delete();
|
||||
|
||||
await this.bankRuleModel().query(trx).findById(ruleId).delete();
|
||||
|
||||
// Triggers `onBankRuleDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.bankRules.onDeleted, {
|
||||
ruleId,
|
||||
trx,
|
||||
} as IBankRuleEventDeletedPayload);
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Knex } from 'knex';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { castArray, uniq } from 'lodash';
|
||||
import { DeleteBankRuleService } from './DeleteBankRule.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteBankRulesService {
|
||||
constructor(private readonly deleteBankRuleService: DeleteBankRuleService) {}
|
||||
|
||||
/**
|
||||
* Delete bank rules.
|
||||
* @param {number | Array<number>} bankRuleId - The bank rule id or ids.
|
||||
* @param {Knex.Transaction} trx - The transaction.
|
||||
*/
|
||||
async deleteBankRules(
|
||||
bankRuleId: number | Array<number>,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const bankRulesIds = uniq(castArray(bankRuleId));
|
||||
|
||||
const results = await PromisePool.withConcurrency(1)
|
||||
.for(bankRulesIds)
|
||||
.process(async (bankRuleId: number) => {
|
||||
await this.deleteBankRuleService.deleteBankRule(bankRuleId, trx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IBankRuleEventEditedPayload,
|
||||
IBankRuleEventEditingPayload,
|
||||
IEditBankRuleDTO,
|
||||
} from '../types';
|
||||
import { BankRule } from '../models/BankRule';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { EditBankRuleDto } from '../dtos/BankRule.dto';
|
||||
import { ModelObject } from 'objection';
|
||||
|
||||
@Injectable()
|
||||
export class EditBankRuleService {
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(BankRule.name)
|
||||
private bankRuleModel: TenantModelProxy<typeof BankRule>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param createDTO
|
||||
* @returns
|
||||
*/
|
||||
private transformDTO(createDTO: EditBankRuleDto): ModelObject<BankRule> {
|
||||
return {
|
||||
...createDTO,
|
||||
} as ModelObject<BankRule>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given bank rule.
|
||||
* @param {number} ruleId -
|
||||
* @param {IEditBankRuleDTO} editBankDTO
|
||||
*/
|
||||
public async editBankRule(ruleId: number, editRuleDTO: EditBankRuleDto) {
|
||||
const oldBankRule = await this.bankRuleModel()
|
||||
.query()
|
||||
.findById(ruleId)
|
||||
.withGraphFetched('conditions')
|
||||
.throwIfNotFound();
|
||||
|
||||
const tranformDTO = this.transformDTO(editRuleDTO);
|
||||
|
||||
return this.uow.withTransaction(async (trx) => {
|
||||
// Triggers `onBankRuleEditing` event.
|
||||
await this.eventPublisher.emitAsync(events.bankRules.onEditing, {
|
||||
oldBankRule,
|
||||
ruleId,
|
||||
editRuleDTO,
|
||||
trx,
|
||||
} as IBankRuleEventEditingPayload);
|
||||
|
||||
// Updates the given bank rule.
|
||||
const bankRule = await this.bankRuleModel()
|
||||
.query(trx)
|
||||
.upsertGraphAndFetch({
|
||||
...tranformDTO,
|
||||
id: ruleId,
|
||||
});
|
||||
// Triggers `onBankRuleEdited` event.
|
||||
await this.eventPublisher.emitAsync(events.bankRules.onEdited, {
|
||||
oldBankRule,
|
||||
bankRule,
|
||||
editRuleDTO,
|
||||
trx,
|
||||
} as IBankRuleEventEditedPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
122
packages/server/src/modules/BankRules/dtos/BankRule.dto.ts
Normal file
122
packages/server/src/modules/BankRules/dtos/BankRule.dto.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsString,
|
||||
IsInt,
|
||||
Min,
|
||||
IsOptional,
|
||||
IsIn,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
ArrayMinSize,
|
||||
IsNotEmpty,
|
||||
} from 'class-validator';
|
||||
import { BankRuleComparator } from '../types';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
class BankRuleConditionDto {
|
||||
@IsNotEmpty()
|
||||
@IsIn(['description', 'amount'])
|
||||
field: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsIn([
|
||||
'equals',
|
||||
'equal',
|
||||
'contains',
|
||||
'not_contain',
|
||||
'bigger',
|
||||
'bigger_or_equal',
|
||||
'smaller',
|
||||
'smaller_or_equal',
|
||||
])
|
||||
comparator: BankRuleComparator = 'contains';
|
||||
|
||||
@IsNotEmpty()
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class CommandBankRuleDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
description: 'The name of the bank rule',
|
||||
example: 'Monthly Salary',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ApiProperty({
|
||||
description: 'The order of the bank rule',
|
||||
example: 1,
|
||||
})
|
||||
order: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ApiProperty({
|
||||
description: 'The account ID to apply the rule if',
|
||||
example: 1,
|
||||
})
|
||||
applyIfAccountId?: number;
|
||||
|
||||
@IsIn(['deposit', 'withdrawal'])
|
||||
@ApiProperty({
|
||||
description: 'The transaction type to apply the rule if',
|
||||
example: 'deposit',
|
||||
})
|
||||
applyIfTransactionType: 'deposit' | 'withdrawal';
|
||||
|
||||
@IsString()
|
||||
@IsIn(['and', 'or'])
|
||||
@ApiProperty({
|
||||
description: 'The conditions type to apply the rule if',
|
||||
example: 'and',
|
||||
})
|
||||
conditionsType: 'and' | 'or' = 'and';
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BankRuleConditionDto)
|
||||
@ApiProperty({
|
||||
description: 'The conditions to apply the rule if',
|
||||
example: [{ field: 'description', comparator: 'contains', value: 'Salary' }],
|
||||
})
|
||||
conditions: BankRuleConditionDto[];
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'The category to assign the rule if',
|
||||
example: 'Income:Salary',
|
||||
})
|
||||
assignCategory: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ApiProperty({
|
||||
description: 'The account ID to assign the rule if',
|
||||
example: 1,
|
||||
})
|
||||
assignAccountId: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'The payee to assign the rule if',
|
||||
example: 'Employer Inc.',
|
||||
})
|
||||
assignPayee?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'The memo to assign the rule if',
|
||||
example: 'Monthly Salary',
|
||||
})
|
||||
assignMemo?: string;
|
||||
}
|
||||
|
||||
export class CreateBankRuleDto extends CommandBankRuleDto {}
|
||||
export class EditBankRuleDto extends CommandBankRuleDto {}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IBankRuleEventDeletingPayload } from '../types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { RevertRecognizedTransactionsService } from '@/modules/BankingTranasctionsRegonize/commands/RevertRecognizedTransactions.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
@Injectable()
|
||||
export class UnlinkBankRuleOnDeleteBankRuleSubscriber {
|
||||
constructor(
|
||||
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Unlinks the bank rule out of recognized transactions.
|
||||
* @param {IBankRuleEventDeletingPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.bankRules.onDeleting)
|
||||
public async unlinkBankRuleOutRecognizedTransactionsOnRuleDeleting({
|
||||
oldBankRule,
|
||||
}: IBankRuleEventDeletingPayload) {
|
||||
await this.revertRecognizedTransactionsService.revertRecognizedTransactions(
|
||||
oldBankRule.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
74
packages/server/src/modules/BankRules/models/BankRule.ts
Normal file
74
packages/server/src/modules/BankRules/models/BankRule.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import { Model } from 'objection';
|
||||
import { BankRuleCondition } from './BankRuleCondition';
|
||||
import { BankRuleAssignCategory, BankRuleConditionType } from '../types';
|
||||
|
||||
export class BankRule extends BaseModel {
|
||||
public readonly id!: number;
|
||||
public readonly name!: string;
|
||||
public readonly order!: number;
|
||||
public readonly applyIfAccountId!: number;
|
||||
public readonly applyIfTransactionType!: string;
|
||||
public readonly assignCategory!: BankRuleAssignCategory;
|
||||
public readonly assignAccountId!: number;
|
||||
public readonly assignPayee!: string;
|
||||
public readonly assignMemo!: string;
|
||||
public readonly conditionsType!: BankRuleConditionType;
|
||||
|
||||
public readonly conditions!: BankRuleCondition[];
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'bank_rules';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { BankRuleCondition } = require('./BankRuleCondition');
|
||||
const { Account } = require('../../Accounts/models/Account.model');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Sale invoice associated entries.
|
||||
*/
|
||||
conditions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: BankRuleCondition,
|
||||
join: {
|
||||
from: 'bank_rules.id',
|
||||
to: 'bank_rule_conditions.ruleId',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Bank rule may associated to the assign account.
|
||||
*/
|
||||
assignAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account,
|
||||
join: {
|
||||
from: 'bank_rules.assignAccountId',
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import { BankRuleComparator } from '../types';
|
||||
|
||||
export class BankRuleCondition extends BaseModel {
|
||||
public id!: number;
|
||||
public bankRuleId!: number;
|
||||
public field!: string;
|
||||
public comparator!: BankRuleComparator;
|
||||
public value!: string;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'bank_rule_conditions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user