Compare commits

...
Sign in to create a new pull request.

5 commits

Author SHA1 Message Date
8e61f2d2e2
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 <marcus@fihlon.swiss>
2026-04-20 10:01:47 +02:00
59d2342104
docs(slides): add Otavio Santana as JSR-354 spec lead 2026-04-20 00:20:21 +02:00
50b6605285
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.
2026-04-20 00:19:32 +02:00
e8162f3f17
refactor(workshop): drop historical currency conversion exercises
Remove tasks depending on ECB-HIST as historical rate queries
are inconsistent and environment-dependent.
2026-04-20 00:18:14 +02:00
e7e8662414
refactor(workshop): remove redundant VAT rounding method
roundVatAmount duplicated default rounding logic and did not
demonstrate a distinct rounding use case.
2026-04-20 00:15:42 +02:00
15 changed files with 688 additions and 85 deletions

View file

@ -154,9 +154,11 @@ BigDecimal amount = new BigDecimal("10.123") // gültig oder nicht?
<div class="my-6"></div> <div class="my-6"></div>
- CHF: 2 Nachkommastellen <ul v-click>
- JPY: keine Nachkommastellen <li>CHF: 2 Nachkommastellen</li>
- TND: 3 Nachkommastellen <li>JPY: keine Nachkommastellen</li>
<li>TND: 3 Nachkommastellen</li>
</ul>
--- ---
@ -234,8 +236,7 @@ Was ist JSR-354?
- Spezifikation: Java Community Process (JCP) - Spezifikation: Java Community Process (JCP)
- Referenzimplementierung: Moneta - Referenzimplementierung: Moneta
- Entwickelt und gepflegt von der Java-Community - Entwickelt und gepflegt von der Java-Community
- Spec Lead JSR-354: Werner Keil - Spec Leads: Anatole Tresch, Werner Keil und Otavio Santana
- Lead der Referenzimplementierung (Moneta): Anatole Tresch
👉 Open Source und gemeinschaftlich weiterentwickelt 👉 Open Source und gemeinschaftlich weiterentwickelt
@ -676,7 +677,7 @@ MonetaryAmount amountCHF = Money.of(10, "CHF");
ConversionQuery query = ConversionQueryBuilder.of() ConversionQuery query = ConversionQueryBuilder.of()
.setTermCurrency("EUR") .setTermCurrency("EUR")
.set(LocalDateTime.class, LocalDateTime.of(2026, 3, 13, 14, 3, 27)) .set(LocalDate.class, LocalDate.of(2026, 3, 13))
.build(); .build();
CurrencyConversion conversion = MonetaryConversions.getConversion(query); CurrencyConversion conversion = MonetaryConversions.getConversion(query);
@ -992,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 # Voraussetzungen
Java-Versionen Java-Versionen
@ -1058,4 +1119,4 @@ layout: center
layout: center layout: center
--- ---
# **Danke!** # Danke!

View file

