diff --git a/slides/slides.md b/slides/slides.md index ee6efed..56d5c6f 100644 --- a/slides/slides.md +++ b/slides/slides.md @@ -154,9 +154,11 @@ BigDecimal amount = new BigDecimal("10.123") // gĂĽltig oder nicht?
-- CHF: 2 Nachkommastellen -- JPY: keine Nachkommastellen -- TND: 3 Nachkommastellen +Create a currency conversion to EUR using a timestamp-based conversion query.
- * - *Include the given {@link LocalDateTime} in the query.
- * - * @param timestamp timestamp used in the conversion query - * @return currency conversion to EUR for the given timestamp - */ - public CurrencyConversion getEurConversionForTimestamp(LocalDateTime timestamp) { - throw new UnsupportedOperationException("TODO"); - } - - /** - *Convert a CHF amount to EUR using a timestamp-based conversion query.
- * - *Build the conversion query with the given timestamp and return the converted amount.
- * - * @param amount amount in CHF - * @param timestamp timestamp used in the conversion query - * @return converted amount in EUR - */ - public MonetaryAmount convertChfToEurAt(MonetaryAmount amount, LocalDateTime timestamp) { - throw new UnsupportedOperationException("TODO"); - } } diff --git a/src/main/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercises.java b/src/main/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercises.java index 99f7615..03d3b2f 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercises.java @@ -24,18 +24,6 @@ public class RoundingStrategyExercises { throw new UnsupportedOperationException("TODO"); } - /** - *Apply explicit default rounding to a VAT amount.
- * - *Use rounding as a separate, explicit step.
- * - * @param taxAmount tax amount to round - * @return rounded tax amount - */ - public MonetaryAmount roundVatAmount(MonetaryAmount taxAmount) { - throw new UnsupportedOperationException("TODO"); - } - /** *Apply Swiss cash rounding to the nearest 0.05 for CHF amounts.
* diff --git a/src/main/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProvider.java b/src/main/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProvider.java new file mode 100644 index 0000000..005f3e4 --- /dev/null +++ b/src/main/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProvider.java @@ -0,0 +1,65 @@ +package swiss.fihlon.workshop.money.part4; + +import javax.money.convert.ConversionQuery; +import javax.money.convert.CurrencyConversion; +import javax.money.convert.ExchangeRate; +import javax.money.convert.ExchangeRateProvider; +import javax.money.convert.ProviderContext; + +/** + *Bonus exercise 1 for implementing a local CSV-based exchange rate provider with JSR-354.
+ * + *The provider is intended for workshop usage and should read rates from the classpath resource + * {@code exchange-rates.csv}.
+ * + *The implementation is intentionally incomplete and should be finished by making tests pass.
+ */ +public class CsvExchangeRateProvider implements ExchangeRateProvider { + + /** + *Name of the classpath CSV resource containing exchange rates.
+ */ + public static final String RESOURCE_NAME = "exchange-rates.csv"; + + /** + *Create a new provider instance.
+ * + *The provider should load exchange rates from {@link #RESOURCE_NAME}.
+ */ + public CsvExchangeRateProvider() { + } + + /** + *Return the provider context for this exchange rate provider.
+ * + * @return provider context including a useful provider name + */ + @Override + public ProviderContext getContext() { + throw new UnsupportedOperationException("TODO"); + } + + /** + *Look up an exchange rate for the given conversion query.
+ * + * @param conversionQuery conversion query containing base and term currency + * @return matching exchange rate, or {@code null} if no matching rate exists + */ + @Override + public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { + throw new UnsupportedOperationException("TODO"); + } + + /** + *Create a currency conversion for the given conversion query.
+ * + *The returned conversion should allow direct amount conversion via {@code amount.with(conversion)}.
+ * + * @param conversionQuery conversion query containing conversion details + * @return currency conversion for the query + */ + @Override + public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) { + throw new UnsupportedOperationException("TODO"); + } +} diff --git a/src/main/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProvider.java b/src/main/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProvider.java new file mode 100644 index 0000000..320bbaa --- /dev/null +++ b/src/main/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProvider.java @@ -0,0 +1,83 @@ +package swiss.fihlon.workshop.money.part4; + +import java.time.LocalDate; +import javax.money.convert.ConversionQuery; +import javax.money.convert.CurrencyConversion; +import javax.money.convert.ExchangeRate; +import javax.money.convert.ExchangeRateProvider; +import javax.money.convert.ProviderContext; + +/** + *Bonus exercise 2 for implementing a local CSV-based historical exchange rate provider with JSR-354.
+ * + *The provider is intended for workshop usage and should read rates from the classpath resource + * {@code exchange-rates-hist.csv}.
+ * + *The lookup should use base currency, term currency, and a date passed in the query context.
+ * + *The implementation is intentionally incomplete and should be finished by making tests pass.
+ */ +public class CsvHistoricalExchangeRateProvider implements ExchangeRateProvider { + + /** + *Name of the classpath CSV resource containing historical exchange rates.
+ */ + public static final String RESOURCE_NAME = "exchange-rates-hist.csv"; + + /** + *Query key type for historical lookups.
+ */ + public static final ClassCreate a new provider instance.
+ * + *The provider should load exchange rates from {@link #RESOURCE_NAME}.
+ */ + public CsvHistoricalExchangeRateProvider() { + } + + /** + *Return the provider context for this exchange rate provider.
+ * + * @return provider context including a useful provider name + */ + @Override + public ProviderContext getContext() { + throw new UnsupportedOperationException("TODO"); + } + + /** + *Look up a historical exchange rate for the given conversion query.
+ * + *The query is expected to contain a {@link LocalDate} under {@link #DATE_QUERY_KEY}.
+ * + *If no exact match exists for the requested date, the provider should use the last available + * rate before that date.
+ * + *The method should return {@code null} only when no suitable earlier rate exists.
+ * + * @param conversionQuery conversion query containing base currency, term currency, and date + * @return matching historical exchange rate with fallback to the latest earlier date, or {@code null} + */ + @Override + public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { + throw new UnsupportedOperationException("TODO"); + } + + /** + *Create a currency conversion for the given conversion query.
+ * + *The conversion should use the same lookup rules as {@link #getExchangeRate(ConversionQuery)}: + * exact date first, otherwise the last available earlier rate.
+ * + *The returned conversion should allow direct amount conversion via {@code amount.with(conversion)}.
+ * + * @param conversionQuery conversion query containing conversion details + * @return currency conversion for the query + */ + @Override + public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) { + throw new UnsupportedOperationException("TODO"); + } +} diff --git a/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsCurrencyFactory.java b/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsCurrencyFactory.java new file mode 100644 index 0000000..f6c60db --- /dev/null +++ b/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsCurrencyFactory.java @@ -0,0 +1,25 @@ +package swiss.fihlon.workshop.money.part4; + +import javax.money.CurrencyUnit; + +/** + *Bonus exercise 3 helper for creating the custom DukePoints currency.
+ * + *The goal is to model {@code DKP} as a custom {@link CurrencyUnit} that can be used with monetary amounts.
+ */ +public class DukePointsCurrencyFactory { + + /** + *Currency code for DukePoints.
+ */ + public static final String DKP = "DKP"; + + /** + *Create the custom DukePoints currency unit.
+ * + * @return custom currency unit with currency code {@code DKP} + */ + public CurrencyUnit createDkpCurrency() { + return new DukePointsCurrencyUnit(); + } +} diff --git a/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsCurrencyUnit.java b/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsCurrencyUnit.java new file mode 100644 index 0000000..aafbcb2 --- /dev/null +++ b/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsCurrencyUnit.java @@ -0,0 +1,65 @@ +package swiss.fihlon.workshop.money.part4; + +import javax.money.CurrencyContext; +import javax.money.CurrencyContextBuilder; +import javax.money.CurrencyUnit; + +/** + *This is a minimal custom CurrencyUnit implementation used for workshop purposes.
+ * + *The implementation is provided to focus on currency integration and conversion logic.
+ */ +public final class DukePointsCurrencyUnit implements CurrencyUnit { + + private static final String CURRENCY_CODE = DukePointsCurrencyFactory.DKP; + private static final int DEFAULT_FRACTION_DIGITS = 2; + private static final int NUMERIC_CODE = 999; + private static final CurrencyContext CONTEXT = + CurrencyContextBuilder.of(DukePointsCurrencyUnit.class.getSimpleName()).build(); + + @Override + public String getCurrencyCode() { + return CURRENCY_CODE; + } + + @Override + public int getNumericCode() { + return NUMERIC_CODE; + } + + @Override + public int getDefaultFractionDigits() { + return DEFAULT_FRACTION_DIGITS; + } + + @Override + public CurrencyContext getContext() { + return CONTEXT; + } + + @Override + public int compareTo(CurrencyUnit otherCurrency) { + return getCurrencyCode().compareTo(otherCurrency.getCurrencyCode()); + } + + @Override + public String toString() { + return getCurrencyCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CurrencyUnit other)) { + return false; + } + return getCurrencyCode().equals(other.getCurrencyCode()); + } + + @Override + public int hashCode() { + return getCurrencyCode().hashCode(); + } +} diff --git a/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProvider.java b/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProvider.java new file mode 100644 index 0000000..0dadb80 --- /dev/null +++ b/src/main/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProvider.java @@ -0,0 +1,59 @@ +package swiss.fihlon.workshop.money.part4; + +import javax.money.convert.ConversionQuery; +import javax.money.convert.CurrencyConversion; +import javax.money.convert.ExchangeRate; +import javax.money.convert.ExchangeRateProvider; +import javax.money.convert.ProviderContext; + +/** + *Bonus exercise 3 for implementing local exchange rates between CHF and DukePoints.
+ * + *The provider should support deterministic conversion for {@code CHF -> DKP} and {@code DKP -> CHF}.
+ * + *The implementation is intentionally incomplete and should be finished by making tests pass.
+ */ +public class DukePointsExchangeRateProvider implements ExchangeRateProvider { + + /** + *Create a new exchange rate provider for DukePoints.
+ */ + public DukePointsExchangeRateProvider() { + } + + /** + *Return provider context metadata for this provider.
+ * + * @return provider context with a useful provider name + */ + @Override + public ProviderContext getContext() { + throw new UnsupportedOperationException("TODO"); + } + + /** + *Return the exchange rate for the given conversion query.
+ * + *Supported pairs are {@code CHF -> DKP} and {@code DKP -> CHF}.
+ * + * @param conversionQuery query containing base and term currency + * @return matching exchange rate, or {@code null} for unsupported currency pairs + */ + @Override + public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { + throw new UnsupportedOperationException("TODO"); + } + + /** + *Create a currency conversion for the given conversion query.
+ * + *The returned conversion should allow direct conversion using {@code amount.with(conversion)}.
+ * + * @param conversionQuery query containing base and term currency + * @return currency conversion based on the local DukePoints rules + */ + @Override + public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) { + throw new UnsupportedOperationException("TODO"); + } +} diff --git a/src/main/resources/exchange-rates-hist.csv b/src/main/resources/exchange-rates-hist.csv new file mode 100644 index 0000000..d5b718b --- /dev/null +++ b/src/main/resources/exchange-rates-hist.csv @@ -0,0 +1,19 @@ +date,base,term,rate +2026-04-15,CHF,EUR,0.92 +2026-04-15,CHF,USD,1.09 +2026-04-15,EUR,CHF,1.09 +2026-04-15,EUR,USD,1.18 +2026-04-15,USD,CHF,0.92 +2026-04-15,USD,EUR,0.85 +2026-04-16,CHF,EUR,0.93 +2026-04-16,CHF,USD,1.10 +2026-04-16,EUR,CHF,1.08 +2026-04-16,EUR,USD,1.19 +2026-04-16,USD,CHF,0.91 +2026-04-16,USD,EUR,0.84 +2026-04-17,CHF,EUR,0.94 +2026-04-17,CHF,USD,1.11 +2026-04-17,EUR,CHF,1.06 +2026-04-17,EUR,USD,1.20 +2026-04-17,USD,CHF,0.90 +2026-04-17,USD,EUR,0.83 diff --git a/src/main/resources/exchange-rates.csv b/src/main/resources/exchange-rates.csv new file mode 100644 index 0000000..f9807ea --- /dev/null +++ b/src/main/resources/exchange-rates.csv @@ -0,0 +1,9 @@ +base,term,rate +CHF,EUR,0.93 +CHF,USD,1.10 +CHF,GBP,0.82 +EUR,CHF,1.08 +USD,CHF,0.91 +GBP,CHF,1.22 +EUR,USD,1.18 +USD,EUR,0.85 diff --git a/src/test/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercisesTest.java b/src/test/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercisesTest.java index 1ad9a00..f563807 100644 --- a/src/test/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercisesTest.java +++ b/src/test/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercisesTest.java @@ -1,13 +1,13 @@ package swiss.fihlon.workshop.money.part2; -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.LocalDateTime; -import javax.money.MonetaryAmount; -import javax.money.convert.CurrencyConversion; import org.javamoney.moneta.Money; import org.junit.jupiter.api.Test; +import javax.money.MonetaryAmount; +import javax.money.convert.CurrencyConversion; + +import static org.assertj.core.api.Assertions.assertThat; + class CurrencyConversionExercisesTest { private final CurrencyConversionExercises exercises = new CurrencyConversionExercises(); @@ -29,25 +29,4 @@ class CurrencyConversionExercisesTest { assertThat(result).isNotNull(); assertThat(result.getCurrency().getCurrencyCode()).isEqualTo("EUR"); } - - @Test - void shouldCreateEurConversionForTimestamp() { - LocalDateTime timestamp = LocalDateTime.of(2024, 1, 15, 10, 30); - - CurrencyConversion conversion = exercises.getEurConversionForTimestamp(timestamp); - - assertThat(conversion).isNotNull(); - assertThat(conversion.getCurrency().getCurrencyCode()).isEqualTo("EUR"); - } - - @Test - void shouldConvertChfToEurAtTimestamp() { - MonetaryAmount chfAmount = Money.of(10, "CHF"); - LocalDateTime timestamp = LocalDateTime.of(2024, 1, 15, 10, 30); - - MonetaryAmount result = exercises.convertChfToEurAt(chfAmount, timestamp); - - assertThat(result).isNotNull(); - assertThat(result.getCurrency().getCurrencyCode()).isEqualTo("EUR"); - } } diff --git a/src/test/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercisesTest.java b/src/test/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercisesTest.java index 6904009..7e2c60e 100644 --- a/src/test/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercisesTest.java +++ b/src/test/java/swiss/fihlon/workshop/money/part2/RoundingStrategyExercisesTest.java @@ -1,11 +1,12 @@ package swiss.fihlon.workshop.money.part2; -import java.math.BigDecimal; -import javax.money.MonetaryAmount; -import javax.money.MonetaryOperator; import org.javamoney.moneta.Money; import org.junit.jupiter.api.Test; +import javax.money.MonetaryAmount; +import javax.money.MonetaryOperator; +import java.math.BigDecimal; + import static org.assertj.core.api.Assertions.assertThat; class RoundingStrategyExercisesTest { @@ -23,17 +24,6 @@ class RoundingStrategyExercisesTest { assertThat(result.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("10.02"); } - @Test - void shouldRoundVatAmount() { - MonetaryAmount taxAmount = Money.of(1.537, "CHF"); - - MonetaryAmount result = exercises.roundVatAmount(taxAmount); - - assertThat(result).isNotNull(); - assertThat(result.getCurrency().getCurrencyCode()).isEqualTo("CHF"); - assertThat(result.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("1.54"); - } - @Test void shouldApplySwissCashRounding() { MonetaryAmount firstAmount = Money.of(10.02, "CHF"); diff --git a/src/test/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProviderTest.java b/src/test/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProviderTest.java new file mode 100644 index 0000000..b972ea1 --- /dev/null +++ b/src/test/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProviderTest.java @@ -0,0 +1,93 @@ +package swiss.fihlon.workshop.money.part4; + +import java.math.BigDecimal; +import javax.money.Monetary; +import javax.money.MonetaryAmount; +import javax.money.convert.ConversionQuery; +import javax.money.convert.ConversionQueryBuilder; +import javax.money.convert.CurrencyConversion; +import javax.money.convert.ExchangeRate; +import org.javamoney.moneta.Money; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class CsvExchangeRateProviderTest { + + private final CsvExchangeRateProvider provider = new CsvExchangeRateProvider(); + + @Test + void shouldReturnProviderContextWithUsefulName() { + String providerName = provider.getContext().getProviderName(); + + assertThat(providerName).isNotBlank(); + assertThat(providerName).containsIgnoringCase("csv"); + } + + @Test + void shouldReturnExchangeRateForChfToEur() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("CHF")) + .setTermCurrency(Monetary.getCurrency("EUR")) + .build(); + + ExchangeRate exchangeRate = provider.getExchangeRate(query); + + assertThat(exchangeRate).isNotNull(); + assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.93"); + } + + @Test + void shouldReturnExchangeRateForUsdToEur() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("USD")) + .setTermCurrency(Monetary.getCurrency("EUR")) + .build(); + + ExchangeRate exchangeRate = provider.getExchangeRate(query); + + assertThat(exchangeRate).isNotNull(); + assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.85"); + } + + @Test + void shouldReturnNullForUnknownCurrencyPair() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("EUR")) + .setTermCurrency(Monetary.getCurrency("GBP")) + .build(); + + ExchangeRate exchangeRate = provider.getExchangeRate(query); + + assertThat(exchangeRate).isNull(); + } + + @Test + void shouldConvertAmountUsingExchangeRate() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("CHF")) + .setTermCurrency(Monetary.getCurrency("EUR")) + .build(); + ExchangeRate exchangeRate = provider.getExchangeRate(query); + MonetaryAmount chfAmount = Money.of(100, "CHF"); + + MonetaryAmount convertedAmount = chfAmount.multiply(exchangeRate.getFactor().numberValueExact(BigDecimal.class)); + + assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("93.00"); + } + + @Test + void shouldCreateCurrencyConversion() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("CHF")) + .setTermCurrency(Monetary.getCurrency("EUR")) + .build(); + + CurrencyConversion conversion = provider.getCurrencyConversion(query); + MonetaryAmount chfAmount = Money.of(100, "CHF"); + MonetaryAmount convertedAmount = chfAmount.with(conversion); + + assertThat(convertedAmount.getCurrency().getCurrencyCode()).isEqualTo("EUR"); + assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("93.00"); + } +} diff --git a/src/test/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProviderTest.java b/src/test/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProviderTest.java new file mode 100644 index 0000000..09fe58a --- /dev/null +++ b/src/test/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProviderTest.java @@ -0,0 +1,106 @@ +package swiss.fihlon.workshop.money.part4; + +import java.math.BigDecimal; +import java.time.LocalDate; +import javax.money.Monetary; +import javax.money.MonetaryAmount; +import javax.money.convert.ConversionQuery; +import javax.money.convert.ConversionQueryBuilder; +import javax.money.convert.CurrencyConversion; +import javax.money.convert.ExchangeRate; +import org.javamoney.moneta.Money; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class CsvHistoricalExchangeRateProviderTest { + + private final CsvHistoricalExchangeRateProvider provider = new CsvHistoricalExchangeRateProvider(); + + @Test + void shouldReturnExchangeRateForChfToEurOn20260415() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("CHF")) + .setTermCurrency(Monetary.getCurrency("EUR")) + .set(LocalDate.class, LocalDate.of(2026, 4, 15)) + .build(); + + ExchangeRate exchangeRate = provider.getExchangeRate(query); + + assertThat(exchangeRate).isNotNull(); + assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.92"); + } + + @Test + void shouldReturnExchangeRateForChfToEurOn20260417() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("CHF")) + .setTermCurrency(Monetary.getCurrency("EUR")) + .set(LocalDate.class, LocalDate.of(2026, 4, 17)) + .build(); + + ExchangeRate exchangeRate = provider.getExchangeRate(query); + + assertThat(exchangeRate).isNotNull(); + assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.94"); + } + + @Test + void shouldReturnExchangeRateForUsdToChfOn20260416() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("USD")) + .setTermCurrency(Monetary.getCurrency("CHF")) + .set(LocalDate.class, LocalDate.of(2026, 4, 16)) + .build(); + + ExchangeRate exchangeRate = provider.getExchangeRate(query); + + assertThat(exchangeRate).isNotNull(); + assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.91"); + } + + @Test + void shouldReturnLastAvailableRateWhenDateIsMissing() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("CHF")) + .setTermCurrency(Monetary.getCurrency("EUR")) + .set(LocalDate.class, LocalDate.of(2026, 4, 18)) + .build(); + + ExchangeRate exchangeRate = provider.getExchangeRate(query); + + assertThat(exchangeRate).isNotNull(); + assertThat(exchangeRate.getFactor().numberValueExact(BigDecimal.class)).isEqualByComparingTo("0.94"); + } + + @Test + void shouldConvertAmountUsingHistoricalRate() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("CHF")) + .setTermCurrency(Monetary.getCurrency("USD")) + .set(LocalDate.class, LocalDate.of(2026, 4, 17)) + .build(); + ExchangeRate exchangeRate = provider.getExchangeRate(query); + MonetaryAmount chfAmount = Money.of(100, "CHF"); + + MonetaryAmount convertedAmount = chfAmount.multiply(exchangeRate.getFactor().numberValueExact(BigDecimal.class)); + + assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("111.00"); + } + + @Test + void shouldCreateCurrencyConversionForHistoricalRate() { + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("CHF")) + .setTermCurrency(Monetary.getCurrency("USD")) + .set(LocalDate.class, LocalDate.of(2026, 4, 17)) + .build(); + + CurrencyConversion conversion = provider.getCurrencyConversion(query); + MonetaryAmount chfAmount = Money.of(100, "CHF"); + MonetaryAmount convertedAmount = chfAmount.with(conversion); + + assertThat(convertedAmount.getCurrency().getCurrencyCode()).isEqualTo("USD"); + assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("111.00"); + } +} diff --git a/src/test/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProviderTest.java b/src/test/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProviderTest.java new file mode 100644 index 0000000..d8f73ab --- /dev/null +++ b/src/test/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProviderTest.java @@ -0,0 +1,87 @@ +package swiss.fihlon.workshop.money.part4; + +import java.math.BigDecimal; +import javax.money.CurrencyUnit; +import javax.money.Monetary; +import javax.money.MonetaryAmount; +import javax.money.convert.ConversionQuery; +import javax.money.convert.ConversionQueryBuilder; +import javax.money.convert.CurrencyConversion; +import javax.money.convert.ExchangeRate; +import org.javamoney.moneta.Money; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class DukePointsExchangeRateProviderTest { + + private final DukePointsCurrencyFactory currencyFactory = new DukePointsCurrencyFactory(); + private final DukePointsExchangeRateProvider exchangeRateProvider = new DukePointsExchangeRateProvider(); + + @Test + void shouldCreateDkpCurrency() { + CurrencyUnit dkp = currencyFactory.createDkpCurrency(); + + assertThat(dkp).isNotNull(); + assertThat(dkp.getCurrencyCode()).isEqualTo("DKP"); + } + + @Test + void shouldConvertChfToDkp() { + CurrencyUnit dkp = currencyFactory.createDkpCurrency(); + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("CHF")) + .setTermCurrency(dkp) + .build(); + ExchangeRate exchangeRate = exchangeRateProvider.getExchangeRate(query); + MonetaryAmount chfAmount = Money.of(100, "CHF"); + + MonetaryAmount convertedAmount = chfAmount.multiply(exchangeRate.getFactor().numberValueExact(BigDecimal.class)); + + assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("10.00"); + } + + @Test + void shouldConvertDkpToChf() { + CurrencyUnit dkp = currencyFactory.createDkpCurrency(); + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(dkp) + .setTermCurrency(Monetary.getCurrency("CHF")) + .build(); + ExchangeRate exchangeRate = exchangeRateProvider.getExchangeRate(query); + MonetaryAmount dkpAmount = Money.of(5, dkp); + + MonetaryAmount convertedAmount = dkpAmount.multiply(exchangeRate.getFactor().numberValueExact(BigDecimal.class)); + + assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("50.00"); + } + + @Test + void shouldReturnNullForUnsupportedCurrencyPair() { + CurrencyUnit dkp = currencyFactory.createDkpCurrency(); + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("EUR")) + .setTermCurrency(dkp) + .build(); + + ExchangeRate exchangeRate = exchangeRateProvider.getExchangeRate(query); + + assertThat(exchangeRate).isNull(); + } + + @Test + void shouldConvertUsingCurrencyConversion() { + CurrencyUnit dkp = currencyFactory.createDkpCurrency(); + ConversionQuery query = ConversionQueryBuilder.of() + .setBaseCurrency(Monetary.getCurrency("CHF")) + .setTermCurrency(dkp) + .build(); + CurrencyConversion conversion = exchangeRateProvider.getCurrencyConversion(query); + MonetaryAmount chfAmount = Money.of(100, "CHF"); + + MonetaryAmount convertedAmount = chfAmount.with(conversion); + + assertThat(convertedAmount.getCurrency().getCurrencyCode()).isEqualTo("DKP"); + assertThat(convertedAmount.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("10.00"); + } +}