- Add .NET 8 backend with Clean Architecture - Add React + Vite + TypeScript frontend - Implement authentication with JWT - Implement Azure Blob Storage client - Implement OCR integration - Implement supplier matching service - Implement voucher generation - Implement Fortnox provider - Add unit and integration tests - Add Docker Compose configuration
139 lines
5.0 KiB
C#
139 lines
5.0 KiB
C#
using InvoiceMaster.Application.DTOs;
|
|
using InvoiceMaster.Core.Entities;
|
|
using InvoiceMaster.Core.Interfaces;
|
|
using MediatR;
|
|
|
|
namespace InvoiceMaster.Application.Commands.Invoices;
|
|
|
|
public record UploadInvoiceCommand(
|
|
Guid UserId,
|
|
string Provider,
|
|
string FileName,
|
|
Stream FileStream,
|
|
long FileSize,
|
|
string ContentType) : IRequest<UploadInvoiceResult>;
|
|
|
|
public record UploadInvoiceResult(
|
|
bool Success,
|
|
string? ErrorMessage,
|
|
InvoiceDetailDto? Invoice);
|
|
|
|
public class UploadInvoiceCommandHandler : IRequestHandler<UploadInvoiceCommand, UploadInvoiceResult>
|
|
{
|
|
private readonly IRepository<Invoice> _invoiceRepository;
|
|
private readonly IRepository<AccountingConnection> _connectionRepository;
|
|
private readonly IBlobStorageService _blobStorage;
|
|
private readonly IOcrService _ocrService;
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
|
|
public UploadInvoiceCommandHandler(
|
|
IRepository<Invoice> invoiceRepository,
|
|
IRepository<AccountingConnection> connectionRepository,
|
|
IBlobStorageService blobStorage,
|
|
IOcrService ocrService,
|
|
IUnitOfWork unitOfWork)
|
|
{
|
|
_invoiceRepository = invoiceRepository;
|
|
_connectionRepository = connectionRepository;
|
|
_blobStorage = blobStorage;
|
|
_ocrService = ocrService;
|
|
_unitOfWork = unitOfWork;
|
|
}
|
|
|
|
public async Task<UploadInvoiceResult> Handle(UploadInvoiceCommand request, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var connections = await _connectionRepository.GetAllAsync(cancellationToken);
|
|
var connection = connections.FirstOrDefault(c =>
|
|
c.UserId == request.UserId &&
|
|
c.Provider.Equals(request.Provider, StringComparison.OrdinalIgnoreCase) &&
|
|
c.IsActive);
|
|
|
|
if (connection == null)
|
|
{
|
|
return new UploadInvoiceResult(false, $"No active connection found for provider '{request.Provider}'", null);
|
|
}
|
|
|
|
var storagePath = await _blobStorage.UploadAsync(request.FileName, request.FileStream, request.ContentType, cancellationToken);
|
|
|
|
var invoice = Invoice.Create(
|
|
connection.Id,
|
|
request.Provider,
|
|
request.FileName,
|
|
storagePath,
|
|
request.FileSize);
|
|
|
|
await _invoiceRepository.AddAsync(invoice, cancellationToken);
|
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
|
|
|
request.FileStream.Position = 0;
|
|
var ocrResult = await _ocrService.ExtractAsync(request.FileStream, request.FileName, cancellationToken);
|
|
|
|
if (ocrResult.Success && ocrResult.Data != null)
|
|
{
|
|
var data = ocrResult.Data;
|
|
invoice.SetExtractionData(
|
|
System.Text.Json.JsonSerializer.Serialize(data),
|
|
ocrResult.Confidence,
|
|
data.SupplierName,
|
|
data.SupplierOrgNumber,
|
|
data.InvoiceNumber,
|
|
data.InvoiceDate,
|
|
data.DueDate,
|
|
data.AmountTotal,
|
|
data.AmountVat,
|
|
data.VatRate,
|
|
data.OcrNumber,
|
|
data.Bankgiro,
|
|
data.Plusgiro,
|
|
data.Currency);
|
|
|
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
|
}
|
|
|
|
var dto = MapToDto(invoice);
|
|
return new UploadInvoiceResult(true, null, dto);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new UploadInvoiceResult(false, ex.Message, null);
|
|
}
|
|
}
|
|
|
|
private static InvoiceDetailDto MapToDto(Invoice invoice)
|
|
{
|
|
return new InvoiceDetailDto(
|
|
invoice.Id,
|
|
invoice.Status.ToString().ToLower(),
|
|
invoice.Provider,
|
|
new FileInfoDto(
|
|
invoice.OriginalFilename,
|
|
invoice.FileSize,
|
|
$"/api/v1/invoices/{invoice.Id}/file"),
|
|
new ExtractionDataDto(
|
|
invoice.ExtractedSupplierName,
|
|
invoice.ExtractedSupplierOrgNumber,
|
|
invoice.ExtractedInvoiceNumber,
|
|
invoice.ExtractedInvoiceDate,
|
|
invoice.ExtractedDueDate,
|
|
invoice.ExtractedAmountTotal,
|
|
invoice.ExtractedAmountVat,
|
|
invoice.ExtractedVatRate,
|
|
invoice.ExtractedOcrNumber,
|
|
invoice.ExtractedBankgiro,
|
|
invoice.ExtractedPlusgiro,
|
|
invoice.ExtractedCurrency,
|
|
invoice.ExtractionConfidence),
|
|
invoice.SupplierNumber != null
|
|
? new SupplierMatchDto(
|
|
invoice.SupplierMatchAction?.ToString() ?? "USE_EXISTING",
|
|
invoice.SupplierNumber,
|
|
invoice.ExtractedSupplierName,
|
|
invoice.SupplierMatchConfidence)
|
|
: null,
|
|
null,
|
|
invoice.CreatedAt);
|
|
}
|
|
}
|