@ -1,6 +1,5 @@
package swiss.fihlon.workshop.money.part2; package swiss.fihlon.workshop.money.part2;
import java.time.LocalDateTime;
import javax.money.MonetaryAmount; import javax.money.MonetaryAmount;
import javax.money.convert.CurrencyConversion; import javax.money.convert.CurrencyConversion;
@ -35,29 +34,4 @@ public class CurrencyConversionExercises {
public MonetaryAmount convertChfToEur(MonetaryAmount amount) { public MonetaryAmount convertChfToEur(MonetaryAmount amount) {
throw new UnsupportedOperationException("TODO"); throw new UnsupportedOperationException("TODO");
} }
/**
* <p>Create a currency conversion to EUR using a timestamp-based conversion query.</p>
*
* <p>Include the given {@link LocalDateTime} in the query.</p>
*
* @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");
}
/**
* <p>Convert a CHF amount to EUR using a timestamp-based conversion query.</p>
*
* <p>Build the conversion query with the given timestamp and return the converted amount.</p>
*
* @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");
}
} }

View file

@ -24,18 +24,6 @@ public class RoundingStrategyExercises {
throw new UnsupportedOperationException("TODO"); throw new UnsupportedOperationException("TODO");
} }
/**
* <p>Apply explicit default rounding to a VAT amount.</p>
*
* <p>Use rounding as a separate, explicit step.</p>
*
* @param taxAmount tax amount to round
* @return rounded tax amount
*/
public MonetaryAmount roundVatAmount(MonetaryAmount taxAmount) {
throw new UnsupportedOperationException("TODO");
}
/** /**
* <p>Apply Swiss cash rounding to the nearest 0.05 for CHF amounts.</p> * <p>Apply Swiss cash rounding to the nearest 0.05 for CHF amounts.</p>
* *

View file

@ -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;
/**
* <p>Bonus exercise 1 for implementing a local CSV-based exchange rate provider with JSR-354.</p>
*
* <p>The provider is intended for workshop usage and should read rates from the classpath resource
* {@code exchange-rates.csv}.</p>
*
* <p>The implementation is intentionally incomplete and should be finished by making tests pass.</p>
*/
public class CsvExchangeRateProvider implements ExchangeRateProvider {
/**
* <p>Name of the classpath CSV resource containing exchange rates.</p>
*/
public static final String RESOURCE_NAME = "exchange-rates.csv";
/**
* <p>Create a new provider instance.</p>
*
* <p>The provider should load exchange rates from {@link #RESOURCE_NAME}.</p>
*/
public CsvExchangeRateProvider() {
}
/**
* <p>Return the provider context for this exchange rate provider.</p>
*
* @return provider context including a useful provider name
*/
@Override
public ProviderContext getContext() {
throw new UnsupportedOperationException("TODO");
}
/**
* <p>Look up an exchange rate for the given conversion query.</p>
*
* @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");
}
/**
* <p>Create a currency conversion for the given conversion query.</p>
*
* <p>The returned conversion should allow direct amount conversion via {@code amount.with(conversion)}.</p>
*
* @param conversionQuery conversion query containing conversion details
* @return currency conversion for the query
*/
@Override
public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) {
throw new UnsupportedOperationException("TODO");
}
}

View file

@ -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;
/**
* <p>Bonus exercise 2 for implementing a local CSV-based historical exchange rate provider with JSR-354.</p>
*
* <p>The provider is intended for workshop usage and should read rates from the classpath resource
* {@code exchange-rates-hist.csv}.</p>
*
* <p>The lookup should use base currency, term currency, and a date passed in the query context.</p>
*
* <p>The implementation is intentionally incomplete and should be finished by making tests pass.</p>
*/
public class CsvHistoricalExchangeRateProvider implements ExchangeRateProvider {
/**
* <p>Name of the classpath CSV resource containing historical exchange rates.</p>
*/
public static final String RESOURCE_NAME = "exchange-rates-hist.csv";
/**
* <p>Query key type for historical lookups.</p>
*/
public static final Class<LocalDate> DATE_QUERY_KEY = LocalDate.class;
/**
* <p>Create a new provider instance.</p>
*
* <p>The provider should load exchange rates from {@link #RESOURCE_NAME}.</p>
*/
public CsvHistoricalExchangeRateProvider() {
}
/**
* <p>Return the provider context for this exchange rate provider.</p>
*
* @return provider context including a useful provider name
*/
@Override
public ProviderContext getContext() {
throw new UnsupportedOperationException("TODO");
}
/**
* <p>Look up a historical exchange rate for the given conversion query.</p>
*
* <p>The query is expected to contain a {@link LocalDate} under {@link #DATE_QUERY_KEY}.</p>
*
* <p>If no exact match exists for the requested date, the provider should use the last available
* rate before that date.</p>
*
* <p>The method should return {@code null} only when no suitable earlier rate exists.</p>
*
* @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");
}
/**
* <p>Create a currency conversion for the given conversion query.</p>
*
* <p>The conversion should use the same lookup rules as {@link #getExchangeRate(ConversionQuery)}:
* exact date first, otherwise the last available earlier rate.</p>
*
* <p>The returned conversion should allow direct amount conversion via {@code amount.with(conversion)}.</p>
*
* @param conversionQuery conversion query containing conversion details
* @return currency conversion for the query
*/
@Override
public CurrencyConversion getCurrencyConversion(ConversionQuery conversionQuery) {
throw new UnsupportedOperationException("TODO");
}
}

View file

@ -0,0 +1,25 @@
package swiss.fihlon.workshop.money.part4;
import javax.money.CurrencyUnit;
/**
* <p>Bonus exercise 3 helper for creating the custom DukePoints currency.</p>
*
* <p>The goal is to model {@code DKP} as a custom {@link CurrencyUnit} that can be used with monetary amounts.</p>
*/
public class DukePointsCurrencyFactory {
/**
* <p>Currency code for DukePoints.</p>
*/
public static final String DKP = "DKP";
/**
* <p>Create the custom DukePoints currency unit.</p>
*
* @return custom currency unit with currency code {@code DKP}
*/
public CurrencyUnit createDkpCurrency() {
return new DukePointsCurrencyUnit();
}
}

