From e7e86624147f6437102cc06b60af4b3d00f90781 Mon Sep 17 00:00:00 2001 From: Marcus Fihlon Date: Mon, 20 Apr 2026 00:15:42 +0200 Subject: [PATCH 1/5] refactor(workshop): remove redundant VAT rounding method roundVatAmount duplicated default rounding logic and did not demonstrate a distinct rounding use case. --- .../money/part2/RoundingStrategyExercises.java | 12 ------------ .../part2/RoundingStrategyExercisesTest.java | 18 ++++-------------- 2 files changed, 4 insertions(+), 26 deletions(-) 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/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"); From e8162f3f178424c597e407429e387cdec0d31dc6 Mon Sep 17 00:00:00 2001 From: Marcus Fihlon Date: Mon, 20 Apr 2026 00:18:14 +0200 Subject: [PATCH 2/5] refactor(workshop): drop historical currency conversion exercises Remove tasks depending on ECB-HIST as historical rate queries are inconsistent and environment-dependent. --- .../part2/CurrencyConversionExercises.java | 26 ---------------- .../CurrencyConversionExercisesTest.java | 31 +++---------------- 2 files changed, 5 insertions(+), 52 deletions(-) diff --git a/src/main/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercises.java b/src/main/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercises.java index 7fd4b9e..945040b 100644 --- a/src/main/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercises.java +++ b/src/main/java/swiss/fihlon/workshop/money/part2/CurrencyConversionExercises.java @@ -1,6 +1,5 @@ package swiss.fihlon.workshop.money.part2; -import java.time.LocalDateTime; import javax.money.MonetaryAmount; import javax.money.convert.CurrencyConversion; @@ -35,29 +34,4 @@ public class CurrencyConversionExercises { public MonetaryAmount convertChfToEur(MonetaryAmount amount) { throw new UnsupportedOperationException("TODO"); } - - /** - *

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/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"); - } } From 50b6605285ba43cc683a2f15514d6027f9ab6d06 Mon Sep 17 00:00:00 2001 From: Marcus Fihlon Date: Mon, 20 Apr 2026 00:19:32 +0200 Subject: [PATCH 3/5] fix(slides): correct date type in historical exchange rate example Use `LocalDate` instead of `LocalDateTime` to align with Moneta/ECB-HIST expectations for date-based queries. --- slides/slides.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slides/slides.md b/slides/slides.md index ee6efed..e92e2cd 100644 --- a/slides/slides.md +++ b/slides/slides.md @@ -676,7 +676,7 @@ MonetaryAmount amountCHF = Money.of(10, "CHF"); ConversionQuery query = ConversionQueryBuilder.of() .setTermCurrency("EUR") - .set(LocalDateTime.class, LocalDateTime.of(2026, 3, 13, 14, 3, 27)) + .set(LocalDate.class, LocalDate.of(2026, 3, 13)) .build(); CurrencyConversion conversion = MonetaryConversions.getConversion(query); From 59d23421041cec017ea325d5b47d1307f9ad6359 Mon Sep 17 00:00:00 2001 From: Marcus Fihlon Date: Mon, 20 Apr 2026 00:20:21 +0200 Subject: [PATCH 4/5] docs(slides): add Otavio Santana as JSR-354 spec lead --- slides/slides.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/slides/slides.md b/slides/slides.md index e92e2cd..bbdaf3b 100644 --- a/slides/slides.md +++ b/slides/slides.md @@ -234,8 +234,7 @@ Was ist JSR-354? - Spezifikation: Java Community Process (JCP) - Referenzimplementierung: Moneta - Entwickelt und gepflegt von der Java-Community -- Spec Lead JSR-354: Werner Keil -- Lead der Referenzimplementierung (Moneta): Anatole Tresch +- Spec Leads: Anatole Tresch, Werner Keil und Otavio Santana 👉 Open Source und gemeinschaftlich weiterentwickelt From 8e61f2d2e2993e77789d9a4eb4a0bae15c176222 Mon Sep 17 00:00:00 2001 From: Marcus Fihlon Date: Mon, 20 Apr 2026 10:01:47 +0200 Subject: [PATCH 5/5] feat(workshop): add bonus exercises for custom providers and currencies Introduce optional bonus tasks in part4 covering: - CSV-based exchange rate provider - historical exchange rate provider with date handling - custom currency (DKP) with bidirectional conversion Provide exercise scaffolding and tests for all scenarios. Signed-off-by: Marcus Fihlon --- slides/slides.md | 70 +++++++++++- .../money/part4/CsvExchangeRateProvider.java | 65 +++++++++++ .../CsvHistoricalExchangeRateProvider.java | 83 ++++++++++++++ .../part4/DukePointsCurrencyFactory.java | 25 +++++ .../money/part4/DukePointsCurrencyUnit.java | 65 +++++++++++ .../part4/DukePointsExchangeRateProvider.java | 59 ++++++++++ src/main/resources/exchange-rates-hist.csv | 19 ++++ src/main/resources/exchange-rates.csv | 9 ++ .../part4/CsvExchangeRateProviderTest.java | 93 +++++++++++++++ ...CsvHistoricalExchangeRateProviderTest.java | 106 ++++++++++++++++++ .../DukePointsExchangeRateProviderTest.java | 87 ++++++++++++++ 11 files changed, 677 insertions(+), 4 deletions(-) create mode 100644 src/main/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProvider.java create mode 100644 src/main/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProvider.java create mode 100644 src/main/java/swiss/fihlon/workshop/money/part4/DukePointsCurrencyFactory.java create mode 100644 src/main/java/swiss/fihlon/workshop/money/part4/DukePointsCurrencyUnit.java create mode 100644 src/main/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProvider.java create mode 100644 src/main/resources/exchange-rates-hist.csv create mode 100644 src/main/resources/exchange-rates.csv create mode 100644 src/test/java/swiss/fihlon/workshop/money/part4/CsvExchangeRateProviderTest.java create mode 100644 src/test/java/swiss/fihlon/workshop/money/part4/CsvHistoricalExchangeRateProviderTest.java create mode 100644 src/test/java/swiss/fihlon/workshop/money/part4/DukePointsExchangeRateProviderTest.java diff --git a/slides/slides.md b/slides/slides.md index bbdaf3b..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 +
    +
  • CHF: 2 Nachkommastellen
  • +
  • JPY: keine Nachkommastellen
  • +
  • TND: 3 Nachkommastellen
  • +
