using BakeryErp.Infrastructure.Database; namespace BakeryErp.Infrastructure.Repositories; public sealed class PurchaseRepository { private readonly DatabaseOptions _options; public PurchaseRepository(DatabaseOptions options) { _options = options; } public IReadOnlyList List() { return LocalDataStoreFile.Load(_options) .PurchaseOrders .OrderByDescending(static order => order.OrderDate) .ThenBy(static order => order.OrderNumber) .ToList(); } public LocalPurchaseOrder? Find(Guid id) { return LocalDataStoreFile.Load(_options) .PurchaseOrders .FirstOrDefault(order => order.Id == id); } public PurchaseReport GetReport() { var orders = LocalDataStoreFile.Load(_options).PurchaseOrders; return new PurchaseReport( orders.Count, orders.Count(static order => order.Status == LocalPurchaseOrderStatus.PendingApproval), orders.Count(static order => order.Status == LocalPurchaseOrderStatus.Approved), orders.Count(static order => order.Status == LocalPurchaseOrderStatus.MonthlyClosed), orders.Where(static order => order.Status != LocalPurchaseOrderStatus.Cancelled).Sum(static order => order.LineAmount), orders.Where(static order => order.Status == LocalPurchaseOrderStatus.Approved).Sum(static order => order.LineAmount), orders.Where(static order => order.Status == LocalPurchaseOrderStatus.MonthlyClosed).Sum(static order => order.LineAmount)); } public IReadOnlyList GetSupplierSummaries() { return LocalDataStoreFile.Load(_options) .PurchaseOrders .Where(static order => order.Status != LocalPurchaseOrderStatus.Cancelled) .GroupBy(static order => order.SupplierName) .Select(group => new SupplierPurchaseSummary( group.Key, group.Count(), group.Sum(static order => order.LineAmount), group.Count(static order => order.Status == LocalPurchaseOrderStatus.MonthlyClosed))) .OrderByDescending(static summary => summary.TotalAmount) .ToList(); } public SavePurchaseOrderResult Save(LocalPurchaseOrder order) { var store = LocalDataStoreFile.Load(_options); var orderNumber = order.OrderNumber.Trim().ToUpperInvariant(); if (string.IsNullOrWhiteSpace(orderNumber) || string.IsNullOrWhiteSpace(order.SupplierName) || string.IsNullOrWhiteSpace(order.ItemName) || string.IsNullOrWhiteSpace(order.Unit)) { return SavePurchaseOrderResult.ValidationError("採購單號、供應商、品項與單位為必填。"); } if (order.Quantity <= 0 || order.UnitPrice < 0) { return SavePurchaseOrderResult.ValidationError("數量需大於 0,單價不可小於 0。"); } var duplicatedNumber = store.PurchaseOrders.Any(existing => existing.Id != order.Id && string.Equals(existing.OrderNumber, orderNumber, StringComparison.OrdinalIgnoreCase)); if (duplicatedNumber) { return SavePurchaseOrderResult.ValidationError("採購單號已存在,請改用其他單號。"); } var existingOrder = store.PurchaseOrders.FirstOrDefault(existing => existing.Id == order.Id); if (existingOrder is null) { order.Id = order.Id == Guid.Empty ? Guid.NewGuid() : order.Id; order.OrderNumber = orderNumber; order.SupplierName = order.SupplierName.Trim(); order.ItemSku = BlankToEmpty(order.ItemSku).ToUpperInvariant(); order.ItemName = order.ItemName.Trim(); order.Unit = order.Unit.Trim(); order.Note = BlankToNull(order.Note); order.Status = LocalPurchaseOrderStatus.PendingApproval; order.CreatedAt = DateTime.UtcNow; store.PurchaseOrders.Add(order); } else { if (existingOrder.Status is LocalPurchaseOrderStatus.Approved or LocalPurchaseOrderStatus.MonthlyClosed) { return SavePurchaseOrderResult.ValidationError("已審核或已月結的採購單不可編輯。"); } existingOrder.OrderNumber = orderNumber; existingOrder.SupplierName = order.SupplierName.Trim(); existingOrder.OrderDate = order.OrderDate; existingOrder.ItemSku = BlankToEmpty(order.ItemSku).ToUpperInvariant(); existingOrder.ItemName = order.ItemName.Trim(); existingOrder.Quantity = order.Quantity; existingOrder.Unit = order.Unit.Trim(); existingOrder.UnitPrice = order.UnitPrice; existingOrder.Note = BlankToNull(order.Note); existingOrder.Status = LocalPurchaseOrderStatus.PendingApproval; existingOrder.UpdatedAt = DateTime.UtcNow; } LocalDataStoreFile.Save(_options, store); return SavePurchaseOrderResult.Success(); } public void Approve(Guid id) { UpdateStatus(id, LocalPurchaseOrderStatus.Approved); } public void CloseMonthly(Guid id) { UpdateStatus(id, LocalPurchaseOrderStatus.MonthlyClosed); } public void Cancel(Guid id) { UpdateStatus(id, LocalPurchaseOrderStatus.Cancelled); } public static string GetStatusName(LocalPurchaseOrderStatus status) { return status switch { LocalPurchaseOrderStatus.Draft => "草稿", LocalPurchaseOrderStatus.PendingApproval => "待審核", LocalPurchaseOrderStatus.Approved => "已審核", LocalPurchaseOrderStatus.MonthlyClosed => "已月結", LocalPurchaseOrderStatus.Cancelled => "已取消", _ => status.ToString() }; } private void UpdateStatus(Guid id, LocalPurchaseOrderStatus status) { var store = LocalDataStoreFile.Load(_options); var order = store.PurchaseOrders.FirstOrDefault(existing => existing.Id == id); if (order is null) { return; } if (status == LocalPurchaseOrderStatus.Cancelled && order.Status == LocalPurchaseOrderStatus.MonthlyClosed) { return; } order.Status = status; order.UpdatedAt = DateTime.UtcNow; if (status == LocalPurchaseOrderStatus.Approved) { order.ApprovedAt = DateTime.UtcNow; } if (status == LocalPurchaseOrderStatus.MonthlyClosed) { order.ClosedAt = DateTime.UtcNow; } LocalDataStoreFile.Save(_options, store); } private static string BlankToEmpty(string? value) { return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); } private static string? BlankToNull(string? value) { return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } } public sealed record PurchaseReport( int OrderCount, int PendingApprovalCount, int ApprovedCount, int MonthlyClosedCount, decimal TotalAmount, decimal PayableAmount, decimal ClosedAmount); public sealed record SupplierPurchaseSummary( string SupplierName, int OrderCount, decimal TotalAmount, int ClosedCount); public sealed record SavePurchaseOrderResult(bool IsSuccess, string? ErrorMessage) { public static SavePurchaseOrderResult Success() => new(true, null); public static SavePurchaseOrderResult ValidationError(string message) => new(false, message); }