refactor: rename namespace to FiscalFlow and upgrade to .NET 10
- Rename InvoiceMaster.* to FiscalFlow.* namespace - Upgrade from .NET 8 to .NET 10 - Update all NuGet packages to latest versions - Update C# language version to 14.0
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using FiscalFlow.Core.Interfaces;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FiscalFlow.Infrastructure.Services;
|
||||
|
||||
public class AzureBlobStorageService : IBlobStorageService
|
||||
{
|
||||
private readonly BlobContainerClient _containerClient;
|
||||
private readonly ILogger<AzureBlobStorageService> _logger;
|
||||
|
||||
public AzureBlobStorageService(IConfiguration configuration, ILogger<AzureBlobStorageService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
var connectionString = configuration["AzureStorage:ConnectionString"];
|
||||
var containerName = configuration["AzureStorage:ContainerName"] ?? "documents";
|
||||
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
_logger.LogWarning("Azure Storage connection string not configured. Using development mode.");
|
||||
_containerClient = null!;
|
||||
}
|
||||
else
|
||||
{
|
||||
_containerClient = new BlobContainerClient(connectionString, containerName);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> UploadAsync(string fileName, Stream content, string contentType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_containerClient == null)
|
||||
{
|
||||
var localPath = Path.Combine("uploads", fileName);
|
||||
Directory.CreateDirectory("uploads");
|
||||
using var fileStream = File.Create(localPath);
|
||||
await content.CopyToAsync(fileStream, cancellationToken);
|
||||
return localPath;
|
||||
}
|
||||
|
||||
await _containerClient.CreateIfNotExistsAsync(PublicAccessType.None, cancellationToken: cancellationToken);
|
||||
|
||||
var blobName = $"{DateTime.UtcNow:yyyy/MM/dd}/{Guid.NewGuid()}_{fileName}";
|
||||
var blobClient = _containerClient.GetBlobClient(blobName);
|
||||
|
||||
var blobHttpHeaders = new BlobHttpHeaders { ContentType = contentType };
|
||||
await blobClient.UploadAsync(content, new BlobUploadOptions { HttpHeaders = blobHttpHeaders }, cancellationToken);
|
||||
|
||||
_logger.LogInformation("File uploaded successfully: {BlobName}", blobName);
|
||||
return blobName;
|
||||
}
|
||||
|
||||
public async Task<Stream> DownloadAsync(string blobName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_containerClient == null)
|
||||
{
|
||||
return File.OpenRead(blobName);
|
||||
}
|
||||
|
||||
var blobClient = _containerClient.GetBlobClient(blobName);
|
||||
var response = await blobClient.DownloadAsync(cancellationToken);
|
||||
return response.Value.Content;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string blobName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_containerClient == null)
|
||||
{
|
||||
if (File.Exists(blobName))
|
||||
{
|
||||
File.Delete(blobName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var blobClient = _containerClient.GetBlobClient(blobName);
|
||||
await blobClient.DeleteIfExistsAsync(cancellationToken: cancellationToken);
|
||||
_logger.LogInformation("File deleted: {BlobName}", blobName);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string blobName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_containerClient == null)
|
||||
{
|
||||
return File.Exists(blobName);
|
||||
}
|
||||
|
||||
var blobClient = _containerClient.GetBlobClient(blobName);
|
||||
return await blobClient.ExistsAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public string GetBlobUrl(string blobName)
|
||||
{
|
||||
if (_containerClient == null)
|
||||
{
|
||||
return $"/uploads/{Path.GetFileName(blobName)}";
|
||||
}
|
||||
|
||||
var blobClient = _containerClient.GetBlobClient(blobName);
|
||||
return blobClient.Uri.ToString();
|
||||
}
|
||||
}
|
||||
154
backend/src/FiscalFlow.Infrastructure/Services/OcrService.cs
Normal file
154
backend/src/FiscalFlow.Infrastructure/Services/OcrService.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using FiscalFlow.Core.Interfaces;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace FiscalFlow.Infrastructure.Services;
|
||||
|
||||
public class OcrService : IOcrService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<OcrService> _logger;
|
||||
private readonly string _apiUrl;
|
||||
private readonly string? _apiKey;
|
||||
|
||||
public OcrService(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<OcrService> logger)
|
||||
{
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
_logger = logger;
|
||||
_apiUrl = configuration["Ocr:ApiUrl"] ?? "http://localhost:8000/api/v1";
|
||||
_apiKey = configuration["Ocr:ApiKey"];
|
||||
}
|
||||
|
||||
public async Task<OcrResult> ExtractAsync(Stream fileStream, string fileName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = new MultipartFormDataContent();
|
||||
var streamContent = new StreamContent(fileStream);
|
||||
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
|
||||
content.Add(streamContent, "file", fileName);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/infer")
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(_apiKey))
|
||||
{
|
||||
request.Headers.Add("X-API-Key", _apiKey);
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError("OCR API error: {StatusCode} - {Content}", response.StatusCode, responseContent);
|
||||
return new OcrResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"OCR API returned {response.StatusCode}"
|
||||
};
|
||||
}
|
||||
|
||||
var result = JsonSerializer.Deserialize<OcrApiResponse>(responseContent, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return new OcrResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid OCR response"
|
||||
};
|
||||
}
|
||||
|
||||
return new OcrResult
|
||||
{
|
||||
Success = result.Success,
|
||||
Data = MapToInvoiceData(result.Fields),
|
||||
Confidence = result.Confidence,
|
||||
ErrorMessage = result.Error
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error calling OCR API");
|
||||
return new OcrResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static InvoiceData MapToInvoiceData(Dictionary<string, OcrField>? fields)
|
||||
{
|
||||
if (fields == null)
|
||||
{
|
||||
return new InvoiceData();
|
||||
}
|
||||
|
||||
return new InvoiceData
|
||||
{
|
||||
SupplierName = GetFieldValue(fields, "supplier_name"),
|
||||
SupplierOrgNumber = GetFieldValue(fields, "supplier_org_number"),
|
||||
InvoiceNumber = GetFieldValue(fields, "invoice_number"),
|
||||
InvoiceDate = ParseDate(GetFieldValue(fields, "invoice_date")),
|
||||
DueDate = ParseDate(GetFieldValue(fields, "due_date")),
|
||||
AmountTotal = ParseDecimal(GetFieldValue(fields, "amount_total")),
|
||||
AmountVat = ParseDecimal(GetFieldValue(fields, "amount_vat")),
|
||||
VatRate = ParseInt(GetFieldValue(fields, "vat_rate")),
|
||||
OcrNumber = GetFieldValue(fields, "ocr_number"),
|
||||
Bankgiro = GetFieldValue(fields, "bankgiro"),
|
||||
Plusgiro = GetFieldValue(fields, "plusgiro"),
|
||||
Currency = GetFieldValue(fields, "currency") ?? "SEK"
|
||||
};
|
||||
}
|
||||
|
||||
private static string? GetFieldValue(Dictionary<string, OcrField> fields, string key)
|
||||
{
|
||||
return fields.TryGetValue(key, out var field) ? field.Value : null;
|
||||
}
|
||||
|
||||
private static DateTime? ParseDate(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return null;
|
||||
if (DateTime.TryParse(value, out var date)) return date;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static decimal? ParseDecimal(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return null;
|
||||
value = value.Replace(",", "").Replace(" ", "");
|
||||
if (decimal.TryParse(value, out var result)) return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int? ParseInt(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return null;
|
||||
if (int.TryParse(value, out var result)) return result;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class OcrApiResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public Dictionary<string, OcrField>? Fields { get; set; }
|
||||
public decimal Confidence { get; set; }
|
||||
}
|
||||
|
||||
public class OcrField
|
||||
{
|
||||
public string? Value { get; set; }
|
||||
public decimal Confidence { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user