using BakeryErp.Infrastructure.Database; namespace BakeryErp.Infrastructure.Repositories; public sealed class PettyCashRepository { private readonly DatabaseOptions _options; public PettyCashRepository(DatabaseOptions options) { _options = options; } public IReadOnlyList List() { return LocalDataStoreFile.Load(_options) .PettyCashTransactions .OrderByDescending(static transaction => transaction.TransactionDate) .ThenBy(static transaction => transaction.TransactionNumber) .ToList(); } public LocalPettyCashTransaction? Find(Guid id) { return LocalDataStoreFile.Load(_options) .PettyCashTransactions .FirstOrDefault(transaction => transaction.Id == id); } public PettyCashReport GetReport() { var transactions = LocalDataStoreFile.Load(_options).PettyCashTransactions; var approvedExpenses = transactions.Where(static transaction => transaction.TransactionType == LocalPettyCashTransactionType.Expense && transaction.Status is LocalPettyCashStatus.Approved or LocalPettyCashStatus.MonthlyClosed); var replenishments = transactions.Where(static transaction => transaction.TransactionType == LocalPettyCashTransactionType.Replenishment && transaction.Status is LocalPettyCashStatus.Approved or LocalPettyCashStatus.Replenished); return new PettyCashReport( transactions.Count, transactions.Count(static transaction => transaction.Status == LocalPettyCashStatus.PendingFinanceApproval), approvedExpenses.Sum(static transaction => transaction.Amount), transactions.Count(static transaction => transaction.Status == LocalPettyCashStatus.MonthlyClosed), replenishments.Sum(static transaction => transaction.Amount), replenishments.Sum(static transaction => transaction.Amount) - approvedExpenses.Sum(static transaction => transaction.Amount)); } public IReadOnlyList GetStoreSummaries() { return LocalDataStoreFile.Load(_options) .PettyCashTransactions .Where(static transaction => transaction.Status != LocalPettyCashStatus.Cancelled) .GroupBy(static transaction => transaction.StoreName) .Select(group => new StorePettyCashSummary( group.Key, group.Count(), group.Where(static transaction => transaction.TransactionType == LocalPettyCashTransactionType.Expense).Sum(static transaction => transaction.Amount), group.Where(static transaction => transaction.TransactionType == LocalPettyCashTransactionType.Replenishment).Sum(static transaction => transaction.Amount))) .OrderByDescending(static summary => summary.ExpenseAmount) .ToList(); } public SavePettyCashResult Save(LocalPettyCashTransaction transaction) { var store = LocalDataStoreFile.Load(_options); var transactionNumber = transaction.TransactionNumber.Trim().ToUpperInvariant(); if (string.IsNullOrWhiteSpace(transactionNumber) || string.IsNullOrWhiteSpace(transaction.StoreName) || string.IsNullOrWhiteSpace(transaction.ExpenseCategory)) { return SavePettyCashResult.ValidationError("零用金單號、門市與分類為必填。"); } if (transaction.Amount <= 0) { return SavePettyCashResult.ValidationError("金額需大於 0。"); } var duplicatedNumber = store.PettyCashTransactions.Any(existing => existing.Id != transaction.Id && string.Equals(existing.TransactionNumber, transactionNumber, StringComparison.OrdinalIgnoreCase)); if (duplicatedNumber) { return SavePettyCashResult.ValidationError("零用金單號已存在,請改用其他單號。"); } var existingTransaction = store.PettyCashTransactions.FirstOrDefault(existing => existing.Id == transaction.Id); if (existingTransaction is null) { transaction.Id = transaction.Id == Guid.Empty ? Guid.NewGuid() : transaction.Id; transaction.TransactionNumber = transactionNumber; transaction.StoreName = transaction.StoreName.Trim(); transaction.ExpenseCategory = transaction.ExpenseCategory.Trim(); transaction.Description = BlankToNull(transaction.Description); transaction.Status = LocalPettyCashStatus.PendingFinanceApproval; transaction.CreatedAt = DateTime.UtcNow; store.PettyCashTransactions.Add(transaction); } else { if (existingTransaction.Status is LocalPettyCashStatus.Approved or LocalPettyCashStatus.MonthlyClosed or LocalPettyCashStatus.Replenished) { return SavePettyCashResult.ValidationError("已審核、已月結或已撥補的零用金紀錄不可編輯。"); } existingTransaction.TransactionNumber = transactionNumber; existingTransaction.StoreName = transaction.StoreName.Trim(); existingTransaction.TransactionDate = transaction.TransactionDate; existingTransaction.TransactionType = transaction.TransactionType; existingTransaction.ExpenseCategory = transaction.ExpenseCategory.Trim(); existingTransaction.Amount = transaction.Amount; existingTransaction.Description = BlankToNull(transaction.Description); existingTransaction.ReceiptImagePath = BlankToNull(transaction.ReceiptImagePath) ?? existingTransaction.ReceiptImagePath; existingTransaction.Status = LocalPettyCashStatus.PendingFinanceApproval; existingTransaction.UpdatedAt = DateTime.UtcNow; } LocalDataStoreFile.Save(_options, store); return SavePettyCashResult.Success(); } public void Approve(Guid id) { UpdateStatus(id, LocalPettyCashStatus.Approved); } public void Reject(Guid id) { UpdateStatus(id, LocalPettyCashStatus.Rejected); } public void CloseMonthly(Guid id) { UpdateStatus(id, LocalPettyCashStatus.MonthlyClosed); } public void Replenish(Guid id) { UpdateStatus(id, LocalPettyCashStatus.Replenished); } public static string GetTransactionTypeName(LocalPettyCashTransactionType type) { return type switch { LocalPettyCashTransactionType.Expense => "門市支出", LocalPettyCashTransactionType.Replenishment => "零用金撥補", LocalPettyCashTransactionType.Adjustment => "調整", _ => type.ToString() }; } public static string GetStatusName(LocalPettyCashStatus status) { return status switch { LocalPettyCashStatus.Draft => "草稿", LocalPettyCashStatus.PendingFinanceApproval => "待財務審核", LocalPettyCashStatus.Approved => "已審核", LocalPettyCashStatus.MonthlyClosed => "已月結", LocalPettyCashStatus.Replenished => "已撥補", LocalPettyCashStatus.Rejected => "已退回", LocalPettyCashStatus.Cancelled => "已取消", _ => status.ToString() }; } private void UpdateStatus(Guid id, LocalPettyCashStatus status) { var store = LocalDataStoreFile.Load(_options); var transaction = store.PettyCashTransactions.FirstOrDefault(existing => existing.Id == id); if (transaction is null) { return; } transaction.Status = status; transaction.UpdatedAt = DateTime.UtcNow; if (status == LocalPettyCashStatus.Approved) { transaction.ApprovedAt = DateTime.UtcNow; } if (status == LocalPettyCashStatus.MonthlyClosed) { transaction.ClosedAt = DateTime.UtcNow; } LocalDataStoreFile.Save(_options, store); } private static string? BlankToNull(string? value) { return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } } public sealed record PettyCashReport( int TransactionCount, int PendingFinanceApprovalCount, decimal ApprovedExpenseAmount, int MonthlyClosedCount, decimal ReplenishmentAmount, decimal EstimatedBalance); public sealed record StorePettyCashSummary( string StoreName, int TransactionCount, decimal ExpenseAmount, decimal ReplenishmentAmount); public sealed record SavePettyCashResult(bool IsSuccess, string? ErrorMessage) { public static SavePettyCashResult Success() => new(true, null); public static SavePettyCashResult ValidationError(string message) => new(false, message); }