diff --git a/ImportAssist.slnx b/ImportAssist.slnx
index f65393d..5f2fc31 100644
--- a/ImportAssist.slnx
+++ b/ImportAssist.slnx
@@ -6,6 +6,9 @@
+
+
+
diff --git a/tests/ImportAssist.Core.Tests/ImportAssist.Core.Tests.csproj b/tests/ImportAssist.Core.Tests/ImportAssist.Core.Tests.csproj
new file mode 100644
index 0000000..0531d54
--- /dev/null
+++ b/tests/ImportAssist.Core.Tests/ImportAssist.Core.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/ImportAssist.Core.Tests/InvoiceCsvParserTests.cs b/tests/ImportAssist.Core.Tests/InvoiceCsvParserTests.cs
new file mode 100644
index 0000000..706dd03
--- /dev/null
+++ b/tests/ImportAssist.Core.Tests/InvoiceCsvParserTests.cs
@@ -0,0 +1,55 @@
+using ImportAssist.Core;
+using Xunit;
+
+namespace ImportAssist.Core.Tests;
+
+public sealed class InvoiceCsvParserTests
+{
+ private const string Header =
+ "InvoiceId;CustomerId;Amount;Currency";
+
+ [Fact]
+ public void Parse_PreservesEmptyFieldsAndRawAmount()
+ {
+ var csv = $"{Header}\nINV-001;;abc;EUR";
+
+ var rows = InvoiceCsvParser.Parse(csv);
+
+ var row = Assert.Single(rows);
+
+ Assert.Equal(
+ new InvoiceRow("INV-001", "", "abc", "EUR"),
+ row);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("InvoiceId;Amount")]
+ public void Parse_RejectsMissingOrIncorrectHeader(string csv)
+ {
+ var exception = Assert.Throws(
+ () => InvoiceCsvParser.Parse(csv));
+
+ Assert.Equal(
+ "Die Datei ist leer oder die Kopfzeile stimmt nicht.",
+ exception.Message);
+ }
+
+ [Theory]
+ [InlineData("INV-002;C-200;89.00")]
+ [InlineData("INV-002;C-200;89.00;EUR;extra")]
+ [InlineData("INV-002;\"C-200\";89.00;EUR")]
+ public void Parse_RejectsMalformedRowWithCorrectLineNumber(
+ string malformedRow)
+ {
+ var csv =
+ $"{Header}\nINV-001;C-100;125.50;EUR\n{malformedRow}";
+
+ var exception = Assert.Throws(
+ () => InvoiceCsvParser.Parse(csv));
+
+ Assert.Equal(
+ "Zeile 3: Erwartet werden vier Spalten ohne Anführungszeichen.",
+ exception.Message);
+ }
+}
\ No newline at end of file
diff --git a/tests/ImportAssist.Core.Tests/InvoiceValidatorTests.cs b/tests/ImportAssist.Core.Tests/InvoiceValidatorTests.cs
new file mode 100644
index 0000000..796f7a9
--- /dev/null
+++ b/tests/ImportAssist.Core.Tests/InvoiceValidatorTests.cs
@@ -0,0 +1,63 @@
+using System.Globalization;
+using ImportAssist.Core;
+using Xunit;
+
+namespace ImportAssist.Core.Tests;
+
+public sealed class InvoiceValidatorTests
+{
+ [Theory]
+ [InlineData("de-DE")]
+ [InlineData("en-US")]
+ public void Validate_AcceptsDecimalPointRegardlessOfCurrentCulture(
+ string cultureName)
+ {
+ var originalCulture = CultureInfo.CurrentCulture;
+
+ try
+ {
+ CultureInfo.CurrentCulture = new CultureInfo(cultureName);
+ var row = new InvoiceRow(
+ "INV-001", "C-100", "125.50", "EUR");
+
+ var errors = InvoiceValidator.Validate(row);
+
+ Assert.Empty(errors);
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = originalCulture;
+ }
+ }
+
+ [Theory]
+ [InlineData("abc", "Betrag ist keine gültige Zahl.")]
+ [InlineData("125,50", "Betrag ist keine gültige Zahl.")]
+ [InlineData("1,250.00", "Betrag ist keine gültige Zahl.")]
+ [InlineData("0", "Betrag muss größer als null sein.")]
+ [InlineData("-10.00", "Betrag muss größer als null sein.")]
+ public void Validate_RejectsInvalidAmount(
+ string amount,
+ string expectedError)
+ {
+ var row = new InvoiceRow(
+ "INV-001", "C-100", amount, "EUR");
+
+ var errors = InvoiceValidator.Validate(row);
+
+ Assert.Equal(expectedError, Assert.Single(errors));
+ }
+
+ [Fact]
+ public void Validate_CollectsAllErrorsForOneInvoice()
+ {
+ var row = new InvoiceRow(" ", "\t", "0", "EUR");
+
+ var errors = InvoiceValidator.Validate(row);
+
+ Assert.Equal(3, errors.Count);
+ Assert.Contains("Rechnungsnummer fehlt.", errors);
+ Assert.Contains("Kundennummer fehlt.", errors);
+ Assert.Contains("Betrag muss größer als null sein.", errors);
+ }
+}
\ No newline at end of file