Files
bigcapital/temp/CashFlow/CashFlow.ts
2025-01-18 22:32:45 +02:00

706 lines
22 KiB
TypeScript

import * as R from 'ramda';
import { defaultTo, map, set, sumBy, isEmpty, mapValues, get } from 'lodash';
import * as mathjs from 'mathjs';
import * as moment from 'moment';
import { compose } from 'lodash/fp';
import { I18nService } from 'nestjs-i18n';
import {
ICashFlowSchemaSection,
ICashFlowStatementQuery,
ICashFlowStatementNetIncomeSection,
ICashFlowStatementAccountSection,
ICashFlowSchemaSectionAccounts,
ICashFlowStatementAccountMeta,
ICashFlowSchemaAccountRelation,
ICashFlowStatementSectionType,
ICashFlowStatementData,
ICashFlowSchemaTotalSection,
ICashFlowStatementTotalSection,
ICashFlowStatementSection,
ICashFlowCashBeginningNode,
ICashFlowStatementAggregateSection,
} from './Cashflow.types';
import { CASH_FLOW_SCHEMA } from './schema';
import { FinancialSheet } from '../../common/FinancialSheet';
import { ACCOUNT_ROOT_TYPE } from '@/constants/accounts';
import { CashFlowStatementDatePeriods } from './CashFlowDatePeriods';
import { DISPLAY_COLUMNS_BY } from './constants';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
import { INumberFormatQuery } from '../../types/Report.types';
import { transformToMapBy } from '@/utils/transform-to-map-by';
import { accumSum } from '@/utils/accum-sum';
import { ModelObject } from 'objection';
export default class CashFlowStatement extends compose(
CashFlowStatementDatePeriods,
FinancialSheetStructure
)(FinancialSheet) {
readonly baseCurrency: string;
readonly i18n: I18nService;
readonly sectionsByIds = {};
readonly cashFlowSchemaMap: Map<string, ICashFlowSchemaSection>;
readonly cashFlowSchemaSeq: Array<string>;
readonly accountByTypeMap: Map<string, Account[]>;
readonly accountsByRootType: Map<string, Account[]>;
readonly ledger: ILedger;
readonly cashLedger: ILedger;
readonly netIncomeLedger: ILedger;
readonly query: ICashFlowStatementQuery;
readonly numberFormat: INumberFormatQuery;
readonly comparatorDateType: string;
readonly dateRangeSet: { fromDate: Date; toDate: Date }[];
/**
* Constructor method.
* @constructor
*/
constructor(
accounts: Account[],
ledger: ILedger,
cashLedger: ILedger,
netIncomeLedger: ILedger,
query: ICashFlowStatementQuery,
baseCurrency: string,
i18n
) {
super();
this.baseCurrency = baseCurrency;
this.i18n = i18n;
this.ledger = ledger;
this.cashLedger = cashLedger;
this.netIncomeLedger = netIncomeLedger;
this.accountByTypeMap = transformToMapBy(accounts, 'accountType');
this.accountsByRootType = transformToMapBy(accounts, 'accountRootType');
this.query = query;
this.numberFormat = this.query.numberFormat;
this.dateRangeSet = [];
this.comparatorDateType =
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
this.initDateRangeCollection();
}
// --------------------------------------------
// # GENERAL UTILITIES
// --------------------------------------------
/**
* Retrieve the expense accounts ids.
* @return {number[]}
*/
private getAccountsIdsByType = (accountType: string): number[] => {
const expenseAccounts = this.accountsByRootType.get(accountType);
const expenseAccountsIds = map(expenseAccounts, 'id');
return expenseAccountsIds;
};
/**
* Detarmines the given display columns by type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
private isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
return this.query.displayColumnsType === displayColumnsBy;
};
/**
* Adjustments the given amount.
* @param {string} direction
* @param {number} amount -
* @return {number}
*/
private amountAdjustment = (direction: 'mines' | 'plus', amount): number => {
return R.when(
R.always(R.equals(direction, 'mines')),
R.multiply(-1)
)(amount);
};
// --------------------------------------------
// # NET INCOME NODE
// --------------------------------------------
/**
* Retrieve the accounts net income.
* @returns {number} - Amount of net income.
*/
private getAccountsNetIncome(): number {
// Mapping income/expense accounts ids.
const incomeAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.INCOME
);
const expenseAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.EXPENSE
);
// Income closing balance.
const incomeClosingBalance = accumSum(incomeAccountsIds, (id) =>
this.netIncomeLedger.whereAccountId(id).getClosingBalance()
);
// Expense closing balance.
const expenseClosingBalance = accumSum(expenseAccountsIds, (id) =>
this.netIncomeLedger.whereAccountId(id).getClosingBalance()
);
// Net income = income - expenses.
const netIncome = incomeClosingBalance - expenseClosingBalance;
return netIncome;
}
/**
* Parses the net income section from the given section schema.
* @param {ICashFlowSchemaSection} nodeSchema - Report section schema.
* @returns {ICashFlowStatementNetIncomeSection}
*/
private netIncomeSectionMapper = (
nodeSchema: ICashFlowSchemaSection
): ICashFlowStatementNetIncomeSection => {
const netIncome = this.getAccountsNetIncome();
const node = {
id: nodeSchema.id,
label: this.i18n.__(nodeSchema.label),
total: this.getAmountMeta(netIncome),
sectionType: ICashFlowStatementSectionType.NET_INCOME,
};
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToNetIncomeNode
)
)(node);
};
// --------------------------------------------
// # ACCOUNT NODE
// --------------------------------------------
/**
* Retrieve account meta.
* @param {ICashFlowSchemaAccountRelation} relation - Account relation.
* @param {IAccount} account -
* @returns {ICashFlowStatementAccountMeta}
*/
private accountMetaMapper = (
relation: ICashFlowSchemaAccountRelation,
account: ModelObject<Account>
): ICashFlowStatementAccountMeta => {
// Retrieve the closing balance of the given account.
const getClosingBalance = (id) =>
this.ledger.whereAccountId(id).getClosingBalance();
const closingBalance = R.compose(
// Multiplies the amount by -1 in case the relation in mines.
R.curry(this.amountAdjustment)(relation.direction)
)(getClosingBalance(account.id));
const node = {
id: account.id,
code: account.code,
label: account.name,
accountType: account.accountType,
adjustmentType: relation.direction,
total: this.getAmountMeta(closingBalance),
sectionType: ICashFlowStatementSectionType.ACCOUNT,
};
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAccountNode
)
)(node);
};
/**
* Retrieve accounts sections by the given schema relation.
* @param {ICashFlowSchemaAccountRelation} relation
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getAccountsBySchemaRelation = (
relation: ICashFlowSchemaAccountRelation
): ICashFlowStatementAccountMeta[] => {
const accounts = defaultTo(this.accountByTypeMap.get(relation.type), []);
const accountMetaMapper = R.curry(this.accountMetaMapper)(relation);
return R.map(accountMetaMapper)(accounts);
};
/**
* Retrieve the accounts meta.
* @param {string[]} types
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getAccountsBySchemaRelations = (
relations: ICashFlowSchemaAccountRelation[]
): ICashFlowStatementAccountMeta[] => {
return R.pipe(
R.append(R.map(this.getAccountsBySchemaRelation)(relations)),
R.flatten
)([]);
};
/**
* Calculates the accounts total
* @param {ICashFlowStatementAccountMeta[]} accounts
* @returns {number}
*/
private getAccountsMetaTotal = (
accounts: ICashFlowStatementAccountMeta[]
): number => {
return sumBy(accounts, 'total.amount');
};
/**
* Retrieve the accounts section from the section schema.
* @param {ICashFlowSchemaSectionAccounts} sectionSchema
* @returns {ICashFlowStatementAccountSection}
*/
private accountsSectionParser = (
sectionSchema: ICashFlowSchemaSectionAccounts
): ICashFlowStatementAccountSection => {
const { accountsRelations } = sectionSchema;
const accounts = this.getAccountsBySchemaRelations(accountsRelations);
const accountsTotal = this.getAccountsMetaTotal(accounts);
const total = this.getTotalAmountMeta(accountsTotal);
const node = {
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
id: sectionSchema.id,
label: this.i18n.__(sectionSchema.label),
footerLabel: this.i18n.__(sectionSchema.footerLabel),
children: accounts,
total,
};
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAggregateNode
)
)(node);
};
/**
* Detarmines the schema section type.
* @param {string} type
* @param {ICashFlowSchemaSection} section
* @returns {boolean}
*/
private isSchemaSectionType = R.curry(
(type: string, section: ICashFlowSchemaSection): boolean => {
return type === section.sectionType;
}
);
// --------------------------------------------
// # AGGREGATE NODE
// --------------------------------------------
/**
* Aggregate schema node parser to aggregate report node.
* @param {ICashFlowSchemaSection} schemaSection
* @returns {ICashFlowStatementAggregateSection}
*/
private regularSectionParser = R.curry(
(
children,
schemaSection: ICashFlowSchemaSection
): ICashFlowStatementAggregateSection => {
const node = {
id: schemaSection.id,
label: this.i18n.__(schemaSection.label),
footerLabel: this.i18n.__(schemaSection.footerLabel),
sectionType: ICashFlowStatementSectionType.AGGREGATE,
children,
};
return R.compose(
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
this.assocRegularSectionTotal
),
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAggregateNode
)
)
)(node);
}
);
private transformSectionsToMap = (sections: ICashFlowSchemaSection[]) => {
return this.reduceNodesDeep(
sections,
(acc, section) => {
if (section.id) {
acc[`${section.id}`] = section;
}
return acc;
},
{}
);
};
// --------------------------------------------
// # TOTAL EQUATION NODE
// --------------------------------------------
private sectionsMapToTotal = (mappedSections: { [key: number]: any }) => {
return mapValues(mappedSections, (node) => get(node, 'total.amount') || 0);
};
/**
* Evauluate equaation string with the given scope table.
* @param {string} equation -
* @param {{ [key: string]: number }} scope -
* @return {number}
*/
private evaluateEquation = (
equation: string,
scope: { [key: string | number]: number }
): number => {
return mathjs.evaluate(equation, scope);
};
/**
* Retrieve the total section from the eqauation parser.
* @param {ICashFlowSchemaTotalSection} sectionSchema
* @param {ICashFlowSchemaSection[]} accumulatedSections
* @returns {ICashFlowStatementTotalSection}
*/
private totalEquationSectionParser = (
accumulatedSections: ICashFlowSchemaSection[],
sectionSchema: ICashFlowSchemaTotalSection
): ICashFlowStatementTotalSection => {
const mappedSectionsById = this.transformSectionsToMap(accumulatedSections);
const nodesTotalById = this.sectionsMapToTotal(mappedSectionsById);
const total = this.evaluateEquation(sectionSchema.equation, nodesTotalById);
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.curry(this.assocTotalEquationDatePeriods)(
mappedSectionsById,
sectionSchema.equation
)
)
)({
sectionType: ICashFlowStatementSectionType.TOTAL,
id: sectionSchema.id,
label: this.i18n.__(sectionSchema.label),
total: this.getTotalAmountMeta(total),
});
};
/**
* Retrieve the beginning cash from date.
* @param {Date|string} fromDate -
* @return {Date}
*/
private beginningCashFrom = (fromDate: string | Date): Date => {
return moment(fromDate).subtract(1, 'days').toDate();
};
/**
* Retrieve account meta.
* @param {ICashFlowSchemaAccountRelation} relation
* @param {IAccount} account
* @returns {ICashFlowStatementAccountMeta}
*/
private cashAccountMetaMapper = (
relation: ICashFlowSchemaAccountRelation,
account: ModelObject<Account>
): ICashFlowStatementAccountMeta => {
const cashToDate = this.beginningCashFrom(this.query.fromDate);
const closingBalance = this.cashLedger
.whereToDate(cashToDate)
.whereAccountId(account.id)
.getClosingBalance();
const node = {
id: account.id,
code: account.code,
label: account.name,
accountType: account.accountType,
adjustmentType: relation.direction,
total: this.getAmountMeta(closingBalance),
sectionType: ICashFlowStatementSectionType.ACCOUNT,
};
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocCashAtBeginningAccountDatePeriods
)
)(node);
};
/**
* Retrieve accounts sections by the given schema relation.
* @param {ICashFlowSchemaAccountRelation} relation
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getCashAccountsBySchemaRelation = (
relation: ICashFlowSchemaAccountRelation
): ICashFlowStatementAccountMeta[] => {
const accounts = this.accountByTypeMap.get(relation.type) || [];
const accountMetaMapper = R.curry(this.cashAccountMetaMapper)(relation);
return accounts.map(accountMetaMapper);
};
/**
* Retrieve the accounts meta.
* @param {string[]} types
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getCashAccountsBySchemaRelations = (
relations: ICashFlowSchemaAccountRelation[]
): ICashFlowStatementAccountMeta[] => {
return R.concat(...R.map(this.getCashAccountsBySchemaRelation)(relations));
};
/**
* Parses the cash at beginning section.
* @param {ICashFlowSchemaTotalSection} sectionSchema -
* @return {ICashFlowCashBeginningNode}
*/
private cashAtBeginningSectionParser = (
nodeSchema: ICashFlowSchemaSection
): ICashFlowCashBeginningNode => {
const { accountsRelations } = nodeSchema;
const children = this.getCashAccountsBySchemaRelations(accountsRelations);
const total = this.getAccountsMetaTotal(children);
const node = {
sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING,
id: nodeSchema.id,
label: this.i18n.__(nodeSchema.label),
children,
total: this.getTotalAmountMeta(total),
};
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocCashAtBeginningDatePeriods
)
)(node);
};
/**
* Parses the schema section.
* @param {ICashFlowSchemaSection} schemaNode
* @returns {ICashFlowSchemaSection}
*/
private schemaSectionParser = (
schemaNode: ICashFlowSchemaSection,
children
): ICashFlowSchemaSection | ICashFlowStatementSection => {
return R.compose(
// Accounts node.
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS),
this.accountsSectionParser
),
// Net income node.
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.NET_INCOME),
this.netIncomeSectionMapper
),
// Cash at beginning node.
R.when(
this.isSchemaSectionType(
ICashFlowStatementSectionType.CASH_AT_BEGINNING
),
this.cashAtBeginningSectionParser
),
// Aggregate node. (that has no section type).
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
this.regularSectionParser(children)
)
)(schemaNode);
};
/**
* Parses the schema section.
* @param {ICashFlowSchemaSection | ICashFlowStatementSection} section
* @param {number} key
* @param {ICashFlowSchemaSection[]} parentValue
* @param {(ICashFlowSchemaSection | ICashFlowStatementSection)[]} accumulatedSections
* @returns {ICashFlowSchemaSection}
*/
private schemaSectionTotalParser = (
section: ICashFlowSchemaSection | ICashFlowStatementSection,
key: number,
parentValue: ICashFlowSchemaSection[],
context,
accumulatedSections: (ICashFlowSchemaSection | ICashFlowStatementSection)[]
): ICashFlowSchemaSection | ICashFlowStatementSection => {
return R.compose(
// Total equation section.
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.TOTAL),
R.curry(this.totalEquationSectionParser)(accumulatedSections)
)
)(section);
};
/**
* Schema sections parser.
* @param {ICashFlowSchemaSection[]}schema
* @returns {ICashFlowStatementSection[]}
*/
private schemaSectionsParser = (
schema: ICashFlowSchemaSection[]
): ICashFlowStatementSection[] => {
return this.mapNodesDeepReverse(schema, this.schemaSectionParser);
};
/**
* Writes the `total` property to the aggregate node.
* @param {ICashFlowStatementSection} section
* @return {ICashFlowStatementSection}
*/
private assocRegularSectionTotal = (section: ICashFlowStatementSection) => {
const total = this.getAccountsMetaTotal(section.children);
return R.assoc('total', this.getTotalAmountMeta(total), section);
};
/**
* Parses total schema nodes.
* @param {(ICashFlowSchemaSection | ICashFlowStatementSection)[]} sections
* @returns {(ICashFlowSchemaSection | ICashFlowStatementSection)[]}
*/
private totalSectionsParser = (
sections: (ICashFlowSchemaSection | ICashFlowStatementSection)[]
): (ICashFlowSchemaSection | ICashFlowStatementSection)[] => {
return this.reduceNodesDeep(
sections,
(acc, value, key, parentValue, context) => {
set(
acc,
context.path,
this.schemaSectionTotalParser(value, key, parentValue, context, acc)
);
return acc;
},
[]
);
};
// --------------------------------------------
// REPORT FILTERING
// --------------------------------------------
/**
* Detarmines the given section has children and not empty.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isSectionHasChildren = (
section: ICashFlowStatementSection
): boolean => {
return !isEmpty(section.children);
};
/**
* Detarmines whether the section has no zero amount.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isSectionNoneZero = (section: ICashFlowStatementSection): boolean => {
return section.total.amount !== 0;
};
/**
* Detarmines whether the parent accounts sections has children.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isAccountsSectionHasChildren = (
section: ICashFlowStatementSection[]
): boolean => {
return R.ifElse(
this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS),
this.isSectionHasChildren,
R.always(true)
)(section);
};
/**
* Detarmines the account section has no zero otherwise returns true.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isAccountLeafNoneZero = (
section: ICashFlowStatementSection[]
): boolean => {
return R.ifElse(
this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNT),
this.isSectionNoneZero,
R.always(true)
)(section);
};
/**
* Deep filters the non-zero accounts leafs of the report sections.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterNoneZeroAccountsLeafs = (
sections: ICashFlowStatementSection[]
): ICashFlowStatementSection[] => {
return this.filterNodesDeep(sections, this.isAccountLeafNoneZero);
};
/**
* Deep filter the non-children sections of the report sections.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterNoneChildrenSections = (
sections: ICashFlowStatementSection[]
): ICashFlowStatementSection[] => {
return this.filterNodesDeep(sections, this.isAccountsSectionHasChildren);
};
/**
* Filters the report data.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterReportData = (
sections: ICashFlowStatementSection[]
): ICashFlowStatementSection[] => {
return R.compose(
this.filterNoneChildrenSections,
this.filterNoneZeroAccountsLeafs
)(sections);
};
/**
* Schema parser.
* @param {ICashFlowSchemaSection[]} schema
* @returns {ICashFlowSchemaSection[]}
*/
private schemaParser = (
schema: ICashFlowSchemaSection[]
): ICashFlowSchemaSection[] => {
return R.compose(
R.when(
R.always(this.query.noneTransactions || this.query.noneZero),
this.filterReportData
),
this.totalSectionsParser,
this.schemaSectionsParser
)(schema);
};
/**
* Retrieve the cashflow statement data.
* @return {ICashFlowStatementData}
*/
public reportData = (): ICashFlowStatementData => {
return this.schemaParser(R.clone(CASH_FLOW_SCHEMA));
};
}