View file

@ -0,0 +1,65 @@
package swiss.fihlon.workshop.money.part4;
import javax.money.CurrencyContext;
import javax.money.CurrencyContextBuilder;
import javax.money.CurrencyUnit;
/**
* <p>This is a minimal custom CurrencyUnit implementation used for workshop purposes.</p>
*
* <p>The implementation is provided to focus on currency integration and conversion logic.</p>
*/
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();
}
}

View file

@ -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;
/**
* <p>Bonus exercise 3 for implementing local exchange rates between CHF and DukePoints.</p>
*
* <p>The provider should support deterministic conversion for {@code CHF -> DKP} and {@code DKP -> CHF}.</p>
*
* <p>The implementation is intentionally incomplete and should be finished by making tests pass.</p>
*/
public class DukePointsExchangeRateProvider implements ExchangeRateProvider {
/**
* <p>Create a new exchange rate provider for DukePoints.</p>
*/
public DukePointsExchangeRateProvider() {
}
/**
* <p>Return provider context metadata for this provider.</p>
*
* @return provider context with a useful provider name
*/
@Override
public ProviderContext getContext() {
throw new UnsupportedOperationException("TODO");
}
/**
* <p>Return the exchange rate for the given conversion query.</p>
*
* <p>Supported pairs are {@code CHF -> DKP} and {@code DKP -> CHF}.</p>
*
* @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");
}
/**
* <p>Create a currency conversion for the given conversion query.</p>
*
* <p>The returned conversion should allow direct conversion using {@code amount.with(conversion)}.</p>
*
* @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");
}
}

View file

@ -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
1 date base term rate
2 2026-04-15 CHF EUR 0.92
3 2026-04-15 CHF USD 1.09
4 2026-04-15 EUR CHF 1.09
5 2026-04-15 EUR USD 1.18
6 2026-04-15 USD CHF 0.92
7 2026-04-15 USD EUR 0.85
8 2026-04-16 CHF EUR 0.93
9 2026-04-16 CHF USD 1.10
10 2026-04-16 EUR CHF 1.08
11 2026-04-16 EUR USD 1.19
12 2026-04-16 USD CHF 0.91
13 2026-04-16 USD EUR 0.84
14 2026-04-17 CHF EUR 0.94
15 2026-04-17 CHF USD 1.11
16 2026-04-17 EUR CHF 1.06
17 2026-04-17 EUR USD 1.20
18 2026-04-17 USD CHF 0.90
19 2026-04-17 USD EUR 0.83

View file

@ -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
1 base term rate
2 CHF EUR 0.93
3 CHF USD 1.10
4 CHF GBP 0.82
5 EUR CHF 1.08
6 USD CHF 0.91
7 GBP CHF 1.22
8 EUR USD 1.18
9 USD EUR 0.85

View file

@ -1,13 +1,13 @@
package swiss.fihlon.workshop.money.part2; 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.javamoney.moneta.Money;
import org.junit.jupiter.api.Test; 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 { class CurrencyConversionExercisesTest {
private final CurrencyConversionExercises exercises = new CurrencyConversionExercises(); private final CurrencyConversionExercises exercises = new CurrencyConversionExercises();
@ -29,25 +29,4 @@ class CurrencyConversionExercisesTest {
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.getCurrency().getCurrencyCode()).isEqualTo("EUR"); 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");
}
} }

View file

@ -1,11 +1,12 @@
package swiss.fihlon.workshop.money.part2; 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.javamoney.moneta.Money;
import org.junit.jupiter.api.Test; 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; import static org.assertj.core.api.Assertions.assertThat;
class RoundingStrategyExercisesTest { class RoundingStrategyExercisesTest {
@ -23,17 +24,6 @@ class RoundingStrategyExercisesTest {
assertThat(result.getNumber().numberValueExact(BigDecimal.class)).isEqualByComparingTo("10.02"); 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 @Test
void shouldApplySwissCashRounding() { void shouldApplySwissCashRounding() {
MonetaryAmount firstAmount = Money.of(10.02, "CHF"); MonetaryAmount firstAmount = Money.of(10.02, "CHF");

View file

@ -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");
}
}

View file

@ -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");
}
}

View file

@ -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");
}
}