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
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user