using BakeryErp.Infrastructure.Database; namespace BakeryErp.Infrastructure.Repositories; public sealed class OcrDocumentRepository { private readonly DatabaseOptions _options; public OcrDocumentRepository(DatabaseOptions options) { _options = options; } public IReadOnlyList List() { return LocalDataStoreFile.Load(_options) .OcrDocuments .OrderByDescending(static document => document.CreatedAt) .ToList(); } public LocalOcrDocument? Find(Guid id) { return LocalDataStoreFile.Load(_options) .OcrDocuments .FirstOrDefault(document => document.Id == id); } public OcrReport GetReport() { var documents = LocalDataStoreFile.Load(_options).OcrDocuments; return new OcrReport( documents.Count, documents.Count(static document => document.Status == LocalOcrStatus.Uploaded), documents.Count(static document => document.Status == LocalOcrStatus.Recognized), documents.Count(static document => document.Status == LocalOcrStatus.MappingRequired), documents.Count(static document => document.Status == LocalOcrStatus.Confirmed), documents.Count(static document => document.Status == LocalOcrStatus.ConvertedToReceipt), documents.Count(static document => document.Status == LocalOcrStatus.Failed), documents.Sum(static document => document.TotalAmount ?? 0)); } public SaveOcrDocumentResult Save(LocalOcrDocument document) { var store = LocalDataStoreFile.Load(_options); var documentNumber = document.DocumentNumber.Trim().ToUpperInvariant(); if (string.IsNullOrWhiteSpace(documentNumber)) { return SaveOcrDocumentResult.ValidationError("單據編號為必填。"); } var duplicatedNumber = store.OcrDocuments.Any(existing => existing.Id != document.Id && string.Equals(existing.DocumentNumber, documentNumber, StringComparison.OrdinalIgnoreCase)); if (duplicatedNumber) { return SaveOcrDocumentResult.ValidationError("單據編號已存在,請改用其他編號。"); } var existingDocument = store.OcrDocuments.FirstOrDefault(existing => existing.Id == document.Id); if (existingDocument is null) { document.Id = document.Id == Guid.Empty ? Guid.NewGuid() : document.Id; document.DocumentNumber = documentNumber; document.FileName = BlankToEmpty(document.FileName); document.SupplierName = BlankToNull(document.SupplierName); document.RawText = BlankToNull(document.RawText); document.RecognizedItems = BlankToNull(document.RecognizedItems); document.ReviewNote = BlankToNull(document.ReviewNote); document.CreatedAt = DateTime.UtcNow; store.OcrDocuments.Add(document); } else { existingDocument.DocumentNumber = documentNumber; existingDocument.FileName = BlankToEmpty(document.FileName); existingDocument.SupplierName = BlankToNull(document.SupplierName); existingDocument.DocumentDate = document.DocumentDate; existingDocument.Status = document.Status; existingDocument.TotalAmount = document.TotalAmount; existingDocument.RawText = BlankToNull(document.RawText); existingDocument.RecognizedItems = BlankToNull(document.RecognizedItems); existingDocument.ReviewNote = BlankToNull(document.ReviewNote); existingDocument.UpdatedAt = DateTime.UtcNow; } LocalDataStoreFile.Save(_options, store); return SaveOcrDocumentResult.Success(); } public void Review(Guid id, bool approved) { var store = LocalDataStoreFile.Load(_options); var document = store.OcrDocuments.FirstOrDefault(existing => existing.Id == id); if (document is null) { return; } document.Status = approved ? LocalOcrStatus.Confirmed : LocalOcrStatus.MappingRequired; document.ReviewedAt = DateTime.UtcNow; document.UpdatedAt = DateTime.UtcNow; LocalDataStoreFile.Save(_options, store); } public void ConvertToReceipt(Guid id) { var store = LocalDataStoreFile.Load(_options); var document = store.OcrDocuments.FirstOrDefault(existing => existing.Id == id); if (document is null) { return; } document.Status = LocalOcrStatus.ConvertedToReceipt; document.UpdatedAt = DateTime.UtcNow; LocalDataStoreFile.Save(_options, store); } public static string GetStatusName(LocalOcrStatus status) { return status switch { LocalOcrStatus.Uploaded => "已上傳", LocalOcrStatus.Recognized => "已辨識", LocalOcrStatus.MappingRequired => "待對應", LocalOcrStatus.Confirmed => "已確認", LocalOcrStatus.ConvertedToReceipt => "已轉進貨", LocalOcrStatus.Failed => "失敗", _ => status.ToString() }; } 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 OcrReport( int TotalCount, int UploadedCount, int RecognizedCount, int MappingRequiredCount, int ConfirmedCount, int ConvertedToReceiptCount, int FailedCount, decimal TotalAmount); public sealed record SaveOcrDocumentResult(bool IsSuccess, string? ErrorMessage) { public static SaveOcrDocumentResult Success() => new(true, null); public static SaveOcrDocumentResult ValidationError(string message) => new(false, message); }