From fec96d77272c8abe5008062affdfbf9bbabc6069 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 20 Mar 2026 00:45:59 +0100 Subject: [PATCH] feat: implement CSV import service with tests (TDD) --- .../CsvImportServiceTests.cs | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/AccountTracking.Api.Tests/CsvImportServiceTests.cs diff --git a/src/AccountTracking.Api.Tests/CsvImportServiceTests.cs b/src/AccountTracking.Api.Tests/CsvImportServiceTests.cs new file mode 100644 index 0000000..55afd6c --- /dev/null +++ b/src/AccountTracking.Api.Tests/CsvImportServiceTests.cs @@ -0,0 +1,123 @@ +using AccountTracking.Api.Data; +using AccountTracking.Api.Services; +using Microsoft.EntityFrameworkCore; + +namespace AccountTracking.Api.Tests; + +public class CsvImportServiceTests +{ + private static AppDbContext CreateDb() + { + var opts = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new AppDbContext(opts); + } + + private static CsvImportService CreateService(AppDbContext db) + => new(db); + + // Minimal valid CSV with two data rows + private const string ValidCsv = """ + Pohyby na účtu 216868554/0300 dne 19.03.2026 + + číslo účtu;datum zaúčtování;částka;měna;zůstatek;číslo protiúčtu;kód banky protiúčtu;jméno protistrany;adresa protistrany;konstantní symbol;variabilní symbol;specifický symbol;označení operace;název trálého příkazu;vlastní poznámka;zpráva;kategorie;původní transakce;kurz;E2E identifikace;reference plátce;původní plátce;konečný příjemce;ID transakce + 216868554/0300;15.03.2026;-1 740,80;CZK;93 346,57;;;;;;205000001;;Transakce platební kartou;;;Zpráva 1;Potraviny;;;;;;;tx-001 + 216868554/0300;14.03.2026;-800,00;CZK;95 087,37;;;;;;205000002;;Transakce platební kartou;;;Zpráva 2;Restaurace;;;;;;;tx-002 + """; + + private const string CsvMissingHeader = """ + Pohyby na účtu + Nějaky obsah bez správné hlavičky + datum;částka + """; + + private const string CsvMissingRequiredColumn = """ + číslo účtu;datum zaúčtování;měna;zůstatek;ID transakce + 216868554/0300;15.03.2026;CZK;93346,57;tx-001 + """; + + [Fact] + public async Task Import_ParsesRowsCorrectly() + { + using var db = CreateDb(); + var svc = CreateService(db); + + var result = await svc.ImportAsync(ValidCsv, "test.csv"); + + Assert.True(result.IsSuccess); + Assert.Equal(2, result.Value!.RecordsImported); + Assert.Equal(0, result.Value.RecordsSkipped); + + var transactions = await db.Transactions.ToListAsync(); + Assert.Equal(2, transactions.Count); + + var tx = transactions.First(t => t.TransactionId == "tx-001"); + Assert.Equal(-1740.80m, tx.Amount); + Assert.Equal(93346.57m, tx.Balance); + Assert.Equal("Potraviny", tx.Category); + Assert.Equal(new DateOnly(2026, 3, 15), tx.BookingDate); + } + + [Fact] + public async Task Import_SkipsDuplicateTransactionIds() + { + using var db = CreateDb(); + var svc = CreateService(db); + + await svc.ImportAsync(ValidCsv, "first.csv"); + var result = await svc.ImportAsync(ValidCsv, "second.csv"); + + Assert.True(result.IsSuccess); + Assert.Equal(0, result.Value!.RecordsImported); + Assert.Equal(2, result.Value.RecordsSkipped); + Assert.Equal(2, await db.Transactions.CountAsync()); + } + + [Fact] + public async Task Import_FindsHeaderByScanning_NotByLineNumber() + { + // Header is on line 3 in ValidCsv — but we scan, not hardcode + using var db = CreateDb(); + var svc = CreateService(db); + + var result = await svc.ImportAsync(ValidCsv, "test.csv"); + + Assert.True(result.IsSuccess); + } + + [Fact] + public async Task Import_ReturnsError_WhenHeaderNotFound() + { + using var db = CreateDb(); + var svc = CreateService(db); + + var result = await svc.ImportAsync(CsvMissingHeader, "bad.csv"); + + Assert.False(result.IsSuccess); + Assert.Contains("hlavičku", result.Error, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task Import_ReturnsError_WhenRequiredColumnMissing() + { + using var db = CreateDb(); + var svc = CreateService(db); + + var result = await svc.ImportAsync(CsvMissingRequiredColumn, "bad.csv"); + + Assert.False(result.IsSuccess); + Assert.Contains("částka", result.Error, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData("1 740,80", 1740.80)] + [InlineData("-4 263,15", -4263.15)] + [InlineData("-800,00", -800.00)] + [InlineData("93 346,57", 93346.57)] + public void ParseAmount_HandlesChechLocaleFormat(string input, decimal expected) + { + var result = CsvImportService.ParseCzechDecimal(input); + Assert.Equal(expected, result); + } +}