From 8ca3509f03b9787a97763fa65eebb829b97da4df Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Wed, 5 May 2021 02:19:43 +0200 Subject: [PATCH] WIP: customer balance report. --- .../api/controllers/FinancialStatements.ts | 15 ++ .../CustomerBalanceSummary/index.ts | 74 +++++++ .../TransactionsByCustomers/index.ts | 72 +++++++ .../VendorBalanceSummary/index.ts | 0 .../src/interfaces/CustomerBalanceSummary.ts | 50 +++++ .../src/interfaces/TransactionsByCustomers.ts | 45 ++++ server/src/interfaces/index.ts | 4 +- .../src/services/Accounting/JournalPoster.ts | 1 + .../CustomerBalanceSummary.ts | 193 ++++++++++++++++++ .../CustomerBalanceSummaryService.ts | 110 ++++++++++ .../CustomerBalanceSummaryTableRows.ts | 108 ++++++++++ .../FinancialStatements/FinancialSheet.ts | 7 + .../TransactionsByCustomers.ts | 81 ++++++++ .../TransactionsByCustomersService.ts | 84 ++++++++ 14 files changed, 843 insertions(+), 1 deletion(-) create mode 100644 server/src/api/controllers/FinancialStatements/CustomerBalanceSummary/index.ts create mode 100644 server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts create mode 100644 server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts create mode 100644 server/src/interfaces/CustomerBalanceSummary.ts create mode 100644 server/src/interfaces/TransactionsByCustomers.ts create mode 100644 server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummary.ts create mode 100644 server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts create mode 100644 server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows.ts create mode 100644 server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts create mode 100644 server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts diff --git a/server/src/api/controllers/FinancialStatements.ts b/server/src/api/controllers/FinancialStatements.ts index 928b940c6..2e44fb697 100644 --- a/server/src/api/controllers/FinancialStatements.ts +++ b/server/src/api/controllers/FinancialStatements.ts @@ -11,6 +11,9 @@ import APAgingSummary from './FinancialStatements/APAgingSummary'; import PurchasesByItemsController from './FinancialStatements/PurchasesByItem'; import SalesByItemsController from './FinancialStatements/SalesByItems'; import InventoryValuationController from './FinancialStatements/InventoryValuationSheet'; +import CustomerBalanceSummaryController from './FinancialStatements/CustomerBalanceSummary'; +import VendorBalanceSummaryController from './FinancialStatements/VendorBalanceSummary'; +import TransactionsByCustomers from './FinancialStatements/TransactionsByCustomers'; @Service() export default class FinancialStatementsService { @@ -57,6 +60,18 @@ export default class FinancialStatementsService { '/inventory-valuation', Container.get(InventoryValuationController).router() ); + router.use( + '/customer-balance-summary', + Container.get(CustomerBalanceSummaryController).router(), + ); + router.use( + '/transactions-by-customers', + Container.get(TransactionsByCustomers).router(), + ) + // router.use( + // '/vendor-balance-summary', + // Container.get(VendorBalanceSummaryController).router(), + // ) return router; } } diff --git a/server/src/api/controllers/FinancialStatements/CustomerBalanceSummary/index.ts b/server/src/api/controllers/FinancialStatements/CustomerBalanceSummary/index.ts new file mode 100644 index 000000000..f9214a9ab --- /dev/null +++ b/server/src/api/controllers/FinancialStatements/CustomerBalanceSummary/index.ts @@ -0,0 +1,74 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { query } from 'express-validator'; +import { Inject } from 'typedi'; +import asyncMiddleware from 'api/middleware/asyncMiddleware'; +import CustomerBalanceSummary from 'services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService'; +import BaseFinancialReportController from '../BaseFinancialReportController'; +import CustomerBalanceSummaryTableRows from 'services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows'; + +export default class CustomerBalanceSummaryReportController extends BaseFinancialReportController { + @Inject() + customerBalanceSummaryService: CustomerBalanceSummary; + + @Inject() + customerBalanceSummaryTableRows: CustomerBalanceSummaryTableRows; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.get( + '/', + this.validationSchema, + asyncMiddleware(this.customerBalanceSummary.bind(this)) + ); + return router; + } + + /** + * Validation schema. + */ + get validationSchema() { + return [ + ...this.sheetNumberFormatValidationSchema, + query('as_date').optional().isISO8601(), + ]; + } + + /** + * Retrieve payable aging summary report. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + async customerBalanceSummary(req: Request, res: Response, next: NextFunction) { + const { tenantId, settings } = req; + const filter = this.matchedQueryData(req); + + try { + const { + data, + columns, + query, + } = await this.customerBalanceSummaryService.customerBalanceSummary( + tenantId, + filter + ); + + const tableRows = this.customerBalanceSummaryTableRows.tableRowsTransformer( + data + ); + return res.status(200).send({ + table: { + rows: tableRows + }, + columns: this.transfromToResponse(columns), + query: this.transfromToResponse(query), + }); + } catch (error) { + next(error); + } + } +} diff --git a/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts b/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts new file mode 100644 index 000000000..8fb5d9c07 --- /dev/null +++ b/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts @@ -0,0 +1,72 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { query } from 'express-validator'; +import { Inject } from 'typedi'; +import asyncMiddleware from 'api/middleware/asyncMiddleware'; +import BaseFinancialReportController from '../BaseFinancialReportController'; +import TransactionsByCustomersService from 'services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService'; + +export default class TransactionsByCustomersReportController extends BaseFinancialReportController { + @Inject() + transactionsByCustomersService: TransactionsByCustomersService; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.get( + '/', + this.validationSchema, + asyncMiddleware(this.transactionsByCustomers.bind(this)) + ); + return router; + } + + /** + * Validation schema. + */ + get validationSchema() { + return [ + ...this.sheetNumberFormatValidationSchema, + query('from_date').optional().isISO8601(), + query('to_date').optional().isISO8601(), + query('none_zero').optional().isBoolean().toBoolean(), + query('none_transactions').optional().isBoolean().toBoolean(), + ]; + } + + /** + * Retrieve payable aging summary report. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + async transactionsByCustomers( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const filter = this.matchedQueryData(req); + + try { + const { + data, + columns, + query, + } = await this.transactionsByCustomersService.transactionsByCustomers( + tenantId, + filter + ); + + return res.status(200).send({ + data: this.transfromToResponse(data), + columns: this.transfromToResponse(columns), + query: this.transfromToResponse(query), + }); + } catch (error) { + next(error); + } + } +} diff --git a/server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts b/server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/interfaces/CustomerBalanceSummary.ts b/server/src/interfaces/CustomerBalanceSummary.ts new file mode 100644 index 000000000..856924bdf --- /dev/null +++ b/server/src/interfaces/CustomerBalanceSummary.ts @@ -0,0 +1,50 @@ +import { INumberFormatQuery } from './FinancialStatements'; + +export interface ICustomerBalanceSummaryQuery { + asDate: Date; + numberFormat: INumberFormatQuery; + comparison: { + percentageOfColumn: boolean; + }; + noneTransactions: boolean; + noneZero: boolean; +} + +export interface ICustomerBalanceSummaryAmount { + amount: number; + formattedAmount: string; + currencyCode: string; +} +export interface ICustomerBalanceSummaryPercentage { + amount: number; + formattedAmount: string; +} + +export interface ICustomerBalanceSummaryCustomer { + customerName: string; + total: ICustomerBalanceSummaryAmount; + percentageOfColumn?: ICustomerBalanceSummaryPercentage; +} + +export interface ICustomerBalanceSummaryTotal { + total: ICustomerBalanceSummaryAmount; + percentageOfColumn?: ICustomerBalanceSummaryPercentage; +} + +export interface ICustomerBalanceSummaryData { + customers: ICustomerBalanceSummaryCustomer[]; + total: ICustomerBalanceSummaryTotal; +} + +export interface ICustomerBalanceSummaryStatement { + data: ICustomerBalanceSummaryData; + columns: {}; + query: ICustomerBalanceSummaryQuery; +} + +export interface ICustomerBalanceSummaryService { + customerBalanceSummary( + tenantId: number, + query: ICustomerBalanceSummaryQuery, + ): Promise; +} diff --git a/server/src/interfaces/TransactionsByCustomers.ts b/server/src/interfaces/TransactionsByCustomers.ts new file mode 100644 index 000000000..ecc010bd0 --- /dev/null +++ b/server/src/interfaces/TransactionsByCustomers.ts @@ -0,0 +1,45 @@ +import { INumberFormatQuery } from './FinancialStatements'; + +export interface ITransactionsByCustomersAmount { + amount: number; + formattedAmount: string; +} + +export interface ITransactionsByCustomersTransaction { + date: string|Date, + credit: ITransactionsByCustomersAmount; + debit: ITransactionsByCustomersAmount; + runningBalance: ITransactionsByCustomersAmount; + referenceNumber: string; + transactionNumber: string; +} + +export interface ITransactionsByCustomersCustomer { + customerName: string; + openingBalance: any; + closingBalance: any; + transactions: ITransactionsByCustomersTransaction[]; +} + +export interface ITransactionsByCustomersFilter { + fromDate: Date; + toDate: Date; + numberFormat: INumberFormatQuery; + noneTransactions: boolean; + noneZero: boolean; +} + +export interface ITransactionsByCustomersData { + customers: ITransactionsByCustomersCustomer[]; +} + +export interface ITransactionsByCustomersStatement { + data: ITransactionsByCustomersData; +} + +export interface ITransactionsByCustomersService { + transactionsByCustomers( + tenantId: number, + filter: ITransactionsByCustomersFilter + ): Promise; +} diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index c2738ab30..aac33fdbd 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -42,4 +42,6 @@ export * from './Mailable'; export * from './InventoryAdjustment'; export * from './Setup' export * from './IInventoryValuationSheet'; -export * from './SalesByItemsSheet'; \ No newline at end of file +export * from './SalesByItemsSheet'; +export * from './CustomerBalanceSummary'; +export * from './TransactionsByCustomers'; \ No newline at end of file diff --git a/server/src/services/Accounting/JournalPoster.ts b/server/src/services/Accounting/JournalPoster.ts index b99d29223..dc26a1d71 100644 --- a/server/src/services/Accounting/JournalPoster.ts +++ b/server/src/services/Accounting/JournalPoster.ts @@ -689,6 +689,7 @@ export default class JournalPoster implements IJournalPoster { }); return balance; } + getAccountEntries(accountId: number) { return this.entries.filter((entry) => entry.account === accountId); diff --git a/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummary.ts b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummary.ts new file mode 100644 index 000000000..9158b3714 --- /dev/null +++ b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummary.ts @@ -0,0 +1,193 @@ +import { sumBy } from 'lodash'; +import * as R from 'ramda'; +import FinancialSheet from '../FinancialSheet'; +import { + IJournalPoster, + ICustomer, + ICustomerBalanceSummaryCustomer, + ICustomerBalanceSummaryQuery, + ICustomerBalanceSummaryData, + ICustomerBalanceSummaryTotal, +} from 'interfaces'; + +export class CustomerBalanceSummaryReport extends FinancialSheet { + receivableLedger: IJournalPoster; + baseCurrency: string; + customers: ICustomer[]; + filter: ICustomerBalanceSummaryQuery; + + /** + * Constructor method. + * @param {IJournalPoster} receivableLedger + * @param {ICustomer[]} customers + * @param {ICustomerBalanceSummaryQuery} filter + * @param {string} baseCurrency + */ + constructor( + receivableLedger: IJournalPoster, + customers: ICustomer[], + filter: ICustomerBalanceSummaryQuery, + baseCurrency: string + ) { + super(); + + this.receivableLedger = receivableLedger; + this.baseCurrency = baseCurrency; + this.customers = customers; + this.filter = filter; + } + + getAmountMeta(amount: number) { + return { + amount, + formattedAmount: amount, + currencyCode: this.baseCurrency, + }; + } + + getPercentageMeta(amount: number) { + return { + amount, + formattedAmount: this.formatPercentage(amount), + }; + } + + /** + * Customer section mapper. + * @param {ICustomer} customer + * @returns {ICustomerBalanceSummaryCustomer} + */ + private customerMapper(customer: ICustomer): ICustomerBalanceSummaryCustomer { + const balance = this.receivableLedger.getContactBalance(null, customer.id); + + return { + customerName: customer.displayName, + total: this.getAmountMeta(balance), + }; + } + + /** + * Retrieve the customer summary section with percentage of column. + * @param {number} total + * @param {ICustomerBalanceSummaryCustomer} customer + * @returns {ICustomerBalanceSummaryCustomer} + */ + private customerCamparsionPercentageOfColumnMapper( + total: number, + customer: ICustomerBalanceSummaryCustomer + ): ICustomerBalanceSummaryCustomer { + const amount = this.getCustomerPercentageOfColumn( + total, + customer.total.amount + ); + return { + ...customer, + percentageOfColumn: this.getPercentageMeta(amount), + }; + } + + /** + * Mappes the customers summary sections with percentage of column. + * @param {ICustomerBalanceSummaryCustomer[]} customers - + * @return {ICustomerBalanceSummaryCustomer[]} + */ + private customerCamparsionPercentageOfColumn( + customers: ICustomerBalanceSummaryCustomer[] + ): ICustomerBalanceSummaryCustomer[] { + const customersTotal = this.getCustomersTotal(customers); + const camparsionPercentageOfColummn = R.curry( + this.customerCamparsionPercentageOfColumnMapper.bind(this) + )(customersTotal); + + return customers.map(camparsionPercentageOfColummn); + } + + /** + * Mappes the customer model object to customer balance summary section. + * @param {ICustomer[]} customers - Customers. + * @returns {ICustomerBalanceSummaryCustomer[]} + */ + private customersMapper( + customers: ICustomer[] + ): ICustomerBalanceSummaryCustomer[] { + return customers.map(this.customerMapper.bind(this)); + } + + /** + * Retrieve the customers sections of the report. + * @param {ICustomer} customers + * @returns {ICustomerBalanceSummaryCustomer[]} + */ + private getCustomersSection( + customers: ICustomer[] + ): ICustomerBalanceSummaryCustomer[] { + return R.compose( + R.when( + R.always(this.filter.comparison.percentageOfColumn), + this.customerCamparsionPercentageOfColumn.bind(this) + ), + this.customersMapper.bind(this) + ).bind(this)(customers); + } + + /** + * Retrieve the customers total. + * @param {ICustomerBalanceSummaryCustomer} customers + * @returns {number} + */ + private getCustomersTotal( + customers: ICustomerBalanceSummaryCustomer[] + ): number { + return sumBy( + customers, + (customer: ICustomerBalanceSummaryCustomer) => customer.total.amount + ); + } + + /** + * Calculates the customer percentage of column. + * @param {number} customerBalance - Customer balance. + * @param {number} totalBalance - Total customers balance. + * @returns {number} + */ + private getCustomerPercentageOfColumn( + customerBalance: number, + totalBalance: number + ) { + return customerBalance > 0 ? totalBalance / customerBalance : 0; + } + + /** + * Retrieve the customers total section. + * @param {ICustomer[]} customers + * @returns {ICustomerBalanceSummaryTotal} + */ + private customersTotalSection( + customers: ICustomerBalanceSummaryCustomer[] + ): ICustomerBalanceSummaryTotal { + const customersTotal = this.getCustomersTotal(customers); + + return { + total: this.getAmountMeta(customersTotal), + percentageOfColumn: this.getPercentageMeta(1), + }; + } + + /** + * Retrieve the report statement data. + * @returns {ICustomerBalanceSummaryData} + */ + public reportData(): ICustomerBalanceSummaryData { + const customersSections = this.getCustomersSection(this.customers); + const customersTotal = this.customersTotalSection(customersSections); + + return { + customers: customersSections, + total: customersTotal, + }; + } + + reportColumns() { + return []; + } +} diff --git a/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts new file mode 100644 index 000000000..259a66d55 --- /dev/null +++ b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts @@ -0,0 +1,110 @@ +import { Inject } from 'typedi'; +import moment from 'moment'; +import TenancyService from 'services/Tenancy/TenancyService'; +import Journal from 'services/Accounting/JournalPoster'; +import { + ICustomerBalanceSummaryService, + ICustomerBalanceSummaryQuery, + ICustomerBalanceSummaryStatement, +} from 'interfaces'; +import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary'; + +export default class CustomerBalanceSummaryService + implements ICustomerBalanceSummaryService { + + @Inject() + tenancy: TenancyService; + + @Inject('logger') + logger: any; + + /** + * Defaults balance sheet filter query. + * @return {ICustomerBalanceSummaryQuery} + */ + get defaultQuery(): ICustomerBalanceSummaryQuery { + return { + asDate: moment().format('YYYY-MM-DD'), + numberFormat: { + precision: 2, + divideOn1000: false, + showZero: false, + formatMoney: 'total', + negativeFormat: 'mines', + }, + comparison: { + percentageOfColumn: true, + }, + noneZero: false, + noneTransactions: false, + }; + } + + /** + * Retrieve the statment of customer balance summary report. + * @param {number} tenantId + * @param {ICustomerBalanceSummaryQuery} query + * @return {Promise} + */ + async customerBalanceSummary( + tenantId: number, + query: ICustomerBalanceSummaryQuery + ): Promise { + const { + accountRepository, + transactionsRepository, + } = this.tenancy.repositories(tenantId); + + const { Customer } = this.tenancy.models(tenantId); + + // Settings tenant service. + const settings = this.tenancy.settings(tenantId); + const baseCurrency = settings.get({ + group: 'organization', key: 'base_currency', + }); + + const filter = { + ...this.defaultQuery, + ...query, + }; + this.logger.info('[customer_balance_summary] trying to calculate the report.', { + filter, + tenantId, + }); + // Retrieve all accounts on the storage. + const accounts = await accountRepository.all(); + const accountsGraph = await accountRepository.getDependencyGraph(); + + // Retrieve all journal transactions based on the given query. + const transactions = await transactionsRepository.journal({ + toDate: query.asDate, + }); + + // Transform transactions to journal collection. + const transactionsJournal = Journal.fromTransactions( + transactions, + tenantId, + accountsGraph + ); + // Retrieve the customers list ordered by the display name. + const customers = await Customer.query().orderBy('displayName'); + + // Report instance. + const reportInstance = new CustomerBalanceSummaryReport( + transactionsJournal, + customers, + filter, + baseCurrency, + ); + // Retrieve the report statement. + const reportData = reportInstance.reportData(); + + // Retrieve the report columns. + const reportColumns = reportInstance.reportColumns(); + + return { + data: reportData, + columns: reportColumns, + }; + } +} diff --git a/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows.ts b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows.ts new file mode 100644 index 000000000..5dc8e5e76 --- /dev/null +++ b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows.ts @@ -0,0 +1,108 @@ +import { get } from 'lodash'; +import { + ICustomerBalanceSummaryData, + ICustomerBalanceSummaryCustomer, + ICustomerBalanceSummaryTotal, +} from 'interfaces'; +import { Service } from 'typedi'; + +interface IColumnMapperMeta { + key: string; + accessor?: string; + value?: string; +} + +interface ITableCell { + value: string; + key: string; +} + +type ITableRow = { + rows: ITableCell[]; +}; + +enum TABLE_ROWS_TYPES { + CUSTOMER = 'CUSTOMER', + TOTAL = 'TOTAL', +} + +function tableMapper( + data: Object[], + columns: IColumnMapperMeta[], + rowsMeta +): ITableRow[] { + return data.map((object) => tableRowMapper(object, columns, rowsMeta)); +} + +function tableRowMapper( + object: Object, + columns: IColumnMapperMeta[], + rowMeta +): ITableRow { + const cells = columns.map((column) => ({ + key: column.key, + value: column.value ? column.value : get(object, column.accessor), + })); + + return { + cells, + ...rowMeta, + }; +} + +@Service() +export default class CustomerBalanceSummaryTableRows { + /** + * Transformes the customers to table rows. + * @param {ICustomerBalanceSummaryCustomer[]} customers + * @returns {ITableRow[]} + */ + private customersTransformer( + customers: ICustomerBalanceSummaryCustomer[] + ): ITableRow[] { + const columns = [ + { key: 'customerName', accessor: 'customerName' }, + { key: 'total', accessor: 'total.formattedAmount' }, + { + key: 'percentageOfColumn', + accessor: 'percentageOfColumn.formattedAmount', + }, + ]; + return tableMapper(customers, columns, { + rowTypes: [TABLE_ROWS_TYPES.CUSTOMER], + }); + } + + /** + * Transformes the total to table row. + * @param {ICustomerBalanceSummaryTotal} total + * @returns {ITableRow} + */ + private totalTransformer(total: ICustomerBalanceSummaryTotal) { + const columns = [ + { key: 'total', value: 'Total' }, + { key: 'total', accessor: 'total.formattedAmount' }, + { + key: 'percentageOfColumn', + accessor: 'percentageOfColumn.formattedAmount', + }, + ]; + return tableRowMapper(total, columns, { + rowTypes: [TABLE_ROWS_TYPES.TOTAL], + }); + } + + /** + * Transformes the customer balance summary to table rows. + * @param {ICustomerBalanceSummaryData} customerBalanceSummary + * @returns {ITableRow[]} + */ + public tableRowsTransformer( + customerBalanceSummary: ICustomerBalanceSummaryData + ): ITableRow[] { + return [ + ...this.customersTransformer(customerBalanceSummary.customers), + this.totalTransformer(customerBalanceSummary.total), + ]; + } +} diff --git a/server/src/services/FinancialStatements/FinancialSheet.ts b/server/src/services/FinancialStatements/FinancialSheet.ts index 64ef0cc5b..e9d9af0de 100644 --- a/server/src/services/FinancialStatements/FinancialSheet.ts +++ b/server/src/services/FinancialStatements/FinancialSheet.ts @@ -55,4 +55,11 @@ export default class FinancialSheet { ...settings }); } + + + protected formatPercentage( + amount + ): string { + return `%${amount * 100}`; + } } diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts new file mode 100644 index 000000000..3e6f4bde4 --- /dev/null +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts @@ -0,0 +1,81 @@ +import * as R from 'ramda'; +import FinancialSheet from '../FinancialSheet'; +import { IAccountTransaction, ICustomer } from 'interfaces'; +import { transaction } from 'objection'; + +export default class TransactionsByCustomers extends FinancialSheet { + customers: ICustomer[]; + transactionsByContact: any; + baseCurrency: string; + + /** + * Constructor method. + * @param {ICustomer} customers + * @param transactionsByContact + * @param {string} baseCurrency + */ + constructor( + customers: ICustomer[], + transactionsByContact: Map, + baseCurrency: string + ) { + super(); + + this.customers = customers; + this.transactionsByContact = transactionsByContact; + this.baseCurrency = baseCurrency; + } + + /** + * + */ + private customerTransactionMapper( + transaction + ): ITransactionsByCustomersTransaction { + return { + credit: transaction.credit, + debit: transaction.debit, + transactionNumber: transaction.transactionNumber, + referenceNumber: transaction.referenceNumber, + date: transaction.date, + createdAt: transaction.createdAt, + }; + } + + private customerTransactionRunningBalance( + openingBalance: number, + transaction: ITransactionsByCustomersTransaction + ): ITransactionsByCustomersTransaction { + + } + + private customerTransactions(customerId: number) { + const transactions = this.transactionsByContact.get(customerId + '') || []; + + return R.compose( + R.map(this.customerTransactionMapper), + R.map(R.curry(this.customerTransactionRunningBalance(0))) + ).bind(this)(transactions); + } + + private customerMapper(customer: ICustomer) { + return { + customerName: customer.displayName, + openingBalance: {}, + closingBalance: {}, + transactions: this.customerTransactions(customer.id), + }; + } + + private customersMapper(customers: ICustomer[]) { + return customers.map(this.customerMapper.bind(this)); + } + + public reportData() { + return this.customersMapper(this.customers); + } + + public reportColumns() { + return []; + } +} diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts new file mode 100644 index 000000000..4582dc77c --- /dev/null +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts @@ -0,0 +1,84 @@ +import { Inject } from 'typedi'; +import moment from 'moment'; +import { groupBy } from 'lodash'; +import TenancyService from 'services/Tenancy/TenancyService'; +import { + ITransactionsByCustomersService, + ITransactionsByCustomersFilter, + ITransactionsByCustomersStatement, +} from 'interfaces'; +import TransactionsByCustomers from './TransactionsByCustomers'; + +export default class TransactionsByCustomersService implements ITransactionsByCustomersService { + @Inject() + tenancy: TenancyService; + + @Inject('logger') + logger: any; + + /** + * Defaults balance sheet filter query. + * @return {ICustomerBalanceSummaryQuery} + */ + get defaultQuery(): ITransactionsByCustomersFilter { + return { + fromDate: moment().format('YYYY-MM-DD'), + toDate: moment().format('YYYY-MM-DD'), + numberFormat: { + precision: 2, + divideOn1000: false, + showZero: false, + formatMoney: 'total', + negativeFormat: 'mines', + }, + comparison: { + percentageOfColumn: true, + }, + noneZero: false, + noneTransactions: false, + }; + } + + /** + * Retrieve transactions by by the customers. + * @param {number} tenantId + * @param {ITransactionsByCustomersFilter} query + * @return {Promise} + */ + public async transactionsByCustomers( + tenantId: number, + query: ITransactionsByCustomersFilter + ): Promise { + const { transactionsRepository } = this.tenancy.repositories(tenantId); + const { Customer } = this.tenancy.models(tenantId); + + // Settings tenant service. + const settings = this.tenancy.settings(tenantId); + const baseCurrency = settings.get({ + group: 'organization', + key: 'base_currency', + }); + + const customers = await Customer.query().orderBy('displayName'); + + // Retrieve all journal transactions based on the given query. + const transactions = await transactionsRepository.journal({ + fromDate: query.fromDate, + toDate: query.toDate, + }); + // Transactions by customers data mapper. + const reportInstance = new TransactionsByCustomers( + customers, + new Map(Object.entries(groupBy(transactions, 'contactId'))), + baseCurrency + ); + const reportData = reportInstance.reportData(); + + const reportColumns = reportInstance.reportColumns(); + + return { + data: reportData, + columns: reportColumns, + }; + } +}