Files
accounting-system/backend/src/InvoiceMaster.Application/Commands/Invoices/UploadInvoiceCommand.cs
Invoice Master 05ea67144f feat: initial project setup
- 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
2026-02-04 20:14:34 +01:00

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