diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c41cc9e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..175e6bb
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ com.pfefferminzia
+ pet-insurance
+ 1.0-SNAPSHOT
+
+
+ 25
+ UTF-8
+
+ 5.11.4
+ 3.13.0
+ 3.5.2
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.jupiter.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.jupiter.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit.jupiter.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven.compiler.plugin.version}
+
+ ${maven.compiler.release}
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven.surefire.plugin.version}
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/com/pfefferminzia/petinsurance/calculations/CalculationsTest.java b/src/test/java/com/pfefferminzia/petinsurance/calculations/CalculationsTest.java
new file mode 100644
index 0000000..52fd31a
--- /dev/null
+++ b/src/test/java/com/pfefferminzia/petinsurance/calculations/CalculationsTest.java
@@ -0,0 +1,175 @@
+package com.pfefferminzia.petinsurance.calculations;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import com.pfefferminzia.petinsurance.animals.Animal;
+import com.pfefferminzia.petinsurance.animals.Cat;
+import com.pfefferminzia.petinsurance.animals.Dog;
+import com.pfefferminzia.petinsurance.animals.SmallAnimal;
+import com.pfefferminzia.petinsurance.exception.InvalidInsuranceContractException;
+import com.pfefferminzia.petinsurance.tariffs.TariffConfiguration;
+import com.pfefferminzia.petinsurance.tariffs.TariffLoader;
+
+class CalculationsTest {
+ private static Calculations calculationsEngine;
+
+ @BeforeAll
+ static void setUp() {
+ TariffConfiguration tariffConfig = TariffLoader.loadFromFile();
+ calculationsEngine = new Calculations(tariffConfig);
+ }
+
+ static Stream dogBreedPremiumCases() {
+ return Stream.of(Arguments.of("Husky", new BigDecimal("3500"), new BigDecimal("980")),
+ Arguments.of("Dackel", new BigDecimal("3500"), new BigDecimal("882")),
+ Arguments.of("Bernhardiner", new BigDecimal("3500"), new BigDecimal("1078")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("dogBreedPremiumCases")
+ void shouldCalculateCorrectlyForDogSevenYearsAndPremiumTariff(String breed,
+ BigDecimal expectedSum, BigDecimal expectedPremium) {
+
+ Animal dog = new Dog(LocalDate.of(2019, 8, 1), breed);
+ InsuranceContract contract =
+ new InsuranceContract(dog, LocalDate.of(2026, 8, 1), "PREMIUM");
+
+ CalculationResult result =
+ calculationsEngine.calculateInsuranceSumAndYearlyPremium(contract);
+
+ assertBigDecimalEquals(expectedSum, result.insuranceSum());
+ assertBigDecimalEquals(expectedPremium, result.yearlyPremium());
+ }
+
+ @Test
+ void shouldCalculateCorrectlyForCatThreeYearsAndOptimalTariff() {
+ Animal cat = new Cat(LocalDate.of(2023, 8, 1));
+ InsuranceContract contract =
+ new InsuranceContract(cat, LocalDate.of(2026, 8, 1), "OPTIMAL");
+
+ CalculationResult result =
+ calculationsEngine.calculateInsuranceSumAndYearlyPremium(contract);
+
+ assertBigDecimalEquals(new BigDecimal("2400"), result.insuranceSum());
+ assertBigDecimalEquals(new BigDecimal("600"), result.yearlyPremium());
+ }
+
+ @Test
+ void shouldCalculateCorrectlyForSmallAnimalOneYearAndKompaktTariff() {
+ Animal smallAnimal = new SmallAnimal(LocalDate.of(2025, 8, 1));
+ InsuranceContract contract =
+ new InsuranceContract(smallAnimal, LocalDate.of(2026, 8, 1), "KOMPAKT");
+
+ CalculationResult result =
+ calculationsEngine.calculateInsuranceSumAndYearlyPremium(contract);
+
+ assertBigDecimalEquals(new BigDecimal("1000"), result.insuranceSum());
+ assertBigDecimalEquals(new BigDecimal("200"), result.yearlyPremium());
+ }
+
+ static Stream ageBracketCases() {
+ return Stream.of(
+ Arguments.of(2, new BigDecimal("200")), // exactly at ≤2 boundary → factor 0.20
+ Arguments.of(3, new BigDecimal("250")), // one above ≤2 boundary → factor 0.25
+ Arguments.of(5, new BigDecimal("250")), // exactly at ≤5 boundary → factor 0.25
+ Arguments.of(6, new BigDecimal("280")), // one above ≤5 boundary → factor 0.28
+ Arguments.of(7, new BigDecimal("280")), // exactly at ≤7 boundary → factor 0.28
+ Arguments.of(8, new BigDecimal("300")) // one above ≤7 boundary → factor 0.30
+ );
+ }
+
+ @ParameterizedTest(name = "age {0} → premium {1}")
+ @MethodSource("ageBracketCases")
+ void shouldApplyCorrectAgeFactorAtEachBracketBoundary(int age, BigDecimal expectedPremium) {
+ LocalDate startDate = LocalDate.of(2026, 8, 1);
+ Animal animal = new SmallAnimal(startDate.minusYears(age));
+ InsuranceContract contract = new InsuranceContract(animal, startDate, "KOMPAKT");
+
+ CalculationResult result =
+ calculationsEngine.calculateInsuranceSumAndYearlyPremium(contract);
+
+ assertBigDecimalEquals(expectedPremium, result.yearlyPremium());
+ }
+
+ @Test
+ void shouldThrowExceptionWhenPremiumTariffIsSelectedBeforeJulyFirst() {
+ Animal dog = new Dog(LocalDate.of(2019, 8, 1), "Schäferhund");
+ InsuranceContract contract =
+ new InsuranceContract(dog, LocalDate.of(2026, 2, 1), "PREMIUM");
+
+ assertThrows(IllegalStateException.class,
+ () -> calculationsEngine.calculateInsuranceSumAndYearlyPremium(contract));
+ }
+
+ @Test
+ void shouldReportViolationWhenAnimalIsNull() {
+ LocalDate insuranceStart = LocalDate.of(2026, 8, 1);
+
+ InvalidInsuranceContractException ex = assertThrows(InvalidInsuranceContractException.class,
+ () -> new InsuranceContract(null, insuranceStart, "KOMPAKT"));
+
+ assertEquals(List.of("Animal darf nicht leer sein"), ex.getViolations());
+ }
+
+ @Test
+ void shouldReportViolationWhenTariffNameIsNull() {
+ Animal animal = new Cat(LocalDate.of(2023, 8, 1));
+ LocalDate insuranceStart = LocalDate.of(2026, 8, 1);
+
+
+ InvalidInsuranceContractException ex = assertThrows(InvalidInsuranceContractException.class,
+ () -> new InsuranceContract(animal, insuranceStart, null));
+
+ assertEquals(List.of("Tarif darf nicht leer sein"), ex.getViolations());
+ }
+
+ @Test
+ void shouldReportViolationWhenInsuranceStartDateIsNull() {
+ Animal animal = new Cat(LocalDate.of(2023, 8, 1));
+
+ InvalidInsuranceContractException ex = assertThrows(InvalidInsuranceContractException.class,
+ () -> new InsuranceContract(animal, null, "KOMPAKT"));
+
+ assertEquals(List.of("Versicherungsstart darf nicht leer sein"), ex.getViolations());
+ }
+
+ @Test
+ void shouldReportViolationWhenBirthdayIsAfterContractStart() {
+ Animal animal = new Cat(LocalDate.of(2026, 8, 1));
+ LocalDate insuranceStart = LocalDate.of(2026, 2, 1);
+
+ InvalidInsuranceContractException ex = assertThrows(InvalidInsuranceContractException.class,
+ () -> new InsuranceContract(animal, insuranceStart, "KOMPAKT"));
+
+ assertEquals(List.of("Start der Versicherung kann nicht vor dem Geburtstag sein."),
+ ex.getViolations());
+ }
+
+ @Test
+ void shouldCollectAllViolationsWhenMultipleFieldsAreInvalid() {
+ InvalidInsuranceContractException ex = assertThrows(InvalidInsuranceContractException.class,
+ () -> new InsuranceContract(null, null, null));
+
+ assertEquals(
+ List.of("Animal darf nicht leer sein", "Tarif darf nicht leer sein",
+ "Versicherungsstart darf nicht leer sein"),
+ ex.getViolations());
+ }
+
+ private void assertBigDecimalEquals(BigDecimal expected, BigDecimal actual) {
+ if (expected.compareTo(actual) != 0) {
+ fail(String.format("Expected: <%s> but is: <%s>", expected.toPlainString(),
+ actual.toPlainString()));
+ }
+ }
+}
diff --git a/src/test/resources/tariff.properties b/src/test/resources/tariff.properties
new file mode 100644
index 0000000..74dd508
--- /dev/null
+++ b/src/test/resources/tariff.properties
@@ -0,0 +1,20 @@
+
+# Animal base insurance sums: species=baseSum
+baseSum.DOG=2500.00
+baseSum.CAT=2000.00
+baseSum.SMALLANIMAL=1000.00
+
+# Tariffs: name=factor,availableFromDate
+tariffs.KOMPAKT=1.0,1970-01-01
+tariffs.OPTIMAL=1.2,1970-01-01
+tariffs.PREMIUM=1.4,2026-07-01
+
+# Age tariffs factor: age=factor
+age.2=0.20
+age.5=0.25
+age.7=0.28
+elderlyFactor=0.30
+
+# Dog breed premium factor: breed=factor
+breed.DACKEL=-0.1
+breed.BERNHARDINER=0.1
\ No newline at end of file