diff --git a/src/AccountTracking.Api/Data/AppDbContext.cs b/src/AccountTracking.Api/Data/AppDbContext.cs new file mode 100644 index 0000000..18f7128 --- /dev/null +++ b/src/AccountTracking.Api/Data/AppDbContext.cs @@ -0,0 +1,31 @@ +using AccountTracking.Api.Models; +using Microsoft.EntityFrameworkCore; + +namespace AccountTracking.Api.Data; + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) { } + + public DbSet Transactions => Set(); + public DbSet ImportLogs => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // transaction_id: case-sensitive unique index using utf8mb4_bin + modelBuilder.Entity() + .HasIndex(t => t.TransactionId) + .IsUnique(); + + modelBuilder.Entity() + .Property(t => t.TransactionId) + .UseCollation("utf8mb4_bin"); + + // ImportedAt stored as UTC + modelBuilder.Entity() + .Property(l => l.ImportedAt) + .HasConversion( + v => DateTime.SpecifyKind(v, DateTimeKind.Utc), + v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); + } +} diff --git a/src/AccountTracking.Api/Models/Dtos/CategorySpendingDto.cs b/src/AccountTracking.Api/Models/Dtos/CategorySpendingDto.cs new file mode 100644 index 0000000..cf2e5e1 --- /dev/null +++ b/src/AccountTracking.Api/Models/Dtos/CategorySpendingDto.cs @@ -0,0 +1,2 @@ +namespace AccountTracking.Api.Models.Dtos; +public record CategorySpendingDto(string Category, decimal Total); diff --git a/src/AccountTracking.Api/Models/Dtos/CumulativeSpendingDto.cs b/src/AccountTracking.Api/Models/Dtos/CumulativeSpendingDto.cs new file mode 100644 index 0000000..e3bc737 --- /dev/null +++ b/src/AccountTracking.Api/Models/Dtos/CumulativeSpendingDto.cs @@ -0,0 +1,2 @@ +namespace AccountTracking.Api.Models.Dtos; +public record CumulativeSpendingDto(int Day, decimal CumulativeSpent); diff --git a/src/AccountTracking.Api/Models/Dtos/ImportResult.cs b/src/AccountTracking.Api/Models/Dtos/ImportResult.cs new file mode 100644 index 0000000..f2e8a2c --- /dev/null +++ b/src/AccountTracking.Api/Models/Dtos/ImportResult.cs @@ -0,0 +1,3 @@ +namespace AccountTracking.Api.Models.Dtos; +public record ImportResult(int RecordsImported, int RecordsSkipped); +public record ErrorResult(string Error); diff --git a/src/AccountTracking.Api/Models/Dtos/LoginRequest.cs b/src/AccountTracking.Api/Models/Dtos/LoginRequest.cs new file mode 100644 index 0000000..ed6161d --- /dev/null +++ b/src/AccountTracking.Api/Models/Dtos/LoginRequest.cs @@ -0,0 +1,2 @@ +namespace AccountTracking.Api.Models.Dtos; +public record LoginRequest(string Username, string Password); diff --git a/src/AccountTracking.Api/Models/Dtos/LoginResponse.cs b/src/AccountTracking.Api/Models/Dtos/LoginResponse.cs new file mode 100644 index 0000000..150bedd --- /dev/null +++ b/src/AccountTracking.Api/Models/Dtos/LoginResponse.cs @@ -0,0 +1,2 @@ +namespace AccountTracking.Api.Models.Dtos; +public record LoginResponse(string Token, string ExpiresAt); diff --git a/src/AccountTracking.Api/Models/Dtos/MonthlyBalanceDto.cs b/src/AccountTracking.Api/Models/Dtos/MonthlyBalanceDto.cs new file mode 100644 index 0000000..b98f29e --- /dev/null +++ b/src/AccountTracking.Api/Models/Dtos/MonthlyBalanceDto.cs @@ -0,0 +1,2 @@ +namespace AccountTracking.Api.Models.Dtos; +public record MonthlyBalanceDto(int Month, decimal ClosingBalance); diff --git a/src/AccountTracking.Api/Models/Dtos/SummaryDto.cs b/src/AccountTracking.Api/Models/Dtos/SummaryDto.cs new file mode 100644 index 0000000..c84ac0e --- /dev/null +++ b/src/AccountTracking.Api/Models/Dtos/SummaryDto.cs @@ -0,0 +1,2 @@ +namespace AccountTracking.Api.Models.Dtos; +public record SummaryDto(decimal TotalSpent, decimal TotalIncome); diff --git a/src/AccountTracking.Api/Models/Dtos/TransactionDto.cs b/src/AccountTracking.Api/Models/Dtos/TransactionDto.cs new file mode 100644 index 0000000..2decbf3 --- /dev/null +++ b/src/AccountTracking.Api/Models/Dtos/TransactionDto.cs @@ -0,0 +1,10 @@ +namespace AccountTracking.Api.Models.Dtos; +public record TransactionDto( + int Id, + DateOnly BookingDate, + string? CounterPartyName, + string? Category, + decimal Amount, + decimal Balance, + string? Message +); diff --git a/src/AccountTracking.Api/Models/Dtos/TransactionListResponse.cs b/src/AccountTracking.Api/Models/Dtos/TransactionListResponse.cs new file mode 100644 index 0000000..acffc75 --- /dev/null +++ b/src/AccountTracking.Api/Models/Dtos/TransactionListResponse.cs @@ -0,0 +1,7 @@ +namespace AccountTracking.Api.Models.Dtos; +public record TransactionListResponse( + IEnumerable Items, + int TotalCount, + int Page, + int PageSize +); diff --git a/src/AccountTracking.Api/Models/ImportLog.cs b/src/AccountTracking.Api/Models/ImportLog.cs new file mode 100644 index 0000000..33690fa --- /dev/null +++ b/src/AccountTracking.Api/Models/ImportLog.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace AccountTracking.Api.Models; + +[Table("import_logs")] +public class ImportLog +{ + [Key] + [Column("id")] + public int Id { get; set; } + + [Column("imported_at")] + public DateTime ImportedAt { get; set; } = DateTime.UtcNow; + + [Column("filename")] + [MaxLength(255)] + public string Filename { get; set; } = ""; + + [Column("records_imported")] + public int RecordsImported { get; set; } + + [Column("records_skipped")] + public int RecordsSkipped { get; set; } +} diff --git a/src/AccountTracking.Api/Models/Transaction.cs b/src/AccountTracking.Api/Models/Transaction.cs new file mode 100644 index 0000000..1758e7a --- /dev/null +++ b/src/AccountTracking.Api/Models/Transaction.cs @@ -0,0 +1,71 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace AccountTracking.Api.Models; + +[Table("transactions")] +public class Transaction +{ + [Key] + [Column("id")] + public int Id { get; set; } + + [Column("account_number")] + [MaxLength(30)] + public string AccountNumber { get; set; } = ""; + + [Column("booking_date")] + public DateOnly BookingDate { get; set; } + + [Column("amount")] + [Precision(15, 2)] + public decimal Amount { get; set; } + + [Column("currency")] + [MaxLength(3)] + public string Currency { get; set; } = "CZK"; + + [Column("balance")] + [Precision(15, 2)] + public decimal Balance { get; set; } + + [Column("counter_party_name")] + [MaxLength(255)] + public string? CounterPartyName { get; set; } + + [Column("operation_description")] + [MaxLength(255)] + public string? OperationDescription { get; set; } + + [Column("message")] + public string? Message { get; set; } + + [Column("category")] + [MaxLength(100)] + public string? Category { get; set; } + + [Column("variable_symbol")] + [MaxLength(30)] + public string? VariableSymbol { get; set; } + + [Column("bank_note")] + [MaxLength(255)] + public string? BankNote { get; set; } + + [Column("transaction_id")] + [MaxLength(512)] + public string TransactionId { get; set; } = ""; + + // Extra CSV columns stored but not used in UI + [Column("counter_bank_code")] [MaxLength(255)] public string? CounterBankCode { get; set; } + [Column("constant_symbol")] [MaxLength(255)] public string? ConstantSymbol { get; set; } + [Column("specific_symbol")] [MaxLength(255)] public string? SpecificSymbol { get; set; } + [Column("order_name")] [MaxLength(255)] public string? OrderName { get; set; } + [Column("exchange_rate")] [MaxLength(255)] public string? ExchangeRate { get; set; } + [Column("e2e_id")] [MaxLength(255)] public string? E2EId { get; set; } + [Column("payer_reference")] [MaxLength(255)] public string? PayerReference { get; set; } + [Column("original_payer")] [MaxLength(255)] public string? OriginalPayer { get; set; } + [Column("final_recipient")] [MaxLength(255)] public string? FinalRecipient { get; set; } + [Column("original_transaction")] [MaxLength(255)] public string? OriginalTransaction { get; set; } +}