diff --git a/src/main/java/com/pfefferminzia/petinsurance/Main.java b/src/main/java/com/pfefferminzia/petinsurance/Main.java deleted file mode 100644 index 360892b..0000000 --- a/src/main/java/com/pfefferminzia/petinsurance/Main.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.pfefferminzia.petinsurance; - -import java.time.LocalDate; -import com.pfefferminzia.petinsurance.animals.Dog; -import com.pfefferminzia.petinsurance.calculations.InsuranceContract; -import com.pfefferminzia.petinsurance.tariffs.Tariff; - -public class Main { - public static void main(String[] args) { - System.out.println(new InsuranceContract(new Dog(LocalDate.of(2025, 1, 1), "Dackel"), - LocalDate.of(2026, 1, 1), Tariff.OPTIMAL)); - } -} diff --git a/src/main/java/com/pfefferminzia/petinsurance/animals/Animal.java b/src/main/java/com/pfefferminzia/petinsurance/animals/Animal.java index 47fe963..2ad2c18 100644 --- a/src/main/java/com/pfefferminzia/petinsurance/animals/Animal.java +++ b/src/main/java/com/pfefferminzia/petinsurance/animals/Animal.java @@ -1,15 +1,8 @@ package com.pfefferminzia.petinsurance.animals; -import java.math.BigDecimal; import java.time.LocalDate; public sealed interface Animal permits Dog, Cat, SmallAnimal { LocalDate birthday(); - - BigDecimal getBaseInsuranceSum(); - - default BigDecimal getPremiumAdjustment() { - return BigDecimal.ZERO; - } } diff --git a/src/main/java/com/pfefferminzia/petinsurance/animals/Cat.java b/src/main/java/com/pfefferminzia/petinsurance/animals/Cat.java index 921a439..d2c73ad 100644 --- a/src/main/java/com/pfefferminzia/petinsurance/animals/Cat.java +++ b/src/main/java/com/pfefferminzia/petinsurance/animals/Cat.java @@ -1,12 +1,6 @@ package com.pfefferminzia.petinsurance.animals; -import java.math.BigDecimal; import java.time.LocalDate; public final record Cat(LocalDate birthday) implements Animal { - - @Override - public BigDecimal getBaseInsuranceSum() { - return BigDecimal.valueOf(2000.0); - } } diff --git a/src/main/java/com/pfefferminzia/petinsurance/animals/Dog.java b/src/main/java/com/pfefferminzia/petinsurance/animals/Dog.java index 40f8156..b6fc470 100644 --- a/src/main/java/com/pfefferminzia/petinsurance/animals/Dog.java +++ b/src/main/java/com/pfefferminzia/petinsurance/animals/Dog.java @@ -1,18 +1,11 @@ package com.pfefferminzia.petinsurance.animals; -import java.math.BigDecimal; import java.time.LocalDate; -import com.pfefferminzia.petinsurance.tariffs.BreedPremiumAdjustment; public final record Dog(LocalDate birthday, String breed) implements Animal { @Override - public BigDecimal getBaseInsuranceSum() { - return BigDecimal.valueOf(2500.0); - } - - @Override - public BigDecimal getPremiumAdjustment() { - return BreedPremiumAdjustment.forBreed(breed); + public String breed() { + return (this.breed == null || this.breed.isBlank()) ? "" : this.breed; } } diff --git a/src/main/java/com/pfefferminzia/petinsurance/animals/SmallAnimal.java b/src/main/java/com/pfefferminzia/petinsurance/animals/SmallAnimal.java index 9e9fc89..e4b8dc6 100644 --- a/src/main/java/com/pfefferminzia/petinsurance/animals/SmallAnimal.java +++ b/src/main/java/com/pfefferminzia/petinsurance/animals/SmallAnimal.java @@ -1,12 +1,6 @@ package com.pfefferminzia.petinsurance.animals; -import java.math.BigDecimal; import java.time.LocalDate; public final record SmallAnimal(LocalDate birthday) implements Animal { - - @Override - public BigDecimal getBaseInsuranceSum() { - return BigDecimal.valueOf(1000.0); - } } diff --git a/src/main/java/com/pfefferminzia/petinsurance/calculations/Calculations.java b/src/main/java/com/pfefferminzia/petinsurance/calculations/Calculations.java index 224aeaf..4d2a970 100644 --- a/src/main/java/com/pfefferminzia/petinsurance/calculations/Calculations.java +++ b/src/main/java/com/pfefferminzia/petinsurance/calculations/Calculations.java @@ -3,23 +3,34 @@ package com.pfefferminzia.petinsurance.calculations; import java.math.BigDecimal; import java.time.LocalDate; import java.time.temporal.ChronoUnit; -import com.pfefferminzia.petinsurance.tariffs.AgeTariff; -import com.pfefferminzia.petinsurance.tariffs.Tariff; +import com.pfefferminzia.petinsurance.animals.*; +import com.pfefferminzia.petinsurance.tariffs.TariffConfiguration; +import com.pfefferminzia.petinsurance.tariffs.TariffOption; public class Calculations { - public CalculationResult result(InsuranceContract contract) { + private final TariffConfiguration tariffConfig; - Tariff tariff = contract.tariff(); + public Calculations(TariffConfiguration tariffConfig) { + this.tariffConfig = tariffConfig; + } - BigDecimal insuranceSum = - getInsuranceSum(contract.animal().getBaseInsuranceSum(), tariff.getRate()); + public CalculationResult calculateInsuranceSumAndYearlyPremium(InsuranceContract contract) { - BigDecimal ageFactor = - ageFactorLookup(contract.animal().birthday(), contract.insuranceStartDate()); + Animal animal = contract.animal(); + LocalDate insuranceStartDate = contract.insuranceStartDate(); + TariffOption tariff = + tariffConfig.getTariffFactor(contract.tariffName(), insuranceStartDate); - BigDecimal breedPremiumAdjustment = contract.animal().getPremiumAdjustment(); + String speciesKey = animal.getClass().getSimpleName().toUpperCase(); + BigDecimal baseInsuranceSum = tariffConfig.getAnimalBaseSum(speciesKey); + + BigDecimal insuranceSum = getInsuranceSum(baseInsuranceSum, tariff.factor()); + + BigDecimal ageFactor = ageFactorLookup(contract.animal().birthday(), insuranceStartDate); + + BigDecimal breedPremiumAdjustment = breedFactorLookup(animal); BigDecimal yearlyPremium = insuranceSum.multiply(ageFactor) .multiply(BigDecimal.ONE.add(breedPremiumAdjustment)); @@ -27,18 +38,22 @@ public class Calculations { return new CalculationResult(insuranceSum, yearlyPremium); } - BigDecimal getInsuranceSum(BigDecimal baseInsuranceSum, BigDecimal tariffRate) { + private BigDecimal getInsuranceSum(BigDecimal baseInsuranceSum, BigDecimal tariffRate) { + return baseInsuranceSum.multiply(tariffRate); } - BigDecimal ageFactorLookup(LocalDate birthday, LocalDate insuranceStartDate) { - if (insuranceStartDate.isBefore(birthday)) { - throw new IllegalArgumentException( - "Start der Versicherung kann nicht vor dem Geburtstag sein."); - } + private BigDecimal ageFactorLookup(LocalDate birthday, LocalDate insuranceStartDate) { long exactYears = ChronoUnit.YEARS.between(birthday, insuranceStartDate); - return AgeTariff.getFactorForAge(Math.toIntExact(exactYears)); + return tariffConfig.getAgeFactor(Math.toIntExact(exactYears)); + } + + private BigDecimal breedFactorLookup(Animal animal) { + return switch (animal) { + case Dog dog -> tariffConfig.getBreedFactor(dog.breed()); + case Cat _,SmallAnimal _ -> BigDecimal.ZERO; + }; } } diff --git a/src/main/java/com/pfefferminzia/petinsurance/calculations/InsuranceContract.java b/src/main/java/com/pfefferminzia/petinsurance/calculations/InsuranceContract.java index 5b1d536..65222af 100644 --- a/src/main/java/com/pfefferminzia/petinsurance/calculations/InsuranceContract.java +++ b/src/main/java/com/pfefferminzia/petinsurance/calculations/InsuranceContract.java @@ -5,21 +5,28 @@ import java.util.ArrayList; import java.util.List; import com.pfefferminzia.petinsurance.animals.Animal; import com.pfefferminzia.petinsurance.exception.InvalidInsuranceContractException; -import com.pfefferminzia.petinsurance.tariffs.Tariff; -public record InsuranceContract(Animal animal, LocalDate insuranceStartDate, Tariff tariff) { - - private static final LocalDate PREMIUM_AVAILABLE_FROM = LocalDate.of(2026, 7, 1); +public record InsuranceContract(Animal animal, LocalDate insuranceStartDate, String tariffName) { public InsuranceContract { + List violations = new ArrayList<>(); - if (animal.birthday().isAfter(insuranceStartDate)) { - violations.add("Start der Versicherung kann nicht vor dem Geburtstag sein."); + if (animal() == null) { + violations.add("Animal darf nicht leer sein"); } - if (insuranceStartDate.isBefore(PREMIUM_AVAILABLE_FROM) && tariff == Tariff.PREMIUM) { - violations.add("Die Premium Tarifvariante ist nicht vor dem 1. Juli 2026 verfügbar."); + if (tariffName == null) { + violations.add("Tarif darf nicht leer sein"); + } + + if (insuranceStartDate == null) { + violations.add("Versicherungsstart darf nicht leer sein"); + } + + if (animal() != null && insuranceStartDate != null + && animal.birthday().isAfter(insuranceStartDate)) { + violations.add("Start der Versicherung kann nicht vor dem Geburtstag sein."); } if (!violations.isEmpty()) { diff --git a/src/main/java/com/pfefferminzia/petinsurance/tariffs/AgeTariff.java b/src/main/java/com/pfefferminzia/petinsurance/tariffs/AgeTariff.java deleted file mode 100644 index 4fde91a..0000000 --- a/src/main/java/com/pfefferminzia/petinsurance/tariffs/AgeTariff.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.pfefferminzia.petinsurance.tariffs; - -import java.math.BigDecimal; - -public enum AgeTariff { - UP_TO_TWO(2, new BigDecimal("0.2")), UP_TO_FIVE(5, new BigDecimal("0.25")), UP_TO_SEVEN(7, - new BigDecimal("0.28")), OVER_SEVEN(Integer.MAX_VALUE, new BigDecimal("0.3")); - - private final int maxAge; - - private final BigDecimal factor; - - private AgeTariff(int maxAge, BigDecimal factor) { - this.maxAge = maxAge; - this.factor = factor; - } - - public static BigDecimal getFactorForAge(int age) { - for (AgeTariff rate : values()) { - if (age <= rate.maxAge) { - return rate.factor; - } - } - throw new AssertionError("Unexpected age: " + age); - } - -} diff --git a/src/main/java/com/pfefferminzia/petinsurance/tariffs/BreedPremiumAdjustment.java b/src/main/java/com/pfefferminzia/petinsurance/tariffs/BreedPremiumAdjustment.java deleted file mode 100644 index 168d184..0000000 --- a/src/main/java/com/pfefferminzia/petinsurance/tariffs/BreedPremiumAdjustment.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.pfefferminzia.petinsurance.tariffs; - -import java.math.BigDecimal; -import java.util.Map; - -public final class BreedPremiumAdjustment { - - private static final Map ADJUSTMENTS = Map.of( - "Dackel", new BigDecimal("-0.1"), - "Bernhardiner", new BigDecimal("0.1") - ); - - public static BigDecimal forBreed(String breed) { - return ADJUSTMENTS.getOrDefault(breed, BigDecimal.ZERO); - } - - private BreedPremiumAdjustment() {} -} diff --git a/src/main/java/com/pfefferminzia/petinsurance/tariffs/Tariff.java b/src/main/java/com/pfefferminzia/petinsurance/tariffs/Tariff.java deleted file mode 100644 index 32d73b3..0000000 --- a/src/main/java/com/pfefferminzia/petinsurance/tariffs/Tariff.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.pfefferminzia.petinsurance.tariffs; - -import java.math.BigDecimal; - -public enum Tariff { - KOMPAKT(BigDecimal.ONE), OPTIMAL(new BigDecimal("1.2")), PREMIUM(new BigDecimal("1.4")); - - private final BigDecimal rate; - - private Tariff(BigDecimal rate) { - this.rate = rate; - } - - public BigDecimal getRate() { - return rate; - } -} diff --git a/src/main/java/com/pfefferminzia/petinsurance/tariffs/TariffConfiguration.java b/src/main/java/com/pfefferminzia/petinsurance/tariffs/TariffConfiguration.java new file mode 100644 index 0000000..6ae57a7 --- /dev/null +++ b/src/main/java/com/pfefferminzia/petinsurance/tariffs/TariffConfiguration.java @@ -0,0 +1,74 @@ +package com.pfefferminzia.petinsurance.tariffs; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collections; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; + +public record TariffConfiguration(Map animalBaseSums, + Map tariffs, NavigableMap ageFactors, + Map breedFactors) { + + public TariffConfiguration { + animalBaseSums = Collections.unmodifiableMap(animalBaseSums); + tariffs = Collections.unmodifiableMap(tariffs); + ageFactors = Collections.unmodifiableNavigableMap(new TreeMap<>(ageFactors)); + breedFactors = Collections.unmodifiableMap(breedFactors); + } + + public BigDecimal getAnimalBaseSum(String species) { + if (species == null || species.isBlank()) { + throw new IllegalArgumentException("Tierart darf nicht leer oder null sein."); + } + + String normalizedKey = species.trim().toUpperCase(); + BigDecimal baseSum = animalBaseSums.get(normalizedKey); + + if (baseSum == null) { + throw new IllegalArgumentException( + "Keine Basis-Versicherungssumme für Tierart konfiguriert: " + species); + } + + return baseSum; + } + + public TariffOption getTariffFactor(String tariffName, LocalDate startDate) { + + TariffOption option = tariffs.get(tariffName.toUpperCase()); + + if (option == null) { + throw new IllegalArgumentException("Unbekannte Tarifvariante: " + tariffName); + } + + if (!option.isAvailableAt(startDate)) { + throw new IllegalStateException("Die Tarifvariante " + tariffName + " ist zum " + + startDate + " noch nicht verfügbar."); + } + + return option; + } + + public BigDecimal getAgeFactor(int age) { + if (age < 0) { + throw new IllegalArgumentException("Alter kann nicht negativ sein: " + age); + } + + var entry = ageFactors.ceilingEntry(age); + if (entry == null) { + throw new IllegalStateException("Alter überschreitet maximal zulässigen Systemwert."); + } + + return entry.getValue(); + } + + public BigDecimal getBreedFactor(String breed) { + if (breed == null || breed.isBlank()) { + return BigDecimal.ZERO; + } + + return breedFactors.getOrDefault(breed.trim().toUpperCase(), BigDecimal.ZERO); + } + +} diff --git a/src/main/java/com/pfefferminzia/petinsurance/tariffs/TariffLoader.java b/src/main/java/com/pfefferminzia/petinsurance/tariffs/TariffLoader.java new file mode 100644 index 0000000..b153e0b --- /dev/null +++ b/src/main/java/com/pfefferminzia/petinsurance/tariffs/TariffLoader.java @@ -0,0 +1,98 @@ +package com.pfefferminzia.petinsurance.tariffs; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Properties; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public final class TariffLoader { + private static final String CONFIG_FILE_NAME = "tariff.properties"; + + public static TariffConfiguration loadFromFile() { + Properties props = new Properties(); + + try (InputStream input = + TariffLoader.class.getClassLoader().getResourceAsStream(CONFIG_FILE_NAME)) { + if (input == null) { + throw new IllegalStateException( + "Konfigurationsdatei nicht verfügbar: " + CONFIG_FILE_NAME); + } + props.load(input); + } catch (IOException e) { + throw new IllegalStateException("Fehler beim Auslesen der Tarif-Konfigurationsdatei.", + e); + } + + return new TariffConfiguration(animalBaseSums(props), tariffs(props), ageFactors(props), + breedFactors(props)); + + } + + private static Map animalBaseSums(Properties props) { + + Map baseSums = new HashMap<>(); + + for (String key : props.stringPropertyNames()) { + if (key.startsWith("baseSum.")) { + String animal = key.substring("baseSum.".length()).toUpperCase(); + BigDecimal baseSum = new BigDecimal(props.getProperty(key)); + baseSums.put(animal, baseSum); + } + } + return baseSums; + } + + private static Map tariffs(Properties props) { + + return props.stringPropertyNames().stream().filter(key -> key.startsWith("tariffs.")) + .map(key -> { + String name = key.substring("tariffs.".length()).toUpperCase(); + String[] values = props.getProperty(key).split(","); + BigDecimal factor = new BigDecimal(values[0]); + LocalDate availableFrom = LocalDate.parse(values[1]); + return new TariffOption(name, factor, availableFrom); + }).collect(Collectors.toMap(TariffOption::name, tariff -> tariff)); + } + + private static NavigableMap ageFactors(Properties props) { + + NavigableMap ageFactors = new TreeMap<>(); + + for (String key : props.stringPropertyNames()) { + if (key.startsWith("age.")) { + Integer age = Integer.parseInt(key.replace("age.", "")); + BigDecimal factor = new BigDecimal(props.getProperty(key)); + ageFactors.put(age, factor); + } + } + + String rawElderly = props.getProperty("elderlyFactor"); + if (rawElderly == null) { + throw new IllegalStateException("Pflichtkonfiguration 'elderlyFactor' fehlt."); + } + ageFactors.put(Integer.MAX_VALUE, new BigDecimal(rawElderly)); + return ageFactors; + } + + private static Map breedFactors(Properties props) { + + Map breedFactors = new HashMap<>(); + + for (String key : props.stringPropertyNames()) { + if (key.startsWith("breed.")) { + String name = key.substring("breed.".length()).toUpperCase(); + BigDecimal factor = new BigDecimal(props.getProperty(key)); + breedFactors.put(name, factor); + } + } + return breedFactors; + } + + private TariffLoader() {} +} diff --git a/src/main/java/com/pfefferminzia/petinsurance/tariffs/TariffOption.java b/src/main/java/com/pfefferminzia/petinsurance/tariffs/TariffOption.java new file mode 100644 index 0000000..7a09eb0 --- /dev/null +++ b/src/main/java/com/pfefferminzia/petinsurance/tariffs/TariffOption.java @@ -0,0 +1,11 @@ +package com.pfefferminzia.petinsurance.tariffs; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public record TariffOption(String name, BigDecimal factor, LocalDate availableFrom) { + + public boolean isAvailableAt(LocalDate date) { + return !date.isBefore(availableFrom); + } +} diff --git a/src/main/resources/tariff.properties b/src/main/resources/tariff.properties new file mode 100644 index 0000000..e9f7990 --- /dev/null +++ b/src/main/resources/tariff.properties @@ -0,0 +1,20 @@ + +# Animal base insurance sums +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.DACKEL=-0.1 +breed.BERNHARDINER=0.1 \ No newline at end of file