feat: implement CSV import service with tests (TDD)
This commit is contained in:
parent
8f4eaeaa74
commit
fec96d7727
123
src/AccountTracking.Api.Tests/CsvImportServiceTests.cs
Normal file
123
src/AccountTracking.Api.Tests/CsvImportServiceTests.cs
Normal file
@ -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<AppDbContext>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user