Distribution_GL Initialization
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
using General_Ledger_Service.DTOs.Accounts;
|
||||
using General_Ledger_Service.DTOs.Reports;
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class AccountRepository(AppDBContext context) : IAccountRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public Task<Account?> GetByIdAsync(long accountId) =>
|
||||
RepositoryHelpers.GetByIdAsync(_context.Accounts, accountId);
|
||||
|
||||
public async Task<Account?> GetByCodeAsync(string accountCode) =>
|
||||
await _context.Accounts.FirstOrDefaultAsync(a => a.AccountCode == accountCode);
|
||||
|
||||
// Used by BankService's auto-created "Bank"/"Cash" GL hierarchy (2026-07-31) to find an
|
||||
// existing system header/type node by its fixed name before creating a new one -
|
||||
// accountName is always a fixed literal here ("Bank", "Cash", a CashAccountType.Name),
|
||||
// never end-user free text, so a name collision would only ever come from the same
|
||||
// auto-creation logic itself, not an unrelated manually-created account.
|
||||
public async Task<Account?> GetByNameAndParentAsync(string accountName, long? parentAccountId) =>
|
||||
await _context.Accounts.FirstOrDefaultAsync(a => a.AccountName == accountName && a.ParentAccountId == parentAccountId);
|
||||
|
||||
public async Task<int> GetAncestorDepthAsync(long parentAccountId) =>
|
||||
RecursiveTreeQueries.GetAncestorDepth(await LoadTreeRowsAsync(), parentAccountId);
|
||||
|
||||
public async Task<string?> GetAncestorBreadcrumbAsync(long accountId) =>
|
||||
RecursiveTreeQueries.GetAncestorBreadcrumb(await LoadTreeRowsAsync(), accountId);
|
||||
|
||||
public async Task<List<AccountTreeNode>> GetDescendantTreeAsync(long accountId)
|
||||
{
|
||||
var rows = RecursiveTreeQueries.GetDescendantTree(await LoadTreeRowsAsync(), accountId);
|
||||
return [.. rows.Select(r => new AccountTreeNode
|
||||
{
|
||||
AccountId = r.Id,
|
||||
AccountCode = r.Code,
|
||||
AccountName = r.Name,
|
||||
ParentAccountId = r.ParentId,
|
||||
Depth = r.Depth
|
||||
})];
|
||||
}
|
||||
|
||||
public async Task<bool> WouldCreateCycleAsync(long accountId, long newParentId) =>
|
||||
RecursiveTreeQueries.WouldCreateCycle(await LoadTreeRowsAsync(), accountId, newParentId);
|
||||
|
||||
public async Task<(List<Account> Items, int TotalCount)> SearchAsync(short? accountTypeId, bool? isActive, bool? isPostable, int? page, int? pageSize)
|
||||
{
|
||||
var query = _context.Accounts.AsQueryable();
|
||||
|
||||
if (accountTypeId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.AccountTypeId == accountTypeId.Value);
|
||||
}
|
||||
|
||||
if (isActive.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.IsActive == isActive.Value);
|
||||
}
|
||||
|
||||
if (isPostable.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.IsPostable == isPostable.Value);
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
var items = await RepositoryHelpers.ApplyPaging(query.OrderBy(a => a.AccountCode), page, pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public Task<Account> AddAsync(Account account) =>
|
||||
RepositoryHelpers.AddAsync(_context.Accounts, account);
|
||||
|
||||
|
||||
// Retrofit 2026-07-22 - flat list, no depth/indentation (05_LKAS_Report_PDF_Templates.md
|
||||
// Section 2): a plain postable-leaf-account listing needs no recursive tree at all.
|
||||
// Fix 2026-10-20: each row shows the account's NET balance in exactly one column (debit
|
||||
// balance -> Debit, credit balance -> Credit, the other stays 0 and renders blank), and an
|
||||
// account whose debits and credits cancel out (net zero) is left out entirely - per
|
||||
// 05_LKAS_Report_PDF_Templates.md Section 2. Previously this returned the gross debit and
|
||||
// credit totals side by side, so any account with movement on both sides (or an entry and
|
||||
// its reversal) showed equal/duplicated figures in both columns.
|
||||
public async Task<List<TrialBalanceRowDto>> GetTrialBalanceAsync(DateOnly asOfDate)
|
||||
{
|
||||
var totals = await GetPostedTotalsAsync(null, asOfDate);
|
||||
var postableAccounts = await _context.Accounts.AsNoTracking().Where(a => a.IsPostable).ToListAsync();
|
||||
|
||||
return [.. postableAccounts
|
||||
.Where(a => totals.TryGetValue(a.AccountId, out var t) && t.Debit != t.Credit)
|
||||
.OrderBy(a => a.AccountCode, StringComparer.Ordinal)
|
||||
.Select(a =>
|
||||
{
|
||||
var net = totals[a.AccountId].Debit - totals[a.AccountId].Credit;
|
||||
return new TrialBalanceRowDto
|
||||
{
|
||||
AccountCode = a.AccountCode,
|
||||
AccountName = a.AccountName,
|
||||
Debit = net > 0 ? net : 0,
|
||||
Credit = net < 0 ? -net : 0
|
||||
};
|
||||
})];
|
||||
}
|
||||
|
||||
// Retrofit 2026-07-31 - Accounting_System_Design_LKAS.md Section 11.3. Leaf/postable rows
|
||||
// only, tagged with balance_sheet_classification inherited down the tree (same
|
||||
// recursive-tag pattern as cash_flow_category/income_statement_section); ReportService
|
||||
// groups these into the nested BalanceSheetReportDto (Non-Current/Current Assets,
|
||||
// Equity, Non-Current/Current Liabilities) and computes section/grand totals. Balance is
|
||||
// already sign-adjusted per account type: Asset is debit-normal (debit - credit);
|
||||
// Liability/Equity are credit-normal, so they use the opposite sign (credit - debit) to
|
||||
// show as conventional positive balances too - the same flip the P&L applies.
|
||||
public async Task<List<BalanceSheetLeafRowDto>> GetBalanceSheetLeafRowsAsync(DateOnly asOfDate)
|
||||
{
|
||||
var accounts = await _context.Accounts.AsNoTracking().ToListAsync();
|
||||
var roots = accounts.Where(a => a.ParentAccountId == null && (a.AccountTypeId == 1 || a.AccountTypeId == 2 || a.AccountTypeId == 3));
|
||||
var classification = InheritTag(accounts, roots, a => a.BalanceSheetClassification);
|
||||
var totals = await GetPostedTotalsAsync(null, asOfDate);
|
||||
|
||||
return [.. accounts
|
||||
.Where(a => a.IsPostable && classification.ContainsKey(a.AccountId) && totals.ContainsKey(a.AccountId))
|
||||
.Select(a => new BalanceSheetLeafRowDto
|
||||
{
|
||||
AccountCode = a.AccountCode,
|
||||
AccountName = a.AccountName,
|
||||
AccountTypeId = a.AccountTypeId,
|
||||
BalanceSheetClassification = classification[a.AccountId],
|
||||
Balance = SignedBalance(a.AccountTypeId, totals[a.AccountId])
|
||||
})
|
||||
.Where(r => r.Balance != 0)
|
||||
.OrderBy(r => r.AccountCode, StringComparer.Ordinal)];
|
||||
}
|
||||
|
||||
// Retrofit 2026-07-22 - Accounting_System_Design_LKAS.md Section 11.4. Leaf/postable rows
|
||||
// only, tagged with income_statement_section inherited down the tree (same recursive-tag
|
||||
// pattern as cash_flow_category); ReportService groups these into the nested
|
||||
// ProfitAndLossReportDto and applies the Expense-side sign flip (Amount here is the raw,
|
||||
// unflipped credit - debit - negative for a normal debit-balance Expense account).
|
||||
public async Task<List<ProfitAndLossLeafRowDto>> GetProfitAndLossLeafRowsAsync(DateOnly periodStart, DateOnly periodEnd)
|
||||
{
|
||||
var accounts = await _context.Accounts.AsNoTracking().ToListAsync();
|
||||
var roots = accounts.Where(a => a.ParentAccountId == null && (a.AccountTypeId == 4 || a.AccountTypeId == 5));
|
||||
var section = InheritTag(accounts, roots, a => a.IncomeStatementSection);
|
||||
var totals = await GetPostedTotalsAsync(periodStart, periodEnd);
|
||||
|
||||
return [.. accounts
|
||||
.Where(a => a.IsPostable && section.ContainsKey(a.AccountId) && totals.ContainsKey(a.AccountId))
|
||||
.Select(a => new ProfitAndLossLeafRowDto
|
||||
{
|
||||
AccountCode = a.AccountCode,
|
||||
AccountName = a.AccountName,
|
||||
AccountTypeId = a.AccountTypeId,
|
||||
IncomeStatementSection = section[a.AccountId],
|
||||
Amount = totals[a.AccountId].Credit - totals[a.AccountId].Debit
|
||||
})
|
||||
.Where(r => r.Amount != 0)
|
||||
.OrderBy(r => r.AccountCode, StringComparer.Ordinal)];
|
||||
}
|
||||
|
||||
// Accounting_System_Design_LKAS.md Section 11.5 - "cash-like" accounts are the recursive
|
||||
// descendants of the configured CASH_ACCOUNT_ROOT_ID system_config value (resolved and
|
||||
// validated by ReportService before this is ever called - a plain long here, so an
|
||||
// unset/invalid config fails with a clear error before reaching any query).
|
||||
public async Task<List<CashFlowWorkingCapitalChangeDto>> GetCashFlowWorkingCapitalChangesAsync(DateOnly periodStart, DateOnly periodEnd, long cashAccountRootId)
|
||||
{
|
||||
var accounts = await _context.Accounts.AsNoTracking().ToListAsync();
|
||||
var cashAccountIds = RecursiveTreeQueries.GetDescendantIds(ToTreeRows(accounts), cashAccountRootId);
|
||||
var category = InheritTag(accounts, accounts.Where(a => a.ParentAccountId == null), a => a.CashFlowCategory);
|
||||
var opening = await GetPostedTotalsAsync(null, periodStart.AddDays(-1));
|
||||
var closing = await GetPostedTotalsAsync(null, periodEnd);
|
||||
|
||||
var rows = new List<CashFlowWorkingCapitalChangeDto>();
|
||||
foreach (var a in accounts.Where(a => a.IsPostable
|
||||
&& !cashAccountIds.Contains(a.AccountId)
|
||||
&& category.GetValueOrDefault(a.AccountId) == "OPERATING"))
|
||||
{
|
||||
var openingBalance = opening.TryGetValue(a.AccountId, out var o) ? SignedBalance(a.AccountTypeId, o) : 0m;
|
||||
var closingBalance = closing.TryGetValue(a.AccountId, out var c) ? SignedBalance(a.AccountTypeId, c) : 0m;
|
||||
var change = closingBalance - openingBalance;
|
||||
if (change == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.Add(new CashFlowWorkingCapitalChangeDto
|
||||
{
|
||||
AccountCode = a.AccountCode,
|
||||
AccountName = a.AccountName,
|
||||
Direction = change >= 0 ? "Increase" : "Decrease",
|
||||
ChangeAmount = a.AccountTypeId == 1 ? -change : change
|
||||
});
|
||||
}
|
||||
|
||||
return [.. rows.OrderBy(r => r.AccountCode, StringComparer.Ordinal)];
|
||||
}
|
||||
|
||||
public async Task<List<CashFlowLineDto>> GetCashFlowCategoryLinesAsync(string category, DateOnly periodStart, DateOnly periodEnd, long cashAccountRootId)
|
||||
{
|
||||
var accounts = await _context.Accounts.AsNoTracking().ToListAsync();
|
||||
var cashAccountIds = RecursiveTreeQueries.GetDescendantIds(ToTreeRows(accounts), cashAccountRootId);
|
||||
var inherited = InheritTag(accounts, accounts.Where(a => a.ParentAccountId == null), a => a.CashFlowCategory);
|
||||
var totals = await GetPostedTotalsAsync(periodStart, periodEnd);
|
||||
|
||||
return [.. accounts
|
||||
.Where(a => a.IsPostable
|
||||
&& !cashAccountIds.Contains(a.AccountId)
|
||||
&& inherited.GetValueOrDefault(a.AccountId) == category
|
||||
&& totals.ContainsKey(a.AccountId))
|
||||
.Select(a => new CashFlowLineDto
|
||||
{
|
||||
AccountCode = a.AccountCode,
|
||||
AccountName = a.AccountName,
|
||||
Amount = totals[a.AccountId].Credit - totals[a.AccountId].Debit
|
||||
})
|
||||
.OrderBy(r => r.AccountCode, StringComparer.Ordinal)];
|
||||
}
|
||||
|
||||
public async Task<decimal> GetCashBalanceAsync(DateOnly asOfDate, long cashAccountRootId)
|
||||
{
|
||||
var accounts = await _context.Accounts.AsNoTracking().ToListAsync();
|
||||
var cashAccountIds = RecursiveTreeQueries.GetDescendantIds(ToTreeRows(accounts), cashAccountRootId);
|
||||
var totals = await GetPostedTotalsAsync(null, asOfDate);
|
||||
|
||||
return totals
|
||||
.Where(t => cashAccountIds.Contains(t.Key))
|
||||
.Sum(t => t.Value.Debit - t.Value.Credit);
|
||||
}
|
||||
|
||||
private async Task<List<RecursiveTreeQueries.TreeRow>> LoadTreeRowsAsync() =>
|
||||
await _context.Accounts.AsNoTracking()
|
||||
.Select(a => new RecursiveTreeQueries.TreeRow
|
||||
{
|
||||
Id = a.AccountId,
|
||||
Code = a.AccountCode,
|
||||
Name = a.AccountName,
|
||||
ParentId = a.ParentAccountId
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
private static List<RecursiveTreeQueries.TreeRow> ToTreeRows(IEnumerable<Account> accounts) =>
|
||||
[.. accounts.Select(a => new RecursiveTreeQueries.TreeRow
|
||||
{
|
||||
Id = a.AccountId,
|
||||
Code = a.AccountCode,
|
||||
Name = a.AccountName,
|
||||
ParentId = a.ParentAccountId
|
||||
})];
|
||||
|
||||
// Per-account base debit/credit totals over posted journal entries, optionally bounded
|
||||
// by entry date (inclusive on both ends). The sums run in the database; accounts with no
|
||||
// matching lines are simply absent from the result.
|
||||
private async Task<Dictionary<long, (decimal Debit, decimal Credit)>> GetPostedTotalsAsync(DateOnly? from, DateOnly? to)
|
||||
{
|
||||
var lines = _context.JournalEntryLines.Where(l => l.Journal.IsPosted);
|
||||
|
||||
if (from.HasValue)
|
||||
{
|
||||
lines = lines.Where(l => l.Journal.EntryDate >= from.Value);
|
||||
}
|
||||
|
||||
if (to.HasValue)
|
||||
{
|
||||
lines = lines.Where(l => l.Journal.EntryDate <= to.Value);
|
||||
}
|
||||
|
||||
var totals = await lines
|
||||
.GroupBy(l => l.AccountId)
|
||||
.Select(g => new { AccountId = g.Key, Debit = g.Sum(l => l.BaseDebit), Credit = g.Sum(l => l.BaseCredit) })
|
||||
.ToListAsync();
|
||||
|
||||
return totals.ToDictionary(t => t.AccountId, t => (t.Debit, t.Credit));
|
||||
}
|
||||
|
||||
// Asset (type 1) is debit-normal; every other type is credit-normal.
|
||||
private static decimal SignedBalance(short accountTypeId, (decimal Debit, decimal Credit) totals) =>
|
||||
accountTypeId == 1 ? totals.Debit - totals.Credit : totals.Credit - totals.Debit;
|
||||
|
||||
// Walks the tree from the given roots, giving each node its own tag or, if it has none,
|
||||
// its parent's inherited tag. Only nodes reachable from the roots appear in the result,
|
||||
// so a root filter (e.g. types 1-3 only) also scopes which accounts are included.
|
||||
private static Dictionary<long, string?> InheritTag(List<Account> accounts, IEnumerable<Account> roots, Func<Account, string?> tag)
|
||||
{
|
||||
var childrenByParent = accounts
|
||||
.Where(a => a.ParentAccountId.HasValue)
|
||||
.ToLookup(a => a.ParentAccountId!.Value);
|
||||
|
||||
var result = new Dictionary<long, string?>();
|
||||
var stack = new Stack<(Account Node, string? Inherited)>();
|
||||
foreach (var root in roots)
|
||||
{
|
||||
stack.Push((root, tag(root)));
|
||||
}
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var (node, inherited) = stack.Pop();
|
||||
if (!result.TryAdd(node.AccountId, inherited))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var child in childrenByParent[node.AccountId])
|
||||
{
|
||||
stack.Push((child, tag(child) ?? inherited));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using General_Ledger_Service.Enums;
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class AuditLogRepository(AppDBContext context) : IAuditLogRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public async Task AddAsync(AuditLog log) =>
|
||||
await _context.AuditLogs.AddAsync(log);
|
||||
|
||||
public async Task<(List<AuditLog> Items, int TotalCount)> ListAsync(
|
||||
List<AuditCategory> categories, string? entityName, AuditAction? action,
|
||||
string? performedBy, DateOnly? from, DateOnly? to, int? page, int? pageSize)
|
||||
{
|
||||
var query = _context.AuditLogs.Where(a => categories.Contains(a.Category));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(entityName))
|
||||
{
|
||||
query = query.Where(a => a.EntityName == entityName);
|
||||
}
|
||||
|
||||
if (action.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.Action == action.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(performedBy))
|
||||
{
|
||||
query = query.Where(a => a.PerformedBy == performedBy);
|
||||
}
|
||||
|
||||
if (from.HasValue)
|
||||
{
|
||||
var fromDateTime = from.Value.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
|
||||
query = query.Where(a => a.OccurredAt >= fromDateTime);
|
||||
}
|
||||
|
||||
if (to.HasValue)
|
||||
{
|
||||
var toDateTimeExclusive = to.Value.AddDays(1).ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
|
||||
query = query.Where(a => a.OccurredAt < toDateTimeExclusive);
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
var items = await RepositoryHelpers.ApplyPaging(query.OrderByDescending(a => a.OccurredAt), page, pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class BankRepository(AppDBContext context) : IBankRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public Task<BankAccount?> GetByIdAsync(long bankAccountId) =>
|
||||
RepositoryHelpers.GetByIdAsync(_context.BankAccounts, bankAccountId);
|
||||
|
||||
public Task<BankAccount> AddAsync(BankAccount bankAccount) =>
|
||||
RepositoryHelpers.AddAsync(_context.BankAccounts, bankAccount);
|
||||
|
||||
public async Task<List<BankAccount>> GetAllAsync() =>
|
||||
await _context.BankAccounts.Include(b => b.GlAccount).OrderBy(b => b.AccountName).ToListAsync();
|
||||
|
||||
public Task<BankStatementLine?> GetStatementLineByIdAsync(long stmtLineId) =>
|
||||
RepositoryHelpers.GetByIdAsync(_context.BankStatementLines, stmtLineId);
|
||||
|
||||
public async Task AddStatementLinesAsync(IEnumerable<BankStatementLine> lines) =>
|
||||
await _context.BankStatementLines.AddRangeAsync(lines);
|
||||
|
||||
public Task<CashAccount> AddCashAccountAsync(CashAccount cashAccount) =>
|
||||
RepositoryHelpers.AddAsync(_context.CashAccounts, cashAccount);
|
||||
|
||||
public async Task<List<CashAccount>> GetAllCashAccountsAsync() =>
|
||||
await _context.CashAccounts.Include(c => c.CashAccountType).Include(c => c.GlAccount).OrderBy(c => c.AccountName).ToListAsync();
|
||||
|
||||
public async Task<CashAccount?> GetCashAccountByIdAsync(long cashAccountId) =>
|
||||
await _context.CashAccounts.Include(c => c.CashAccountType).Include(c => c.GlAccount)
|
||||
.FirstOrDefaultAsync(c => c.CashAccountId == cashAccountId);
|
||||
|
||||
public async Task<bool> AnyDefaultCashAccountAsync() =>
|
||||
await _context.CashAccounts.AnyAsync(c => c.IsDefault);
|
||||
|
||||
public void RemoveCashAccount(CashAccount cashAccount) => _context.CashAccounts.Remove(cashAccount);
|
||||
|
||||
public async Task<bool> CashAccountNumberExistsAsync(string accountNumber) =>
|
||||
await _context.CashAccounts.AnyAsync(c => c.AccountNumber == accountNumber);
|
||||
|
||||
public async Task<CashAccountType?> GetCashAccountTypeByNameAsync(string name) =>
|
||||
await _context.CashAccountTypes.FirstOrDefaultAsync(t => t.Name.ToLower() == name.ToLower());
|
||||
|
||||
public Task<CashAccountType> AddCashAccountTypeAsync(CashAccountType cashAccountType) =>
|
||||
RepositoryHelpers.AddAsync(_context.CashAccountTypes, cashAccountType);
|
||||
|
||||
public async Task<List<CashAccountType>> GetAllCashAccountTypesAsync() =>
|
||||
await _context.CashAccountTypes.OrderBy(t => t.Name).ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using General_Ledger_Service.DTOs.Reports;
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class BudgetRepository(AppDBContext context) : IBudgetRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public Task<Budget?> GetByIdAsync(long budgetId) =>
|
||||
RepositoryHelpers.GetByIdAsync(_context.Budgets, budgetId);
|
||||
|
||||
public Task<Budget> AddAsync(Budget budget) =>
|
||||
RepositoryHelpers.AddAsync(_context.Budgets, budget);
|
||||
|
||||
public async Task AddLinesAsync(IEnumerable<BudgetLine> lines) =>
|
||||
await _context.BudgetLines.AddRangeAsync(lines);
|
||||
|
||||
public async Task<List<Budget>> GetAllAsync() =>
|
||||
await _context.Budgets.OrderBy(b => b.Name).ToListAsync();
|
||||
|
||||
public async Task<List<BudgetVsActualRowDto>> GetBudgetVsActualAsync(long budgetId)
|
||||
{
|
||||
var budgetLines = await _context.BudgetLines
|
||||
.Where(bl => bl.BudgetId == budgetId)
|
||||
.OrderBy(bl => bl.Account.AccountCode)
|
||||
.Select(bl => new
|
||||
{
|
||||
bl.BudgetLineId,
|
||||
bl.AccountId,
|
||||
bl.Account.AccountCode,
|
||||
bl.Account.AccountName,
|
||||
bl.PeriodId,
|
||||
bl.BudgetedAmount
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var accountIds = budgetLines.Select(bl => bl.AccountId).Distinct().ToList();
|
||||
var periodIds = budgetLines.Select(bl => bl.PeriodId).Distinct().ToList();
|
||||
|
||||
// Actual = posted base debit - credit per (account, period), summed in the database.
|
||||
var actuals = await _context.JournalEntryLines
|
||||
.Where(l => l.Journal.IsPosted
|
||||
&& accountIds.Contains(l.AccountId)
|
||||
&& periodIds.Contains(l.Journal.PeriodId))
|
||||
.GroupBy(l => new { l.AccountId, l.Journal.PeriodId })
|
||||
.Select(g => new { g.Key.AccountId, g.Key.PeriodId, Amount = g.Sum(l => l.BaseDebit - l.BaseCredit) })
|
||||
.ToListAsync();
|
||||
|
||||
var actualByAccountAndPeriod = actuals.ToDictionary(a => (a.AccountId, a.PeriodId), a => a.Amount);
|
||||
|
||||
return [.. budgetLines.Select(bl =>
|
||||
{
|
||||
actualByAccountAndPeriod.TryGetValue((bl.AccountId, bl.PeriodId), out var actual);
|
||||
return new BudgetVsActualRowDto
|
||||
{
|
||||
BudgetLineId = bl.BudgetLineId,
|
||||
AccountCode = bl.AccountCode,
|
||||
AccountName = bl.AccountName,
|
||||
PeriodId = bl.PeriodId,
|
||||
BudgetedAmount = bl.BudgetedAmount,
|
||||
ActualAmount = actual,
|
||||
Variance = bl.BudgetedAmount - actual
|
||||
};
|
||||
})];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using General_Ledger_Service.Enums;
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class ChequeRepository(AppDBContext context) : IChequeRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public Task<ChequeBook> AddChequeBookAsync(ChequeBook chequeBook) =>
|
||||
RepositoryHelpers.AddAsync(_context.ChequeBooks, chequeBook);
|
||||
|
||||
public Task<ChequeBook?> GetChequeBookByIdAsync(long chequeBookId) =>
|
||||
RepositoryHelpers.GetByIdAsync(_context.ChequeBooks, chequeBookId);
|
||||
|
||||
public async Task<ChequeBook?> GetChequeBookByNoAsync(string chequeBookNo, bool includePages = false)
|
||||
{
|
||||
var query = _context.ChequeBooks.AsQueryable();
|
||||
if (includePages)
|
||||
{
|
||||
query = query.Include(b => b.Pages);
|
||||
}
|
||||
|
||||
return await query.FirstOrDefaultAsync(b => b.ChequeBookNo == chequeBookNo);
|
||||
}
|
||||
|
||||
public Task<bool> ChequeBookNoExistsAsync(string chequeBookNo) =>
|
||||
RepositoryHelpers.ExistsAsync(_context.ChequeBooks, b => b.ChequeBookNo == chequeBookNo);
|
||||
|
||||
public async Task<(List<ChequeBook> Items, int TotalCount)> ListChequeBooksAsync(
|
||||
long? bankAccountId, long? branchId, ChequeBookStatus? status, int? page, int? pageSize)
|
||||
{
|
||||
var query = _context.ChequeBooks.AsQueryable();
|
||||
|
||||
if (bankAccountId.HasValue) query = query.Where(b => b.BankAccountId == bankAccountId.Value);
|
||||
if (branchId.HasValue) query = query.Where(b => b.BranchId == branchId.Value);
|
||||
if (status.HasValue) query = query.Where(b => b.Status == status.Value);
|
||||
|
||||
query = query.OrderByDescending(b => b.ReceivedDate).ThenBy(b => b.ChequeBookNo);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
var items = await RepositoryHelpers.ApplyPaging(query, page, pageSize).ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public async Task AddChequePagesAsync(IEnumerable<ChequePage> pages) =>
|
||||
await _context.ChequePages.AddRangeAsync(pages);
|
||||
|
||||
public async Task<ChequePage?> GetChequePageByNoAsync(string chequeNo) =>
|
||||
await _context.ChequePages.Include(p => p.ChequeBook).FirstOrDefaultAsync(p => p.ChequeNo == chequeNo);
|
||||
|
||||
public Task<bool> ChequeNoExistsAsync(string chequeNo) =>
|
||||
RepositoryHelpers.ExistsAsync(_context.ChequePages, p => p.ChequeNo == chequeNo);
|
||||
|
||||
public async Task<(List<ChequePage> Items, int TotalCount)> ListChequePagesAsync(
|
||||
string? chequeBookNo, ChequeIssueStatus? issueStatus, ChequePayeeType? payeeType,
|
||||
long? payeeId, int? page, int? pageSize)
|
||||
{
|
||||
var query = _context.ChequePages.Include(p => p.ChequeBook).AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(chequeBookNo)) query = query.Where(p => p.ChequeBook.ChequeBookNo == chequeBookNo);
|
||||
if (issueStatus.HasValue) query = query.Where(p => p.IssueStatus == issueStatus.Value);
|
||||
if (payeeType.HasValue) query = query.Where(p => p.PayeeType == payeeType.Value);
|
||||
if (payeeId.HasValue) query = query.Where(p => p.PayeeId == payeeId.Value);
|
||||
|
||||
query = query.OrderBy(p => p.ChequeNo);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
var items = await RepositoryHelpers.ApplyPaging(query, page, pageSize).ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public Task<ReceivedCheque> AddReceivedChequeAsync(ReceivedCheque receivedCheque) =>
|
||||
RepositoryHelpers.AddAsync(_context.ReceivedCheques, receivedCheque);
|
||||
|
||||
public Task<ReceivedCheque?> GetReceivedChequeByIdAsync(long receivedChequeId) =>
|
||||
RepositoryHelpers.GetByIdAsync(_context.ReceivedCheques, receivedChequeId);
|
||||
|
||||
public async Task<(List<ReceivedCheque> Items, int TotalCount)> ListReceivedChequesAsync(
|
||||
long? companyId, long? branchId, ReceivedChequeStatus? status,
|
||||
ReceivedChequeFromType? receivedFromType, int? page, int? pageSize)
|
||||
{
|
||||
var query = _context.ReceivedCheques.AsQueryable();
|
||||
|
||||
if (companyId.HasValue) query = query.Where(c => c.CompanyId == companyId.Value);
|
||||
if (branchId.HasValue) query = query.Where(c => c.BranchId == branchId.Value);
|
||||
if (status.HasValue) query = query.Where(c => c.Status == status.Value);
|
||||
if (receivedFromType.HasValue) query = query.Where(c => c.ReceivedFromType == receivedFromType.Value);
|
||||
|
||||
query = query.OrderByDescending(c => c.ReceivedDate).ThenByDescending(c => c.ReceivedChequeId);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
var items = await RepositoryHelpers.ApplyPaging(query, page, pageSize).ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using General_Ledger_Service.DTOs.CostCenters;
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class CostCenterRepository(AppDBContext context) : ICostCenterRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public Task<CostCenter?> GetByIdAsync(long costCenterId) =>
|
||||
RepositoryHelpers.GetByIdAsync(_context.CostCenters, costCenterId);
|
||||
|
||||
public async Task<CostCenter?> GetByCodeAsync(string code) =>
|
||||
await _context.CostCenters.FirstOrDefaultAsync(c => c.Code == code);
|
||||
|
||||
public async Task<int> GetAncestorDepthAsync(long parentCostCenterId) =>
|
||||
RecursiveTreeQueries.GetAncestorDepth(await LoadTreeRowsAsync(), parentCostCenterId);
|
||||
|
||||
public async Task<string?> GetAncestorBreadcrumbAsync(long costCenterId) =>
|
||||
RecursiveTreeQueries.GetAncestorBreadcrumb(await LoadTreeRowsAsync(), costCenterId);
|
||||
|
||||
public async Task<List<CostCenterTreeNode>> GetDescendantTreeAsync(long costCenterId)
|
||||
{
|
||||
var rows = RecursiveTreeQueries.GetDescendantTree(await LoadTreeRowsAsync(), costCenterId);
|
||||
return [.. rows.Select(r => new CostCenterTreeNode
|
||||
{
|
||||
CostCenterId = r.Id,
|
||||
Code = r.Code,
|
||||
Name = r.Name,
|
||||
ParentCostCenterId = r.ParentId,
|
||||
Depth = r.Depth
|
||||
})];
|
||||
}
|
||||
|
||||
public Task<CostCenter> AddAsync(CostCenter costCenter) =>
|
||||
RepositoryHelpers.AddAsync(_context.CostCenters, costCenter);
|
||||
|
||||
private async Task<List<RecursiveTreeQueries.TreeRow>> LoadTreeRowsAsync() =>
|
||||
await _context.CostCenters.AsNoTracking()
|
||||
.Select(c => new RecursiveTreeQueries.TreeRow
|
||||
{
|
||||
Id = c.CostCenterId,
|
||||
Code = c.Code,
|
||||
Name = c.Name,
|
||||
ParentId = c.ParentCostCenterId
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class CurrencyRepository(AppDBContext context) : ICurrencyRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public async Task<List<Currency>> GetAllAsync() =>
|
||||
await _context.Currencies
|
||||
.OrderBy(c => c.CurrencyCode)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<string> GetBaseCurrencyCodeAsync() =>
|
||||
(await _context.Currencies.FirstOrDefaultAsync(c => c.IsBaseCurrency))?.CurrencyCode ?? "LKR";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class FiscalCalendarRepository(AppDBContext context) : IFiscalCalendarRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public Task<FiscalYear?> GetFiscalYearByIdAsync(int fiscalYearId) =>
|
||||
RepositoryHelpers.GetByIdAsync(_context.FiscalYears, fiscalYearId);
|
||||
|
||||
public Task<bool> HasOverlappingFiscalYearRangeAsync(DateOnly startDate, DateOnly endDate) =>
|
||||
RepositoryHelpers.ExistsAsync(_context.FiscalYears, fy => startDate <= fy.EndDate && endDate >= fy.StartDate);
|
||||
|
||||
public Task<FiscalYear> AddFiscalYearAsync(FiscalYear fiscalYear) =>
|
||||
RepositoryHelpers.AddAsync(_context.FiscalYears, fiscalYear);
|
||||
|
||||
public Task<AccountingPeriod?> GetPeriodByIdAsync(int periodId) =>
|
||||
RepositoryHelpers.GetByIdAsync(_context.AccountingPeriods, periodId);
|
||||
|
||||
public async Task<AccountingPeriod?> GetPeriodByDateAsync(DateOnly date) =>
|
||||
await _context.AccountingPeriods
|
||||
.FirstOrDefaultAsync(p => p.StartDate <= date && p.EndDate >= date);
|
||||
|
||||
public async Task<List<AccountingPeriod>> GetPeriodsByFiscalYearIdAsync(int fiscalYearId) =>
|
||||
await _context.AccountingPeriods
|
||||
.Where(p => p.FiscalYearId == fiscalYearId)
|
||||
.OrderBy(p => p.PeriodNumber)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task AddPeriodRangeAsync(IEnumerable<AccountingPeriod> periods) =>
|
||||
await _context.AccountingPeriods.AddRangeAsync(periods);
|
||||
|
||||
public Task<PeriodLockLog> AddPeriodLockLogAsync(PeriodLockLog log) =>
|
||||
RepositoryHelpers.AddAsync(_context.PeriodLockLogs, log);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class FixedAssetRepository(AppDBContext context) : IFixedAssetRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public Task<FixedAsset?> GetByIdAsync(long assetId) =>
|
||||
RepositoryHelpers.GetByIdAsync(_context.FixedAssets, assetId);
|
||||
|
||||
public async Task<FixedAsset?> GetByCodeAsync(string assetCode) =>
|
||||
await _context.FixedAssets.FirstOrDefaultAsync(a => a.AssetCode == assetCode);
|
||||
|
||||
public Task<FixedAsset> AddAsync(FixedAsset asset) =>
|
||||
RepositoryHelpers.AddAsync(_context.FixedAssets, asset);
|
||||
|
||||
public async Task<List<FixedAsset>> GetAllAsync() =>
|
||||
await _context.FixedAssets.OrderBy(a => a.AssetCode).ToListAsync();
|
||||
|
||||
public async Task<DepreciationSchedule?> GetLatestScheduleByAssetIdAsync(long assetId) =>
|
||||
await _context.DepreciationSchedules
|
||||
.Where(s => s.AssetId == assetId)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
public Task<DepreciationSchedule> AddScheduleAsync(DepreciationSchedule schedule) =>
|
||||
RepositoryHelpers.AddAsync(_context.DepreciationSchedules, schedule);
|
||||
|
||||
public Task<AssetDisposal> AddDisposalAsync(AssetDisposal disposal) =>
|
||||
RepositoryHelpers.AddAsync(_context.AssetDisposals, disposal);
|
||||
|
||||
// Accounting_System_Design_LKAS.md Section 11.5 - the only non-cash add-back this system
|
||||
// currently tracks. ROUND(...,10) guards the same decimal-overflow class of bug fixed
|
||||
// elsewhere in the report queries (03_Progress_Tracker.md's 2026-07-21 bugfix entry).
|
||||
public async Task<decimal> GetDepreciationAddBackAsync(DateOnly periodStart, DateOnly periodEnd)
|
||||
{
|
||||
return await _context.DepreciationSchedules
|
||||
.Where(ds => ds.Journal != null
|
||||
&& ds.Journal.IsPosted
|
||||
&& ds.Journal.EntryDate >= periodStart
|
||||
&& ds.Journal.EntryDate <= periodEnd)
|
||||
.SumAsync(ds => (decimal?)ds.DepreciationAmount) ?? 0m;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using General_Ledger_Service.DTOs.Reports;
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class JournalEntryRepository(AppDBContext context) : IJournalEntryRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public async Task<JournalEntry?> GetJournalEntryByJournalNoAsync(string journalNo) =>
|
||||
await _context.JournalEntries
|
||||
.Include(j => j.Lines)
|
||||
.FirstOrDefaultAsync(j => j.JournalNo == journalNo);
|
||||
|
||||
public async Task<int> CountJournalEntriesForYearAsync(int year) =>
|
||||
await _context.JournalEntries.CountAsync(j => j.JournalNo.StartsWith($"JV-{year}-"));
|
||||
|
||||
public Task<JournalEntry> AddJournalEntryAsync(JournalEntry entry) =>
|
||||
RepositoryHelpers.AddAsync(_context.JournalEntries, entry);
|
||||
|
||||
public async Task AddJournalEntryLinesAsync(IEnumerable<JournalEntryLine> lines) =>
|
||||
await _context.JournalEntryLines.AddRangeAsync(lines);
|
||||
|
||||
public async Task<List<GeneralLedgerLineDto>> GetGeneralLedgerLinesAsync(long accountId, DateOnly periodStart, DateOnly periodEnd)
|
||||
{
|
||||
var treeRows = await _context.Accounts.AsNoTracking()
|
||||
.Select(a => new RecursiveTreeQueries.TreeRow
|
||||
{
|
||||
Id = a.AccountId,
|
||||
Code = a.AccountCode,
|
||||
Name = a.AccountName,
|
||||
ParentId = a.ParentAccountId
|
||||
})
|
||||
.ToListAsync();
|
||||
var accountIds = RecursiveTreeQueries.GetDescendantIds(treeRows, accountId).ToList();
|
||||
|
||||
var lines = _context.JournalEntryLines
|
||||
.Where(l => l.Journal.IsPosted
|
||||
&& accountIds.Contains(l.AccountId)
|
||||
&& l.Journal.EntryDate >= periodStart
|
||||
&& l.Journal.EntryDate <= periodEnd);
|
||||
|
||||
return await BuildGeneralLedgerLinesAsync(lines);
|
||||
}
|
||||
|
||||
public async Task<List<GeneralLedgerLineDto>> GetGeneralLedgerLinesForAllAccountsAsync(DateOnly periodStart, DateOnly periodEnd)
|
||||
{
|
||||
var lines = _context.JournalEntryLines
|
||||
.Where(l => l.Journal.IsPosted
|
||||
&& l.Account.IsPostable
|
||||
&& l.Journal.EntryDate >= periodStart
|
||||
&& l.Journal.EntryDate <= periodEnd);
|
||||
|
||||
return await BuildGeneralLedgerLinesAsync(lines);
|
||||
}
|
||||
|
||||
// Orders by account code, then entry date/journal (the running balance restarts for each
|
||||
// account and accumulates in that same date/journal order), then fetches the rows and
|
||||
// computes the running balance in memory.
|
||||
private static async Task<List<GeneralLedgerLineDto>> BuildGeneralLedgerLinesAsync(IQueryable<JournalEntryLine> lines)
|
||||
{
|
||||
var rows = await lines
|
||||
.OrderBy(l => l.Account.AccountCode)
|
||||
.ThenBy(l => l.Journal.EntryDate)
|
||||
.ThenBy(l => l.JournalId)
|
||||
.ThenBy(l => l.LineId)
|
||||
.Select(l => new
|
||||
{
|
||||
l.AccountId,
|
||||
l.Journal.EntryDate,
|
||||
l.Journal.JournalNo,
|
||||
l.Account.AccountCode,
|
||||
l.Account.AccountName,
|
||||
l.Journal.Narration,
|
||||
l.DebitAmount,
|
||||
l.CreditAmount
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var runningBalances = new Dictionary<long, decimal>();
|
||||
var result = new List<GeneralLedgerLineDto>(rows.Count);
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
runningBalances.TryGetValue(row.AccountId, out var previous);
|
||||
var running = previous + row.DebitAmount - row.CreditAmount;
|
||||
runningBalances[row.AccountId] = running;
|
||||
|
||||
result.Add(new GeneralLedgerLineDto
|
||||
{
|
||||
EntryDate = row.EntryDate,
|
||||
JournalNo = row.JournalNo,
|
||||
AccountCode = row.AccountCode,
|
||||
AccountName = row.AccountName,
|
||||
Narration = row.Narration,
|
||||
DebitAmount = row.DebitAmount,
|
||||
CreditAmount = row.CreditAmount,
|
||||
RunningBalance = running
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task<JournalCorrection> AddJournalCorrectionAsync(JournalCorrection correction) =>
|
||||
RepositoryHelpers.AddAsync(_context.JournalCorrections, correction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class RecurringJournalTemplateRepository(AppDBContext context) : IRecurringJournalTemplateRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public async Task<RecurringJournalTemplate?> GetByIdAsync(long templateId) =>
|
||||
await _context.RecurringJournalTemplates
|
||||
.Include(t => t.Lines)
|
||||
.FirstOrDefaultAsync(t => t.TemplateId == templateId);
|
||||
|
||||
public Task<RecurringJournalTemplate> AddAsync(RecurringJournalTemplate template) =>
|
||||
RepositoryHelpers.AddAsync(_context.RecurringJournalTemplates, template);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class SystemConfigRepository(AppDBContext context) : ISystemConfigRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public async Task<SystemConfig?> GetByKeyAsync(string configKey) =>
|
||||
await _context.SystemConfigs.FirstOrDefaultAsync(c => c.ConfigKey == configKey);
|
||||
|
||||
public async Task<List<SystemConfig>> GetAllAsync() =>
|
||||
await _context.SystemConfigs.OrderBy(c => c.ConfigKey).ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using General_Ledger_Service.Infra;
|
||||
using General_Ledger_Service.Interfaces.Repositories;
|
||||
using General_Ledger_Service.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
public class TaxRepository(AppDBContext context) : ITaxRepository
|
||||
{
|
||||
private readonly AppDBContext _context = context;
|
||||
|
||||
public async Task<TaxType?> GetTaxTypeByCodeAsync(string code) =>
|
||||
await _context.TaxTypes.FirstOrDefaultAsync(t => t.Code == code);
|
||||
|
||||
public async Task<List<TaxType>> GetAllTaxTypesAsync() =>
|
||||
await _context.TaxTypes.OrderBy(t => t.Code).ToListAsync();
|
||||
|
||||
public async Task<TaxCode?> GetEffectiveTaxCodeByCodeAsync(string code, DateOnly txnDate) =>
|
||||
await _context.TaxCodes.FirstOrDefaultAsync(tc =>
|
||||
tc.Code == code &&
|
||||
tc.EffectiveFrom <= txnDate &&
|
||||
(tc.EffectiveTo == null || tc.EffectiveTo >= txnDate));
|
||||
|
||||
public async Task<TaxCode?> GetByCodeAsync(string code) =>
|
||||
await _context.TaxCodes.FirstOrDefaultAsync(tc => tc.Code == code);
|
||||
|
||||
public Task<TaxCode> AddTaxCodeAsync(TaxCode taxCode) =>
|
||||
RepositoryHelpers.AddAsync(_context.TaxCodes, taxCode);
|
||||
|
||||
public async Task<List<TaxCode>> GetAllTaxCodesAsync() =>
|
||||
await _context.TaxCodes.OrderBy(tc => tc.Code).ToListAsync();
|
||||
|
||||
// Accounting_System_Design_LKAS.md Section 11.7 - Non-Deductible Expenses. Expense is
|
||||
// debit-normal, so base_credit - base_debit is negative for a normal balance; ABS() here
|
||||
// (rather than in the Service layer) since this figure is always presented as a positive
|
||||
// "Add:" line, same convention ReportService applies for the P&L's Expense-side sections.
|
||||
public async Task<decimal> GetNonDeductibleExpensesAsync(DateOnly periodStart, DateOnly periodEnd)
|
||||
{
|
||||
var net = await _context.JournalEntryLines
|
||||
.Where(l => l.Journal.IsPosted
|
||||
&& l.Journal.EntryDate >= periodStart
|
||||
&& l.Journal.EntryDate <= periodEnd
|
||||
&& l.Account.AccountTypeId == 5
|
||||
&& !l.Account.IsTaxDeductible)
|
||||
.SumAsync(l => (decimal?)(l.BaseCredit - l.BaseDebit)) ?? 0m;
|
||||
|
||||
return Math.Abs(net);
|
||||
}
|
||||
|
||||
// Reused for APIT/WHT/quarterly-tax-payment credit accounts (Section 11.7, steps 8-10) -
|
||||
// each is a plain credit-normal balance sum against one configured accounts.account_id.
|
||||
public async Task<decimal> GetAccountCreditActivityAsync(long accountId, DateOnly periodStart, DateOnly periodEnd)
|
||||
{
|
||||
return await _context.JournalEntryLines
|
||||
.Where(l => l.Journal.IsPosted
|
||||
&& l.Journal.EntryDate >= periodStart
|
||||
&& l.Journal.EntryDate <= periodEnd
|
||||
&& l.AccountId == accountId)
|
||||
.SumAsync(l => (decimal?)(l.BaseCredit - l.BaseDebit)) ?? 0m;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user