From c57f2de970de65e14f99d1e442afcc04adb84d49 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Thu, 6 May 2021 03:10:14 +0200 Subject: [PATCH] WIP: transactions by customers report. --- .../api/controllers/FinancialStatements.ts | 3 +- .../TransactionsByCustomers/index.ts | 10 ++ .../src/interfaces/TransactionsByCustomers.ts | 9 +- .../TransactionsByCustomers.ts | 170 +++++++++++++++--- .../TransactionsByCustomersService.ts | 14 +- .../TransactionsByCustomersTableRows.ts | 113 ++++++++++++ 6 files changed, 288 insertions(+), 31 deletions(-) create mode 100644 server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows.ts diff --git a/server/src/api/controllers/FinancialStatements.ts b/server/src/api/controllers/FinancialStatements.ts index 5be1b3ed8..052b7c84c 100644 --- a/server/src/api/controllers/FinancialStatements.ts +++ b/server/src/api/controllers/FinancialStatements.ts @@ -71,8 +71,7 @@ export default class FinancialStatementsService { router.use( '/transactions-by-customers', Container.get(TransactionsByCustomers).router(), - ) - + ); return router; } } diff --git a/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts b/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts index 8fb5d9c07..092d81df6 100644 --- a/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts +++ b/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts @@ -4,11 +4,15 @@ import { Inject } from 'typedi'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import BaseFinancialReportController from '../BaseFinancialReportController'; import TransactionsByCustomersService from 'services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService'; +import TransactionsByCustomersTableRows from 'services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows'; export default class TransactionsByCustomersReportController extends BaseFinancialReportController { @Inject() transactionsByCustomersService: TransactionsByCustomersService; + @Inject() + transactionsByCustomersTableRows: TransactionsByCustomersTableRows; + /** * Router constructor. */ @@ -60,6 +64,12 @@ export default class TransactionsByCustomersReportController extends BaseFinanci filter ); + return res.status(200).send({ + table: { + rows: this.transactionsByCustomersTableRows.tableRows(data), + }, + }); + return res.status(200).send({ data: this.transfromToResponse(data), columns: this.transfromToResponse(columns), diff --git a/server/src/interfaces/TransactionsByCustomers.ts b/server/src/interfaces/TransactionsByCustomers.ts index ecc010bd0..f888b249a 100644 --- a/server/src/interfaces/TransactionsByCustomers.ts +++ b/server/src/interfaces/TransactionsByCustomers.ts @@ -3,6 +3,7 @@ import { INumberFormatQuery } from './FinancialStatements'; export interface ITransactionsByCustomersAmount { amount: number; formattedAmount: string; + currencyCode: string; } export interface ITransactionsByCustomersTransaction { @@ -10,9 +11,11 @@ export interface ITransactionsByCustomersTransaction { credit: ITransactionsByCustomersAmount; debit: ITransactionsByCustomersAmount; runningBalance: ITransactionsByCustomersAmount; + currencyCode: string; referenceNumber: string; transactionNumber: string; -} + createdAt: string|Date, +}; export interface ITransactionsByCustomersCustomer { customerName: string; @@ -29,9 +32,7 @@ export interface ITransactionsByCustomersFilter { noneZero: boolean; } -export interface ITransactionsByCustomersData { - customers: ITransactionsByCustomersCustomer[]; -} +export type ITransactionsByCustomersData = ITransactionsByCustomersCustomer[]; export interface ITransactionsByCustomersStatement { data: ITransactionsByCustomersData; diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts index 3e6f4bde4..fad0d198f 100644 --- a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts @@ -1,22 +1,34 @@ import * as R from 'ramda'; +import { sumBy } from 'lodash'; import FinancialSheet from '../FinancialSheet'; -import { IAccountTransaction, ICustomer } from 'interfaces'; -import { transaction } from 'objection'; +import { + ITransactionsByCustomersTransaction, + ITransactionsByCustomersFilter, + ITransactionsByCustomersCustomer, + ITransactionsByCustomersAmount, + ITransactionsByCustomersData, + INumberFormatQuery, + IAccountTransaction, + ICustomer, +} from 'interfaces'; export default class TransactionsByCustomers extends FinancialSheet { - customers: ICustomer[]; - transactionsByContact: any; - baseCurrency: string; + readonly customers: ICustomer[]; + readonly transactionsByContact: any; + readonly filter: ITransactionsByCustomersFilter; + readonly baseCurrency: string; + readonly numberFormat: INumberFormatQuery; /** * Constructor method. * @param {ICustomer} customers - * @param transactionsByContact + * @param {Map} transactionsByContact * @param {string} baseCurrency */ constructor( customers: ICustomer[], transactionsByContact: Map, + filter: ITransactionsByCustomersFilter, baseCurrency: string ) { super(); @@ -24,17 +36,24 @@ export default class TransactionsByCustomers extends FinancialSheet { this.customers = customers; this.transactionsByContact = transactionsByContact; this.baseCurrency = baseCurrency; + this.filter = filter; + this.numberFormat = this.filter.numberFormat; } /** - * + * Customer transaction mapper. + * @param {any} transaction - + * @return {Omit} */ private customerTransactionMapper( transaction - ): ITransactionsByCustomersTransaction { + ): Omit { + const currencyCode = 'USD'; + return { - credit: transaction.credit, - debit: transaction.debit, + credit: this.getCustomerAmount(transaction.credit, currencyCode), + debit: this.getCustomerAmount(transaction.debit, currencyCode), + currencyCode: 'USD', transactionNumber: transaction.transactionNumber, referenceNumber: transaction.referenceNumber, date: transaction.date, @@ -42,39 +61,144 @@ export default class TransactionsByCustomers extends FinancialSheet { }; } + /** + * Customer transactions mapper with running balance. + * @param {number} openingBalance + * @param {ITransactionsByCustomersTransaction[]} transactions + * @returns {ITransactionsByCustomersTransaction[]} + */ private customerTransactionRunningBalance( openingBalance: number, - transaction: ITransactionsByCustomersTransaction - ): ITransactionsByCustomersTransaction { - + transactions: Omit[] + ): any { + let _openingBalance = openingBalance; + + return transactions.map( + (transaction: ITransactionsByCustomersTransaction) => { + _openingBalance += transaction.debit.amount; + _openingBalance -= transaction.credit.amount; + + const runningBalance = this.getCustomerAmount( + _openingBalance, + transaction.currencyCode + ); + return { ...transaction, runningBalance }; + } + ); } - private customerTransactions(customerId: number) { + /** + * Retrieve the customer transactions from the given customer id and opening balance. + * @param {number} customerId - Customer id. + * @param {number} openingBalance - Opening balance amount. + * @returns {ITransactionsByCustomersTransaction[]} + */ + private customerTransactions( + customerId: number, + openingBalance: number + ): ITransactionsByCustomersTransaction[] { const transactions = this.transactionsByContact.get(customerId + '') || []; return R.compose( - R.map(this.customerTransactionMapper), - R.map(R.curry(this.customerTransactionRunningBalance(0))) + R.curry(this.customerTransactionRunningBalance)(openingBalance), + R.map(this.customerTransactionMapper.bind(this)) ).bind(this)(transactions); } - private customerMapper(customer: ICustomer) { + /** + * Retrieve the customer closing balance from the given transactions and opening balance. + * @param {number} customerTransactions + * @param {number} openingBalance + * @returns {number} + */ + private getCustomerClosingBalance( + customerTransactions: ITransactionsByCustomersTransaction[], + openingBalance: number + ): number { + const closingBalance = openingBalance; + + const totalCredit = sumBy(customerTransactions, 'credit'); + const totalDebit = sumBy(customerTransactions, 'debit'); + + return closingBalance + (totalDebit - totalCredit); + } + + /** + * Retrieve the given customer opening balance from the given customer id. + * @param {number} customerId + * @returns {number} + */ + private getCustomerOpeningBalance(customerId: number): number { + return 0; + } + + /** + * Customer section mapper. + * @param {ICustomer} customer + * @returns {ITransactionsByCustomersCustomer} + */ + private customerMapper( + customer: ICustomer + ): ITransactionsByCustomersCustomer { + const openingBalance = this.getCustomerOpeningBalance(1); + const transactions = this.customerTransactions(customer.id, openingBalance); + const closingBalance = this.getCustomerClosingBalance(transactions, 0); + return { customerName: customer.displayName, - openingBalance: {}, - closingBalance: {}, - transactions: this.customerTransactions(customer.id), + openingBalance: this.getCustomerAmount( + openingBalance, + customer.currencyCode + ), + closingBalance: this.getCustomerAmount( + closingBalance, + customer.currencyCode + ), + transactions, }; } - private customersMapper(customers: ICustomer[]) { - return customers.map(this.customerMapper.bind(this)); + /** + * Retrieve the customer amount format meta. + * @param {number} amount + * @param {string} currencyCode + * @returns {ITransactionsByCustomersAmount} + */ + private getCustomerAmount( + amount: number, + currencyCode: string + ): ITransactionsByCustomersAmount { + return { + amount, + formattedAmount: this.formatNumber(amount, { currencyCode }), + currencyCode, + }; } - public reportData() { + /** + * Retrieve the customers sections of the report. + * @param {ICustomer[]} customers + * @returns {ITransactionsByCustomersCustomer[]} + */ + private customersMapper( + customers: ICustomer[] + ): ITransactionsByCustomersCustomer[] { + return R.compose(R.map(this.customerMapper.bind(this))).bind(this)( + customers + ); + } + + /** + * Retrieve the report data. + * @returns {ITransactionsByCustomersData} + */ + public reportData(): ITransactionsByCustomersData { return this.customersMapper(this.customers); } + /** + * Retrieve the report columns. + */ public reportColumns() { return []; } diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts index 4582dc77c..9c8d354e7 100644 --- a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts @@ -9,7 +9,8 @@ import { } from 'interfaces'; import TransactionsByCustomers from './TransactionsByCustomers'; -export default class TransactionsByCustomersService implements ITransactionsByCustomersService { +export default class TransactionsByCustomersService + implements ITransactionsByCustomersService { @Inject() tenancy: TenancyService; @@ -59,6 +60,10 @@ export default class TransactionsByCustomersService implements ITransactionsByCu key: 'base_currency', }); + const filter = { + ...this.defaultQuery, + ...query, + }; const customers = await Customer.query().orderBy('displayName'); // Retrieve all journal transactions based on the given query. @@ -66,10 +71,15 @@ export default class TransactionsByCustomersService implements ITransactionsByCu fromDate: query.fromDate, toDate: query.toDate, }); + // Transactions map by contact id. + const transactionsMap = new Map( + Object.entries(groupBy(transactions, 'contactId')) + ); // Transactions by customers data mapper. const reportInstance = new TransactionsByCustomers( customers, - new Map(Object.entries(groupBy(transactions, 'contactId'))), + transactionsMap, + filter, baseCurrency ); const reportData = reportInstance.reportData(); diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows.ts new file mode 100644 index 000000000..09b66298e --- /dev/null +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows.ts @@ -0,0 +1,113 @@ +import * as R from 'ramda'; +import { tableRowMapper, tableMapper } from 'utils'; +import { ITransactionsByCustomersCustomer, ITableRow } from 'interfaces'; + +enum ROW_TYPE { + OPENING_BALANCE = 'OPENING_BALANCE', + CLOSING_BALANCE = 'CLOSING_BALANCE', + TRANSACTION = 'TRANSACTION', + CUSTOMER = 'CUSTOMER', +} + +export default class TransactionsByCustomersTableRows { + /** + * Retrieve the table rows of customer transactions. + * @param {ITransactionsByCustomersCustomer} customer + * @returns {ITableRow[]} + */ + private customerTransactions( + customer: ITransactionsByCustomersCustomer + ): ITableRow[] { + const columns = [ + { key: 'date', accessor: 'date' }, + { key: 'account', accessor: 'account.name' }, + { key: 'referenceType', accessor: 'referenceType' }, + { key: 'transactionType', accessor: 'transactionType' }, + { key: 'credit', accessor: 'credit.formattedAmount' }, + { key: 'debit', accessor: 'debit.formattedAmount' }, + ]; + return tableMapper(customer.transactions, columns, { + rowTypes: [ROW_TYPE.TRANSACTION] + }); + } + + /** + * Retrieve the table row of customer opening balance. + * @param {ITransactionsByCustomersCustomer} customer + * @returns {ITableRow} + */ + private customerOpeningBalance( + customer: ITransactionsByCustomersCustomer + ): ITableRow { + const columns = [ + { key: 'openingBalanceLabel', value: 'Opening balance' }, + { + key: 'openingBalanceValue', + accessor: 'openingBalance.formattedAmount', + }, + ]; + return tableRowMapper(customer, columns, { + rowTypes: [ROW_TYPE.OPENING_BALANCE] + }); + } + + /** + * Retrieve the table row of customer closing balance. + * @param {ITransactionsByCustomersCustomer} customer - + * @returns {ITableRow} + */ + private customerClosingBalance( + customer: ITransactionsByCustomersCustomer + ): ITableRow { + const columns = [ + { key: 'openingBalanceLabel', value: 'Closing balance' }, + { + key: 'openingBalanceValue', + accessor: 'closingBalance.formattedAmount', + }, + ]; + return tableRowMapper(customer, columns, { + rowTypes: [ROW_TYPE.CLOSING_BALANCE] + }); + } + + /** + * Retrieve the table row of customer details. + * @param {ITransactionsByCustomersCustomer} customer - + * @returns {ITableRow[]} + */ + private customerDetails( + customer: ITransactionsByCustomersCustomer + ) { + const columns = [{ key: 'customerName', accessor: 'customerName' }]; + + return { + ...tableRowMapper(customer, columns, { rowTypes: [ROW_TYPE.CUSTOMER] }), + children: R.pipe( + R.append(this.customerOpeningBalance(customer)), + R.concat(this.customerTransactions(customer)), + R.append(this.customerClosingBalance(customer)), + )([]), + } + } + + /** + * Retrieve the table rows of the customer section. + * @param {ITransactionsByCustomersCustomer} customer + * @returns {ITableRow[]} + */ + private customerRowsMapper(customer: ITransactionsByCustomersCustomer) { + return R.pipe( + R.append(this.customerDetails(customer)), + ).bind(this)([]); + } + + /** + * Retrieve the table rows of transactions by customers report. + * @param {ITransactionsByCustomersCustomer[]} customers + * @returns {ITableRow[]} + */ + public tableRows(customers: ITransactionsByCustomersCustomer[]): ITableRow[] { + return R.map(this.customerRowsMapper.bind(this))(customers); + } +}