Skip to content

Commit

Permalink
[MODFIN-349] - Implement a new endpoint for currency exchange calcula…
Browse files Browse the repository at this point in the history
…tion (#225)

* [MODFIN-349] - Implement a new endpoint for currency exchange calculation

* [MODFIN-349] - Implement a new endpoint for currency exchange calculation

* [MODFIN-346] - Covered with Test cases

* [MODFIN-346] - Added to ApiTestSuite

* [MODFIN-346] - Added to ApiTestSuite

* [MODFIN-346] - Fixed unit test

* [MODFIN-346] - Fixed unit test

* [MODFIN-346] - Fixed Import style

* [MODFIN-346] - Fixed Import style
  • Loading branch information
azizbekxm authored Jan 26, 2024
1 parent 8df20cd commit 25260f2
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 5 deletions.
80 changes: 80 additions & 0 deletions ramls/calculate-exchange.raml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#%RAML 1.0

title: Calculate exchange
version: v1
protocols: [ HTTP, HTTPS ]
baseUri: https://github.com/folio-org/mod-finance

documentation:
- title: Calculate exchange API
content: This documents the API calls that can be made to exchange calculation

types:
errors: !include raml-util/schemas/error.schema
amount:
type: number
description: Amount expressed as a number of major currency units that needs to be calculate
example: 99.95
currency_code:
description: currency_code expressed as a code of currency
type: string
example: USD
exchange_calculation:
type: number
description: currency_code expressed as a total of exchange calculation
example: 99.95

/finance/calculate_exchange:
displayName: Calculate exchange
description: Calculate exchange API
get:
description: "Get exchange calculation"
queryParameters:
source_currency:
description: "Source currency code"
type: currency_code
required: true
example: USD
target_currency:
description: "Target currency code"
type: currency_code
required: true
example: EUR
amount:
description: "The amount of money to calculate exchange"
type: amount
required: true
example: 100.0
responses:
200:
description: "Exchange calculation successfully retrieved"
body:
application/json:
type: exchange_calculation
example:
strict: false
value: 200.0
400:
description: "Bad request, e.g. malformed request body or query parameter. Details of the error (e.g. name of the parameter or line/character number with malformed data) provided in the response."
body:
application/json:
type: errors
example:
strict: false
value: !include raml-util/examples/error.sample
404:
description: "Exchange rate is not available"
body:
application/json:
type: errors
example:
strict: false
value: !include raml-util/examples/error.sample
500:
description: "Internal server error, e.g. due to misconfiguration"
body:
application/json:
type: errors
example:
strict: false
value: !include raml-util/examples/error.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
import static org.javamoney.moneta.convert.ExchangeRateType.ECB;
import static org.javamoney.moneta.convert.ExchangeRateType.IDENTITY;

import javax.money.Monetary;
import javax.money.convert.CurrencyConversionException;
import javax.money.convert.MonetaryConversions;

import io.vertx.core.Context;
import io.vertx.core.Future;
import org.folio.rest.exception.HttpException;
import org.folio.rest.jaxrs.model.ExchangeRate;
import org.javamoney.moneta.Money;

import io.vertx.core.Context;
public class ExchangeHelper extends AbstractHelper {

public class ExchangeRateHelper extends AbstractHelper {
public ExchangeRateHelper(Context ctx) {
public ExchangeHelper(Context ctx) {
super(ctx);
}

Expand All @@ -32,4 +35,16 @@ public ExchangeRate getExchangeRate(String from, String to) {
throw new HttpException(400, e.getMessage());
}
}

public Future<Double> calculateExchange(String sourceCurrency, String targetCurrency, Number amount) {
return Future.succeededFuture()
.map(v -> {
var initialAmount = Money.of(amount, sourceCurrency);
var rate = getExchangeRate(sourceCurrency, targetCurrency).getExchangeRate();

return initialAmount.multiply(rate)
.with(Monetary.getDefaultRounding())
.getNumber().doubleValue();
});
}
}
27 changes: 27 additions & 0 deletions src/main/java/org/folio/rest/impl/CalculateExchangeApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.folio.rest.impl;

import static io.vertx.core.Future.succeededFuture;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.folio.rest.util.HelperUtils.handleErrorResponse;

import javax.ws.rs.core.Response;
import java.util.Map;

import io.vertx.core.AsyncResult;
import io.vertx.core.Context;
import io.vertx.core.Handler;
import org.folio.rest.helper.ExchangeHelper;
import org.folio.rest.jaxrs.resource.FinanceCalculateExchange;

public class CalculateExchangeApi implements FinanceCalculateExchange {

@Override
public void getFinanceCalculateExchange(String sourceCurrency, String targetCurrency, Number amount,
Map<String, String> okapiHeaders, Handler<AsyncResult<Response>> asyncResultHandler,
Context vertxContext) {
ExchangeHelper helper = new ExchangeHelper(vertxContext);
helper.calculateExchange(sourceCurrency, targetCurrency, amount)
.onSuccess(body -> asyncResultHandler.handle(succeededFuture(Response.ok(body, APPLICATION_JSON).build())))
.onFailure(e -> handleErrorResponse(asyncResultHandler, helper, e));
}
}
4 changes: 2 additions & 2 deletions src/main/java/org/folio/rest/impl/ExchangeRateApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import javax.ws.rs.core.Response;

import org.folio.rest.annotations.Validate;
import org.folio.rest.helper.ExchangeRateHelper;
import org.folio.rest.helper.ExchangeHelper;
import org.folio.rest.jaxrs.resource.FinanceExchangeRate;

import io.vertx.core.AsyncResult;
Expand All @@ -21,7 +21,7 @@ public class ExchangeRateApi implements FinanceExchangeRate {
@Validate
public void getFinanceExchangeRate(String from, String to, Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
ExchangeRateHelper helper = new ExchangeRateHelper(vertxContext);
ExchangeHelper helper = new ExchangeHelper(vertxContext);
vertxContext.executeBlocking(promise -> promise.complete(helper.getExchangeRate(from, to)))
.onSuccess(body -> asyncResultHandler.handle(succeededFuture(Response.ok(body, APPLICATION_JSON).build())))
.onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t));
Expand Down
5 changes: 5 additions & 0 deletions src/test/java/org/folio/ApiTestSuite.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.concurrent.TimeoutException;

import org.folio.rest.impl.BudgetsApiTest;
import org.folio.rest.impl.CalculateExchangeApiTest;
import org.folio.rest.impl.EncumbrancesTest;
import org.folio.rest.impl.EntitiesCrudBasicsTest;
import org.folio.rest.impl.ExchangeRateTest;
Expand Down Expand Up @@ -116,6 +117,10 @@ class LedgerRolloversErrorsApiTestNested extends LedgerRolloverErrorsApiTest {
class LedgerRolloversProgressApiTestNested extends LedgerRolloverProgressApiTest {
}

@Nested
class CalculateExchangeApiTestNested extends CalculateExchangeApiTest {
}

@Nested
class ExchangeRateTestNested extends ExchangeRateTest {
}
Expand Down
105 changes: 105 additions & 0 deletions src/test/java/org/folio/rest/impl/CalculateExchangeApiTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.folio.rest.impl;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.folio.rest.util.TestConfig.isVerticleNotDeployed;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.folio.ApiTestSuite;
import org.folio.rest.util.RestTestUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class CalculateExchangeApiTest {
private static final Logger logger = LogManager.getLogger(ExchangeRateTest.class);

private static final double ONE_HUNDRED = 100.0;
private static final String CALCULATE_EXCHANGE_RATE_PATH = "finance/calculate_exchange";
private static final String VALID_REQUEST = "?source_currency=USD&target_currency=EUR&amount=100.0";
private static final String SAME_CURRENCIES = "?source_currency=USD&target_currency=USD&amount=100.0";
private static final String NON_EXISTENT_CURRENCY = "?source_currency=ABC&target_currency=EUR&amount=100.0";
private static final String MISSING_SOURCE_CURRENCY = "?target_currency=EUR&amount=100.0";
private static final String MISSING_TARGET_CURRENCY = "?source_currency=USD&amount=100.0";
private static final String MISSING_AMOUNT = "?source_currency=USD&target_currency=EUR";
private static final String INVALID_CURRENCY = "?source_currency=US&target_currency=USD&amount=100.0";
private static final String EXCHANGE_NOT_AVAILABLE = "?source_currency=USD&target_currency=ALL&amount=100.0";
private static boolean runningOnOwn;
@BeforeAll
static void beforeAll() throws InterruptedException, ExecutionException, TimeoutException {
if (isVerticleNotDeployed()) {
ApiTestSuite.before();
runningOnOwn = true;
}
}

@AfterAll
static void after() {
if (runningOnOwn) {
ApiTestSuite.after();
}
}

@Test
void calculateExchange() {
logger.info("=== Test get exchange rate: Success ===");
var exchangeCalculation = RestTestUtils.verifyGet(CALCULATE_EXCHANGE_RATE_PATH + VALID_REQUEST, APPLICATION_JSON, 200).as(Double.class);
assertNotNull(exchangeCalculation);
}

@Test
void calculateExchangeForSameCurrencies() {
logger.info("=== Test exchange calculation for same currency codes: Success, Amount=100.0 ===");
var exchangeCalculation = RestTestUtils.verifyGet(CALCULATE_EXCHANGE_RATE_PATH + SAME_CURRENCIES, APPLICATION_JSON, 200).as(Double.class);
assertThat(ONE_HUNDRED, equalTo(exchangeCalculation));
}

@Test
void calculateExchangeForNonexistentCurrency(){
logger.info("=== Test exchange calculation for non-existent currency code: BAD_REQUEST ===");
RestTestUtils.verifyGet(CALCULATE_EXCHANGE_RATE_PATH + NON_EXISTENT_CURRENCY, "", 500);
}

@Test
void calculateExchangeMissingParameters() {
logger.info("=== Test exchange calculation missing query parameters: BAD_REQUEST ===");
RestTestUtils.verifyGet(CALCULATE_EXCHANGE_RATE_PATH, "", 500);
}

@Test
void calculateExchangeMissingSourceCurrencyParameter() {
logger.info("=== Test exchange calculation missing SOURCE_CURRENCY parameter: BAD_REQUEST ===");
RestTestUtils.verifyGet(CALCULATE_EXCHANGE_RATE_PATH + MISSING_SOURCE_CURRENCY, "", 500);
}

@Test
void calculateExchangeMissingTargetCurrencyParameter() {
logger.info("=== Test exchange calculation missing TARGET_CURRENCY parameter: BAD_REQUEST ===");
RestTestUtils.verifyGet(CALCULATE_EXCHANGE_RATE_PATH + MISSING_TARGET_CURRENCY, "", 400);
}

@Test
void calculateExchangeMissingAmountParameter() {
logger.info("=== Test exchange calculation missing AMOUNT parameter: BAD_REQUEST ===");
RestTestUtils.verifyGet(CALCULATE_EXCHANGE_RATE_PATH + MISSING_AMOUNT, "", 500);
}


@Test
void calculateExchangeInvalidCurrencyCode() {
logger.info("=== Test exchange calculation for invalid currency code: BAD_REQUEST ===");
RestTestUtils.verifyGet(CALCULATE_EXCHANGE_RATE_PATH + INVALID_CURRENCY, "", 500);
}

@Test
void getExchangeRateNoRate() {
logger.info("=== Test exchange calculation source currency USD target currency ALL : NOT_FOUND ===");
RestTestUtils.verifyGet(CALCULATE_EXCHANGE_RATE_PATH + EXCHANGE_NOT_AVAILABLE, "", 404);
}
}

0 comments on commit 25260f2

Please sign in to comment.