using AccountsDAL.DTO.AccountsReports; using AccountsDAL.Query.AccountReports; using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; namespace AccountsDAL.CustomCode.AccountReports.AccountProfitLoss { public class AccountProfitLossReportDAL : IAccountProfitLossReportDAL { private readonly IQueryExecutor _queryExecutor; public AccountProfitLossReportDAL(IQueryExecutor queryExecutor) { _queryExecutor = queryExecutor; } public async Task GetProfitLossReport( int firstNumber, int maxResult, AccountReportCriteria criteria, List sortBy, LoginDTO loginDTO) { var (filter, parameters) = AccountReportFilterBuilder.Build(criteria, loginDTO); parameters.Add("CurrencyBasis", criteria.CurrencyBasis); parameters.Add("Offset", Math.Max(0, firstNumber - 1)); parameters.Add("PageSize", maxResult); var orderBy = AccountReportSortBuilder.BuildTrialBalanceOrderBy(sortBy, ProfitLossReportQB.DETAIL_DEFAULT_ORDER_BY); var finalSql = ProfitLossReportQB.GET_PROFIT_LOSS_DETAIL.Replace("{DynamicFilter}", filter).Replace("{OrderBy}", orderBy); var paged = await _queryExecutor .QueryPagedAsync(loginDTO, finalSql, parameters) .ConfigureAwait(false); var items = (paged.Items ?? Enumerable.Empty()).ToList(); var totalsSql = ProfitLossReportQB.GET_PROFIT_LOSS_TOTALS.Replace("{DynamicFilter}", filter); var totals = await _queryExecutor .QuerySingleAsync(loginDTO, totalsSql, parameters) .ConfigureAwait(false); var tradingDebit = totals?.TradingDebit ?? 0; var tradingCredit = totals?.TradingCredit ?? 0; var pandLDebit = totals?.PandLDebit ?? 0; var pandLCredit = totals?.PandLCredit ?? 0; var grossProfit = tradingCredit - tradingDebit; var netProfit = grossProfit + (pandLCredit - pandLDebit); InsertSummaryRows(items, grossProfit, netProfit); return new ProfitLossResultDTO { Items = items, TotalCount = paged.TotalCount, TradingDebit = tradingDebit, TradingCredit = tradingCredit, PandLDebit = pandLDebit, PandLCredit = pandLCredit, NetProfit = netProfit }; } // Gross Profit and Net Profit are differences between two subtotals, not sums the generic // grouping/subtotal engine (GB5Shared.Export.GroupTreeBuilder) can derive on its own — same // justification Trial Balance uses for its Retained-Earnings plug row (see // TrialBalanceReportQB.cs class header). Everything else (Total Income/Total Expense per // section, Schedule/Group/Account subtotals) comes free from that engine once the // ISGROUPCOLUMN chain is seeded correctly, so only these two rows need C#-side insertion. // Gross Profit is positioned right after the last Trading-section row (skipped if the period // has no Trading activity at all); Net Profit always closes the whole result set. private static void InsertSummaryRows(List items, decimal grossProfit, decimal netProfit) { var lastTradingIndex = items.FindLastIndex(r => r.Section == "Trading"); if (lastTradingIndex >= 0) { items.Insert(lastTradingIndex + 1, BuildSummaryRow("Trading", "Gross Profit", "GrossProfit", grossProfit)); } items.Add(BuildSummaryRow("P&L", "Net Profit", "NetProfit", netProfit)); } // `net` here is already in "profit-positive" polarity (Credit total - Debit total — see // caller). Every other row in this report uses the opposite, NET_EXPR-native polarity // (Debit-heavy = positive => Debit column; Credit-heavy = negative => Credit column, shown // positive) — same rule every Income row already renders under, since income postings are // credit-heavy. Flip the sign here so a real profit lands in the Credit column alongside the // Income rows it's summarizing, and a loss lands in Debit alongside Expense rows. private static ProfitLossReportDTO BuildSummaryRow(string section, string label, string rowType, decimal net) => new() { Section = section, NatureType = label, RowType = rowType, EffectiveAccountName = label, AccountName = label, Debit = net < 0 ? -net : 0, Credit = net > 0 ? net : 0, }; private class ProfitLossTotalsDTO { public decimal TradingDebit { get; set; } public decimal TradingCredit { get; set; } public decimal PandLDebit { get; set; } public decimal PandLCredit { get; set; } } } }