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:
Invoice Master
2026-02-04 23:18:59 +01:00
parent 05ea67144f
commit ad6ce08e3e
214 changed files with 29692 additions and 1820 deletions

View File

@@ -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();
}
}

View 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; }
}