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