--- @@ -991,6 +993,66 @@ Warenkorb / Bestellung --- +# Bonusaufgabe 1 + +Eigener Exchange Rate Provider + +Package: `swiss.fihlon.workshop.money.part4` + +- Implementiere einen eigenen `ExchangeRateProvider` +- Lade Wechselkurse aus `exchange-rates.csv` +- CSV enthält: + - Quellwährung + - Zielwährung + - Wechselkurs +- Stelle Umrechnungen zwischen den Währungen bereit +- Noch **ohne** Historisierung + +Relevante Interfaces: `ExchangeRateProvider` + +👉 Ziel: eigener Provider mit lokal geladenen Kursen + +--- + +# Bonus 2 + +Historische Kurse + +Package: `swiss.fihlon.workshop.money.part4` + +- Erweitere deinen Provider aus Bonus 1 +- Verwende `exchange-rates-hist.csv` +- CSV enthält zusätzlich: + - Datum pro Wechselkurs +- Wähle den passenden Kurs für ein angefragtes Datum +- Fallback: sinnvoller Standard (z. B. letzter verfügbarer Kurs) + +Relevante Interfaces: `ExchangeRateProvider` + +👉 Ziel: zeitabhängige Wechselkurse unterstützen + +--- + +# Bonus 3 + +Eigene Währung + +Package: `swiss.fihlon.workshop.money.part4` + +- Implementiere die eigene Währung `DKP` +- Vorgegebener Umrechnungsfaktor: + - 10 CHF = 1 DKP +- Baue einen eigenen Converter +- Unterstütze: + - CHF → DKP + - DKP → CHF + +Relevante Interfaces: `CurrencyUnit` und `ExchangeRateProvider` + +👉 Ziel: eigene Währung ins Modell integrieren + +--- + # Voraussetzungen Java-Versionen @@ -1057,4 +1119,4 @@ layout: center layout: center --- -# **Danke!** +# Danke! 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 Class DATE_QUERY_KEY = LocalDate.class; + + /** + *

Create 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/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"); + } +}