Spaces:
Sleeping
Sleeping
| using BakeryErp.Infrastructure.Database; | |
| namespace BakeryErp.Infrastructure.Repositories; | |
| public sealed class PettyCashRepository | |
| { | |
| private readonly DatabaseOptions _options; | |
| public PettyCashRepository(DatabaseOptions options) | |
| { | |
| _options = options; | |
| } | |
| public IReadOnlyList<LocalPettyCashTransaction> 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<StorePettyCashSummary> 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); | |
| } | |