Distribution_GL Initialization
This commit is contained in:
@@ -0,0 +1,437 @@
|
||||
using General_Ledger_Service.DTOs.Common;
|
||||
using General_Ledger_Service.DTOs.Reports;
|
||||
using General_Ledger_Service.Enums;
|
||||
using General_Ledger_Service.Exceptions;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Interfaces.Services;
|
||||
using General_Ledger_Service.Services.CsvDocuments;
|
||||
using General_Ledger_Service.Services.PdfDocuments;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace General_Ledger_Service.Services
|
||||
{
|
||||
public class ReportService(
|
||||
IAccountRepository accountRepository,
|
||||
IJournalEntryRepository journalEntryRepository,
|
||||
IBudgetRepository budgetRepository,
|
||||
ITaxRepository taxRepository,
|
||||
IFixedAssetRepository fixedAssetRepository,
|
||||
ISystemConfigRepository systemConfigRepository,
|
||||
ICurrencyRepository currencyRepository,
|
||||
IConfiguration configuration) : IReportService
|
||||
{
|
||||
private readonly IAccountRepository _accountRepository = accountRepository;
|
||||
private readonly IJournalEntryRepository _journalEntryRepository = journalEntryRepository;
|
||||
private readonly IBudgetRepository _budgetRepository = budgetRepository;
|
||||
private readonly ITaxRepository _taxRepository = taxRepository;
|
||||
private readonly IFixedAssetRepository _fixedAssetRepository = fixedAssetRepository;
|
||||
private readonly ISystemConfigRepository _systemConfigRepository = systemConfigRepository;
|
||||
private readonly ICurrencyRepository _currencyRepository = currencyRepository;
|
||||
private readonly IConfiguration _configuration = configuration;
|
||||
|
||||
public async Task<ApiResponse> GenerateAsync(ListReportsRequest request)
|
||||
{
|
||||
if (!Enum.TryParse<ReportType>(request.ReportType, ignoreCase: true, out var reportType))
|
||||
{
|
||||
throw new BadRequestException(
|
||||
$"Invalid reportType '{request.ReportType}'. Must be one of: {string.Join(", ", Enum.GetNames<ReportType>())}.");
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<ReportOutputFormat>(request.OutputFormat, ignoreCase: true, out var outputFormat))
|
||||
{
|
||||
throw new BadRequestException(
|
||||
$"Invalid outputFormat '{request.OutputFormat}'. Must be one of: {string.Join(", ", Enum.GetNames<ReportOutputFormat>())}.");
|
||||
}
|
||||
|
||||
object data = reportType switch
|
||||
{
|
||||
ReportType.GeneralLedger => await BuildGeneralLedgerAsync(request),
|
||||
ReportType.TrialBalance => await BuildTrialBalanceAsync(request),
|
||||
ReportType.BalanceSheet => await BuildBalanceSheetAsync(request),
|
||||
ReportType.ProfitAndLoss => await BuildProfitAndLossAsync(request),
|
||||
ReportType.CashFlow => await BuildCashFlowAsync(request),
|
||||
ReportType.BudgetVsActual => await BuildBudgetVsActualAsync(request),
|
||||
ReportType.TaxSummary => await BuildTaxSummaryAsync(request),
|
||||
_ => throw new BadRequestException($"Unsupported reportType '{reportType}'.")
|
||||
};
|
||||
|
||||
if (outputFormat == ReportOutputFormat.Pdf)
|
||||
{
|
||||
var pdfBytes = await RenderPdfAsync(reportType, request, data);
|
||||
return ResponseHelpers.Ok("Report generated successfully", new
|
||||
{
|
||||
fileName = $"{reportType}.pdf",
|
||||
contentType = "application/pdf",
|
||||
contentBase64 = Convert.ToBase64String(pdfBytes)
|
||||
});
|
||||
}
|
||||
|
||||
if (outputFormat == ReportOutputFormat.Csv)
|
||||
{
|
||||
var csvBytes = RenderCsv(reportType, data);
|
||||
return ResponseHelpers.Ok("Report generated successfully", new
|
||||
{
|
||||
fileName = $"{reportType}.csv",
|
||||
contentType = "text/csv",
|
||||
contentBase64 = Convert.ToBase64String(csvBytes)
|
||||
});
|
||||
}
|
||||
|
||||
return ResponseHelpers.Ok("Report generated successfully", data);
|
||||
}
|
||||
|
||||
// 05_LKAS_Report_PDF_Templates.md Section 10 - one writer per report, same flattening
|
||||
// approach as the PDF layouts (a Section/Category column standing in for what the PDF
|
||||
// shows via headers/indentation), sharing CsvBuilder for row-building/escaping.
|
||||
private static byte[] RenderCsv(ReportType reportType, object data) => reportType switch
|
||||
{
|
||||
ReportType.TrialBalance => TrialBalanceCsvDocument.Generate((List<TrialBalanceRowDto>)data),
|
||||
ReportType.BalanceSheet => BalanceSheetCsvDocument.Generate((BalanceSheetReportDto)data),
|
||||
ReportType.ProfitAndLoss => ProfitAndLossCsvDocument.Generate((ProfitAndLossReportDto)data),
|
||||
ReportType.CashFlow => CashFlowCsvDocument.Generate((CashFlowReportDto)data),
|
||||
ReportType.GeneralLedger => GeneralLedgerCsvDocument.Generate((List<GeneralLedgerLineDto>)data),
|
||||
ReportType.BudgetVsActual => BudgetVsActualCsvDocument.Generate((List<BudgetVsActualRowDto>)data),
|
||||
ReportType.TaxSummary => IncomeTaxComputationCsvDocument.Generate((IncomeTaxComputationDto)data),
|
||||
_ => throw new BadRequestException($"CSV output for reportType '{reportType}' is not implemented yet.")
|
||||
};
|
||||
|
||||
// One bespoke QuestPDF IDocument per report (05_LKAS_Report_PDF_Templates.md), sharing
|
||||
// one header/footer component (ReportPageHeader) - replaces the old single generic
|
||||
// reflection-based renderer. `request`'s already-validated period/date fields are reused
|
||||
// as-is (the corresponding Build*Async call above already proved them non-null).
|
||||
private async Task<byte[]> RenderPdfAsync(ReportType reportType, ListReportsRequest request, object data)
|
||||
{
|
||||
var companyName = _configuration["Company:Name"] ?? "Company Name Not Configured";
|
||||
var currencyCode = await _currencyRepository.GetBaseCurrencyCodeAsync();
|
||||
|
||||
IDocument document = reportType switch
|
||||
{
|
||||
ReportType.TrialBalance => new TrialBalancePdfDocument(companyName, currencyCode, request.AsOfDate!.Value, (List<TrialBalanceRowDto>)data),
|
||||
ReportType.BudgetVsActual => new BudgetVsActualPdfDocument(companyName, currencyCode, request.BudgetId!.Value, (List<BudgetVsActualRowDto>)data),
|
||||
ReportType.TaxSummary => new IncomeTaxComputationPdfDocument(
|
||||
companyName,
|
||||
_configuration["Company:Address"] ?? "",
|
||||
_configuration["Company:Tin"] ?? "",
|
||||
_configuration["Company:Brn"] ?? "",
|
||||
currencyCode,
|
||||
(IncomeTaxComputationDto)data),
|
||||
ReportType.GeneralLedger => new GeneralLedgerPdfDocument(companyName, currencyCode, request.PeriodStart!.Value, request.PeriodEnd!.Value, (List<GeneralLedgerLineDto>)data),
|
||||
ReportType.BalanceSheet => new BalanceSheetPdfDocument(companyName, currencyCode, request.AsOfDate!.Value, (BalanceSheetReportDto)data),
|
||||
ReportType.ProfitAndLoss => new ProfitAndLossPdfDocument(companyName, currencyCode, (ProfitAndLossReportDto)data),
|
||||
ReportType.CashFlow => new CashFlowPdfDocument(companyName, currencyCode, (CashFlowReportDto)data),
|
||||
_ => throw new BadRequestException($"PDF output for reportType '{reportType}' is not implemented yet.")
|
||||
};
|
||||
|
||||
return document.GeneratePdf();
|
||||
}
|
||||
|
||||
private async Task<List<GeneralLedgerLineDto>> BuildGeneralLedgerAsync(ListReportsRequest request)
|
||||
{
|
||||
var periodStart = request.PeriodStart ?? throw new BadRequestException("periodStart is required for the GeneralLedger report");
|
||||
var periodEnd = request.PeriodEnd ?? throw new BadRequestException("periodEnd is required for the GeneralLedger report");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.AccountCode))
|
||||
{
|
||||
return await _journalEntryRepository.GetGeneralLedgerLinesForAllAccountsAsync(periodStart, periodEnd);
|
||||
}
|
||||
|
||||
var account = await _accountRepository.GetByCodeAsync(request.AccountCode) ?? throw new NotFoundException("Account not found");
|
||||
|
||||
return await _journalEntryRepository.GetGeneralLedgerLinesAsync(account.AccountId, periodStart, periodEnd);
|
||||
}
|
||||
|
||||
private async Task<List<TrialBalanceRowDto>> BuildTrialBalanceAsync(ListReportsRequest request)
|
||||
{
|
||||
var asOfDate = request.AsOfDate ?? throw new BadRequestException("asOfDate is required for the TrialBalance report");
|
||||
return await _accountRepository.GetTrialBalanceAsync(asOfDate);
|
||||
}
|
||||
|
||||
// Retrofit 2026-07-31 - Accounting_System_Design_LKAS.md Section 11.3. Restructured around
|
||||
// accounts.balance_sheet_classification (same recursive-tag pattern as cash_flow_category/
|
||||
// income_statement_section), replacing the old flat recursive-rollup array with a nested
|
||||
// Non-Current/Current Assets, Equity, Non-Current/Current Liabilities shape - a
|
||||
// user-supplied reference template (a classified LKAS 1 Statement of Financial Position)
|
||||
// needs that split and it isn't derivable from account_type_id alone. Untagged leaf
|
||||
// Asset/Liability rows land in their own "unclassified" bucket rather than being silently
|
||||
// dropped, same rule ProfitAndLoss's Unclassified section already established.
|
||||
private async Task<BalanceSheetReportDto> BuildBalanceSheetAsync(ListReportsRequest request)
|
||||
{
|
||||
var asOfDate = request.AsOfDate ?? throw new BadRequestException("asOfDate is required for the BalanceSheet report");
|
||||
var leafRows = await _accountRepository.GetBalanceSheetLeafRowsAsync(asOfDate);
|
||||
|
||||
static BalanceSheetSectionDto BuildSection(IEnumerable<BalanceSheetLeafRowDto> rows)
|
||||
{
|
||||
var lines = rows.Select(r => new BalanceSheetLineDto { AccountCode = r.AccountCode, AccountName = r.AccountName, Balance = r.Balance }).ToList();
|
||||
return new BalanceSheetSectionDto { Lines = lines, Total = lines.Sum(l => l.Balance) };
|
||||
}
|
||||
|
||||
var assetRows = leafRows.Where(r => r.AccountTypeId == 1).ToList();
|
||||
var liabilityRows = leafRows.Where(r => r.AccountTypeId == 2).ToList();
|
||||
var equityRows = leafRows.Where(r => r.AccountTypeId == 3).ToList();
|
||||
|
||||
var nonCurrentAssets = BuildSection(assetRows.Where(r => r.BalanceSheetClassification == "NON_CURRENT_ASSET"));
|
||||
var currentAssets = BuildSection(assetRows.Where(r => r.BalanceSheetClassification == "CURRENT_ASSET"));
|
||||
var unclassifiedAssets = BuildSection(assetRows.Where(r => r.BalanceSheetClassification is not ("NON_CURRENT_ASSET" or "CURRENT_ASSET")));
|
||||
|
||||
var nonCurrentLiabilities = BuildSection(liabilityRows.Where(r => r.BalanceSheetClassification == "NON_CURRENT_LIABILITY"));
|
||||
var currentLiabilities = BuildSection(liabilityRows.Where(r => r.BalanceSheetClassification == "CURRENT_LIABILITY"));
|
||||
var unclassifiedLiabilities = BuildSection(liabilityRows.Where(r => r.BalanceSheetClassification is not ("NON_CURRENT_LIABILITY" or "CURRENT_LIABILITY")));
|
||||
|
||||
var equity = BuildSection(equityRows);
|
||||
|
||||
// No closing-entry mechanism exists anywhere in this codebase - a fiscal year's
|
||||
// Income/Expense activity never gets moved into Equity by any journal entry. So
|
||||
// "Current Year Earnings" has to be cumulative since inception (DateOnly.MinValue),
|
||||
// not scoped to the fiscal year containing asOfDate, or the equation below breaks
|
||||
// again the moment a second fiscal year has any posted activity. Confirmed with the
|
||||
// project owner 2026-07-20 - see CLAUDE.md/03_Progress_Tracker.md. Folded into the
|
||||
// Equity section as its own line (retrofit 2026-07-31) rather than a separate
|
||||
// top-level field, since it's conceptually an Equity line item.
|
||||
var profitAndLoss = await BuildProfitAndLossReportAsync(DateOnly.MinValue, asOfDate);
|
||||
equity.Lines.Add(new BalanceSheetLineDto { AccountCode = "", AccountName = "Current Year Earnings", Balance = profitAndLoss.NetProfitForPeriod });
|
||||
equity.Total += profitAndLoss.NetProfitForPeriod;
|
||||
|
||||
var totalAssets = nonCurrentAssets.Total + currentAssets.Total + unclassifiedAssets.Total;
|
||||
var totalEquityAndLiabilities = equity.Total + nonCurrentLiabilities.Total + currentLiabilities.Total + unclassifiedLiabilities.Total;
|
||||
|
||||
return new BalanceSheetReportDto
|
||||
{
|
||||
AsOfDate = asOfDate,
|
||||
NonCurrentAssets = nonCurrentAssets,
|
||||
CurrentAssets = currentAssets,
|
||||
UnclassifiedAssets = unclassifiedAssets,
|
||||
TotalAssets = totalAssets,
|
||||
Equity = equity,
|
||||
NonCurrentLiabilities = nonCurrentLiabilities,
|
||||
CurrentLiabilities = currentLiabilities,
|
||||
UnclassifiedLiabilities = unclassifiedLiabilities,
|
||||
TotalEquityAndLiabilities = totalEquityAndLiabilities
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ProfitAndLossReportDto> BuildProfitAndLossAsync(ListReportsRequest request)
|
||||
{
|
||||
var periodStart = request.PeriodStart ?? throw new BadRequestException("periodStart is required for the ProfitAndLoss report");
|
||||
var periodEnd = request.PeriodEnd ?? throw new BadRequestException("periodEnd is required for the ProfitAndLoss report");
|
||||
return await BuildProfitAndLossReportAsync(periodStart, periodEnd);
|
||||
}
|
||||
|
||||
// Retrofit 2026-07-22 - Accounting_System_Design_LKAS.md Section 11.4. Shared by the
|
||||
// ProfitAndLoss report itself, BalanceSheet's "Current Year Earnings" row, and CashFlow's
|
||||
// profitForPeriod - all three just need the resulting NetProfitForPeriod figure, computed
|
||||
// once here from the raw leaf rows so the Expense-side sign convention lives in exactly
|
||||
// one place.
|
||||
private async Task<ProfitAndLossReportDto> BuildProfitAndLossReportAsync(DateOnly periodStart, DateOnly periodEnd)
|
||||
{
|
||||
var leafRows = await _accountRepository.GetProfitAndLossLeafRowsAsync(periodStart, periodEnd);
|
||||
|
||||
// Raw Amount (credit - debit) is already positive-normal for Income (account_type_id
|
||||
// 4) but negative-normal for Expense (account_type_id 5, debit-normal) - flip Expense
|
||||
// rows to positive so every expense-side section/total reads as a positive figure,
|
||||
// per Accounting_System_Design_LKAS.md Section 11.7's explicit sign-convention note
|
||||
// (the same net_movement formula is shared between the P&L and Income Tax Computation
|
||||
// queries). Unclassified rows keep their raw (already net-correct) sign so summing
|
||||
// them straight into NetProfitForPeriod doesn't need a second special case.
|
||||
ProfitAndLossSectionDto BuildSection(string section, bool isExpense) =>
|
||||
BuildSectionFromRows(leafRows.Where(r => r.IncomeStatementSection == section), isExpense);
|
||||
|
||||
var sales = BuildSection("SALES", isExpense: false);
|
||||
var costOfSales = BuildSection("COST_OF_SALES", isExpense: true);
|
||||
var grossProfit = sales.Total - costOfSales.Total;
|
||||
|
||||
var otherIncome = BuildSection("OTHER_INCOME", isExpense: false);
|
||||
var distributionExpenses = BuildSection("DISTRIBUTION_EXPENSE", isExpense: true);
|
||||
var administrationExpenses = BuildSection("ADMINISTRATION_EXPENSE", isExpense: true);
|
||||
var otherExpenses = BuildSection("OTHER_EXPENSE", isExpense: true);
|
||||
var financialExpenses = BuildSection("FINANCIAL_EXPENSE", isExpense: true);
|
||||
var totalExpenses = distributionExpenses.Total + administrationExpenses.Total + otherExpenses.Total + financialExpenses.Total;
|
||||
|
||||
var unclassifiedRows = leafRows.Where(r => r.IncomeStatementSection is null).ToList();
|
||||
var unclassified = new ProfitAndLossSectionDto
|
||||
{
|
||||
Lines = [.. unclassifiedRows.Select(r => new ProfitAndLossLineDto { AccountCode = r.AccountCode, AccountName = r.AccountName, Amount = r.Amount })],
|
||||
Total = unclassifiedRows.Sum(r => r.Amount)
|
||||
};
|
||||
|
||||
var netProfitForPeriod = grossProfit + otherIncome.Total - totalExpenses + unclassified.Total;
|
||||
|
||||
return new ProfitAndLossReportDto
|
||||
{
|
||||
PeriodStart = periodStart,
|
||||
PeriodEnd = periodEnd,
|
||||
Sales = sales,
|
||||
CostOfSales = costOfSales,
|
||||
GrossProfit = grossProfit,
|
||||
OtherIncome = otherIncome,
|
||||
DistributionExpenses = distributionExpenses,
|
||||
AdministrationExpenses = administrationExpenses,
|
||||
OtherExpenses = otherExpenses,
|
||||
FinancialExpenses = financialExpenses,
|
||||
Unclassified = unclassified,
|
||||
NetProfitForPeriod = netProfitForPeriod
|
||||
};
|
||||
}
|
||||
|
||||
private static ProfitAndLossSectionDto BuildSectionFromRows(IEnumerable<ProfitAndLossLeafRowDto> rows, bool isExpense)
|
||||
{
|
||||
var lines = rows.Select(r => new ProfitAndLossLineDto
|
||||
{
|
||||
AccountCode = r.AccountCode,
|
||||
AccountName = r.AccountName,
|
||||
Amount = isExpense ? -r.Amount : r.Amount
|
||||
}).ToList();
|
||||
|
||||
return new ProfitAndLossSectionDto { Lines = lines, Total = lines.Sum(l => l.Amount) };
|
||||
}
|
||||
|
||||
// Full LKAS 7 indirect-method cash flow - Accounting_System_Design_LKAS.md Section 11.5.
|
||||
private async Task<CashFlowReportDto> BuildCashFlowAsync(ListReportsRequest request)
|
||||
{
|
||||
var periodStart = request.PeriodStart ?? throw new BadRequestException("periodStart is required for the CashFlow report");
|
||||
var periodEnd = request.PeriodEnd ?? throw new BadRequestException("periodEnd is required for the CashFlow report");
|
||||
|
||||
var cashAccountRootConfig = await _systemConfigRepository.GetByKeyAsync("CASH_ACCOUNT_ROOT_ID");
|
||||
if (cashAccountRootConfig is null || !long.TryParse(cashAccountRootConfig.ConfigValue, out var cashAccountRootId))
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"CASH_ACCOUNT_ROOT_ID is not configured yet. Set it via PUT /api/v1/system-config/CASH_ACCOUNT_ROOT_ID to the account_id of your Cash and Cash Equivalents COA header before running the CashFlow report.");
|
||||
}
|
||||
|
||||
var profitAndLoss = await BuildProfitAndLossReportAsync(periodStart, periodEnd);
|
||||
var profitForPeriod = profitAndLoss.NetProfitForPeriod;
|
||||
|
||||
var depreciationAddBack = await _fixedAssetRepository.GetDepreciationAddBackAsync(periodStart, periodEnd);
|
||||
|
||||
var nonCashAdjustments = new List<CashFlowNonCashAdjustmentDto>();
|
||||
if (depreciationAddBack != 0)
|
||||
{
|
||||
nonCashAdjustments.Add(new CashFlowNonCashAdjustmentDto { Description = "Depreciation", Amount = depreciationAddBack });
|
||||
}
|
||||
|
||||
var workingCapitalChanges = await _accountRepository.GetCashFlowWorkingCapitalChangesAsync(periodStart, periodEnd, cashAccountRootId);
|
||||
var netCashFromOperating = profitForPeriod + depreciationAddBack + workingCapitalChanges.Sum(c => c.ChangeAmount);
|
||||
|
||||
var investingLines = await _accountRepository.GetCashFlowCategoryLinesAsync("INVESTING", periodStart, periodEnd, cashAccountRootId);
|
||||
var netInvesting = investingLines.Sum(l => l.Amount);
|
||||
|
||||
var financingLines = await _accountRepository.GetCashFlowCategoryLinesAsync("FINANCING", periodStart, periodEnd, cashAccountRootId);
|
||||
var netFinancing = financingLines.Sum(l => l.Amount);
|
||||
|
||||
var netIncreaseDecrease = netCashFromOperating + netInvesting + netFinancing;
|
||||
|
||||
var openingCashBalance = await _accountRepository.GetCashBalanceAsync(periodStart.AddDays(-1), cashAccountRootId);
|
||||
var closingCashBalance = await _accountRepository.GetCashBalanceAsync(periodEnd, cashAccountRootId);
|
||||
|
||||
return new CashFlowReportDto
|
||||
{
|
||||
PeriodStart = periodStart,
|
||||
PeriodEnd = periodEnd,
|
||||
OperatingActivities = new CashFlowOperatingActivitiesDto
|
||||
{
|
||||
ProfitForPeriod = profitForPeriod,
|
||||
NonCashAdjustments = nonCashAdjustments,
|
||||
WorkingCapitalChanges = workingCapitalChanges,
|
||||
NetCashFromOperatingActivities = netCashFromOperating
|
||||
},
|
||||
InvestingActivities = new CashFlowInvestingActivitiesDto
|
||||
{
|
||||
Lines = investingLines,
|
||||
NetCashFromInvestingActivities = netInvesting
|
||||
},
|
||||
FinancingActivities = new CashFlowFinancingActivitiesDto
|
||||
{
|
||||
Lines = financingLines,
|
||||
NetCashFromFinancingActivities = netFinancing
|
||||
},
|
||||
NetIncreaseDecreaseInCash = netIncreaseDecrease,
|
||||
OpeningCashBalance = openingCashBalance,
|
||||
ClosingCashBalance = closingCashBalance
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<BudgetVsActualRowDto>> BuildBudgetVsActualAsync(ListReportsRequest request)
|
||||
{
|
||||
var budgetId = request.BudgetId ?? throw new BadRequestException("budgetId is required for the BudgetVsActual report");
|
||||
_ = await _budgetRepository.GetByIdAsync(budgetId) ?? throw new NotFoundException("Budget not found");
|
||||
return await _budgetRepository.GetBudgetVsActualAsync(budgetId);
|
||||
}
|
||||
|
||||
// Retrofit 2026-07-22 - Accounting_System_Design_LKAS.md Section 11.7. Full corporate
|
||||
// Income Tax Computation (current tax only, no deferred tax), replacing the 2026-07-21
|
||||
// VAT/WHT/NBT per-tax_code summary wholesale under the same TaxSummary reportType name.
|
||||
private async Task<IncomeTaxComputationDto> BuildTaxSummaryAsync(ListReportsRequest request)
|
||||
{
|
||||
var periodStart = request.PeriodStart ?? throw new BadRequestException("periodStart is required for the TaxSummary report");
|
||||
var periodEnd = request.PeriodEnd ?? throw new BadRequestException("periodEnd is required for the TaxSummary report");
|
||||
|
||||
var profitAndLoss = await BuildProfitAndLossReportAsync(periodStart, periodEnd);
|
||||
var profitBeforeTax = profitAndLoss.NetProfitForPeriod;
|
||||
|
||||
var nonDeductibleExpenses = await _taxRepository.GetNonDeductibleExpensesAsync(periodStart, periodEnd);
|
||||
var allowableDeductions = request.AllowableDeductions ?? 0;
|
||||
var adjustedBusinessProfit = profitBeforeTax + nonDeductibleExpenses - allowableDeductions;
|
||||
|
||||
var otherTaxableIncome = request.OtherTaxableIncome ?? 0;
|
||||
var assessableIncome = adjustedBusinessProfit + otherTaxableIncome;
|
||||
|
||||
var qualifyingPaymentsReliefs = request.QualifyingPaymentsReliefs ?? await GetConfigDecimalAsync("STATUTORY_RELIEF_AMOUNT", 0);
|
||||
var taxableIncome = assessableIncome - qualifyingPaymentsReliefs;
|
||||
|
||||
var taxRatePercent = request.TaxRateOverride ?? await GetConfigDecimalAsync("CORPORATE_INCOME_TAX_RATE", 30);
|
||||
var corporateIncomeTax = taxableIncome * taxRatePercent / 100;
|
||||
|
||||
var surchargeAmount = request.SurchargeAmount ?? 0;
|
||||
var grossTaxLiability = corporateIncomeTax + surchargeAmount;
|
||||
|
||||
// Unlike CASH_ACCOUNT_ROOT_ID, an unset credit account isn't a hard failure - a
|
||||
// company that simply has no APIT/WHT/quarterly-payment activity yet (or hasn't
|
||||
// configured the account) just gets a 0 credit, not a blocked report.
|
||||
var apitCredit = await GetConfiguredAccountCreditAsync("APIT_CREDIT_ACCOUNT_ID", periodStart, periodEnd);
|
||||
var whtCredit = await GetConfiguredAccountCreditAsync("WHT_CREDIT_ACCOUNT_ID", periodStart, periodEnd);
|
||||
var quarterlyTaxPayments = await GetConfiguredAccountCreditAsync("QUARTERLY_TAX_PAYMENT_ACCOUNT_ID", periodStart, periodEnd);
|
||||
|
||||
var balanceTaxPayable = grossTaxLiability - apitCredit - whtCredit - quarterlyTaxPayments;
|
||||
|
||||
return new IncomeTaxComputationDto
|
||||
{
|
||||
PeriodStart = periodStart,
|
||||
PeriodEnd = periodEnd,
|
||||
ProfitBeforeTax = profitBeforeTax,
|
||||
NonDeductibleExpenses = nonDeductibleExpenses,
|
||||
AllowableDeductions = allowableDeductions,
|
||||
AdjustedBusinessProfit = adjustedBusinessProfit,
|
||||
OtherTaxableIncome = otherTaxableIncome,
|
||||
AssessableIncome = assessableIncome,
|
||||
QualifyingPaymentsReliefs = qualifyingPaymentsReliefs,
|
||||
TaxableIncome = taxableIncome,
|
||||
TaxRatePercent = taxRatePercent,
|
||||
CorporateIncomeTax = corporateIncomeTax,
|
||||
SurchargeAmount = surchargeAmount,
|
||||
GrossTaxLiability = grossTaxLiability,
|
||||
ApitCredit = apitCredit,
|
||||
WhtCredit = whtCredit,
|
||||
QuarterlyTaxPayments = quarterlyTaxPayments,
|
||||
BalanceTaxPayable = balanceTaxPayable
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<decimal> GetConfigDecimalAsync(string key, decimal fallback)
|
||||
{
|
||||
var config = await _systemConfigRepository.GetByKeyAsync(key);
|
||||
return config is not null && decimal.TryParse(config.ConfigValue, out var value) ? value : fallback;
|
||||
}
|
||||
|
||||
private async Task<decimal> GetConfiguredAccountCreditAsync(string configKey, DateOnly periodStart, DateOnly periodEnd)
|
||||
{
|
||||
var config = await _systemConfigRepository.GetByKeyAsync(configKey);
|
||||
if (config is null || !long.TryParse(config.ConfigValue, out var accountId))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return await _taxRepository.GetAccountCreditActivityAsync(accountId, periodStart, periodEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user