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:
Invoice Master
2026-02-04 20:14:34 +01:00
commit 05ea67144f
250 changed files with 50402 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
using InvoiceMaster.Core.Entities;
using Microsoft.EntityFrameworkCore;
namespace InvoiceMaster.Infrastructure.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<User> Users => Set<User>();
public DbSet<AccountingConnection> AccountingConnections => Set<AccountingConnection>();
public DbSet<Invoice> Invoices => Set<Invoice>();
public DbSet<SupplierCache> SupplierCaches => Set<SupplierCache>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
}
}

View File

@@ -0,0 +1,49 @@
using InvoiceMaster.Core.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace InvoiceMaster.Infrastructure.Data.Configurations;
public class AccountingConnectionConfiguration : IEntityTypeConfiguration<AccountingConnection>
{
public void Configure(EntityTypeBuilder<AccountingConnection> builder)
{
builder.ToTable("accounting_connections");
builder.HasKey(e => e.Id);
builder.Property(e => e.Provider)
.IsRequired()
.HasMaxLength(50);
builder.Property(e => e.AccessTokenEncrypted)
.IsRequired();
builder.Property(e => e.RefreshTokenEncrypted)
.IsRequired();
builder.Property(e => e.CompanyName)
.HasMaxLength(255);
builder.Property(e => e.CompanyOrgNumber)
.HasMaxLength(20);
builder.Property(e => e.DefaultVoucherSeries)
.HasMaxLength(10)
.HasDefaultValue("A");
builder.Property(e => e.Scope);
builder.HasIndex(e => new { e.UserId, e.Provider })
.IsUnique();
builder.HasIndex(e => e.Provider);
builder.HasIndex(e => e.IsActive);
builder.HasIndex(e => e.ExpiresAt);
builder.HasOne(e => e.User)
.WithMany(u => u.Connections)
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@@ -0,0 +1,75 @@
using InvoiceMaster.Core.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace InvoiceMaster.Infrastructure.Data.Configurations;
public class InvoiceConfiguration : IEntityTypeConfiguration<Invoice>
{
public void Configure(EntityTypeBuilder<Invoice> builder)
{
builder.ToTable("invoices");
builder.HasKey(e => e.Id);
builder.Property(e => e.Provider)
.IsRequired()
.HasMaxLength(50);
builder.Property(e => e.OriginalFilename)
.IsRequired()
.HasMaxLength(255);
builder.Property(e => e.StoragePath)
.IsRequired();
builder.Property(e => e.FileHash)
.HasMaxLength(64);
builder.Property(e => e.ExtractedSupplierName)
.HasMaxLength(255);
builder.Property(e => e.ExtractedSupplierOrgNumber)
.HasMaxLength(20);
builder.Property(e => e.ExtractedInvoiceNumber)
.HasMaxLength(100);
builder.Property(e => e.ExtractedOcrNumber)
.HasMaxLength(50);
builder.Property(e => e.ExtractedBankgiro)
.HasMaxLength(50);
builder.Property(e => e.ExtractedPlusgiro)
.HasMaxLength(50);
builder.Property(e => e.ExtractedCurrency)
.HasMaxLength(3)
.HasDefaultValue("SEK");
builder.Property(e => e.SupplierNumber)
.HasMaxLength(50);
builder.Property(e => e.VoucherSeries)
.HasMaxLength(10);
builder.Property(e => e.VoucherNumber)
.HasMaxLength(50);
builder.Property(e => e.ErrorCode)
.HasMaxLength(50);
builder.HasIndex(e => e.ConnectionId);
builder.HasIndex(e => e.Provider);
builder.HasIndex(e => e.Status);
builder.HasIndex(e => e.CreatedAt);
builder.HasIndex(e => e.FileHash);
builder.HasIndex(e => e.ExtractedSupplierOrgNumber);
builder.HasOne(e => e.Connection)
.WithMany(c => c.Invoices)
.HasForeignKey(e => e.ConnectionId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@@ -0,0 +1,66 @@
using InvoiceMaster.Core.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace InvoiceMaster.Infrastructure.Data.Configurations;
public class SupplierCacheConfiguration : IEntityTypeConfiguration<SupplierCache>
{
public void Configure(EntityTypeBuilder<SupplierCache> builder)
{
builder.ToTable("supplier_cache");
builder.HasKey(e => e.Id);
builder.Property(e => e.SupplierNumber)
.IsRequired()
.HasMaxLength(50);
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(255);
builder.Property(e => e.OrganisationNumber)
.HasMaxLength(20);
builder.Property(e => e.Address1)
.HasMaxLength(255);
builder.Property(e => e.Address2)
.HasMaxLength(255);
builder.Property(e => e.Postcode)
.HasMaxLength(20);
builder.Property(e => e.City)
.HasMaxLength(100);
builder.Property(e => e.Country)
.HasMaxLength(100);
builder.Property(e => e.Phone)
.HasMaxLength(50);
builder.Property(e => e.Email)
.HasMaxLength(255);
builder.Property(e => e.BankgiroNumber)
.HasMaxLength(50);
builder.Property(e => e.PlusgiroNumber)
.HasMaxLength(50);
builder.HasIndex(e => new { e.ConnectionId, e.SupplierNumber })
.IsUnique();
builder.HasIndex(e => e.ConnectionId);
builder.HasIndex(e => e.OrganisationNumber);
builder.HasIndex(e => e.Name);
builder.HasIndex(e => e.ExpiresAt);
builder.HasOne(e => e.Connection)
.WithMany()
.HasForeignKey(e => e.ConnectionId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@@ -0,0 +1,37 @@
using InvoiceMaster.Core.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace InvoiceMaster.Infrastructure.Data.Configurations;
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("users");
builder.HasKey(e => e.Id);
builder.Property(e => e.Email)
.IsRequired()
.HasMaxLength(255);
builder.HasIndex(e => e.Email)
.IsUnique();
builder.Property(e => e.HashedPassword)
.IsRequired()
.HasMaxLength(255);
builder.Property(e => e.FullName)
.HasMaxLength(255);
builder.Property(e => e.IsActive)
.HasDefaultValue(true);
builder.Property(e => e.IsSuperuser)
.HasDefaultValue(false);
builder.HasIndex(e => e.IsActive);
}
}