From 68117dcb2c85239332964795dfb076c4832a16ad Mon Sep 17 00:00:00 2001 From: Amanuel Date: Sun, 26 Jan 2025 20:11:53 +0100 Subject: [PATCH 1/3] Chapa client can now retry calls - Refactor to be able to better configure api client - Api ClientProvider is able to log calls with the flag debug=true - Api ClientProvider can accept custom OkHttpClient built by the user (provider.setClient(clientHere)) - Introduce well test retrofit2 as a client provider --- pom.xml | 26 +- src/main/java/com/yaphet/chapa/Chapa.java | 245 +++++----------- .../com/yaphet/chapa/client/ChapaClient.java | 102 ++++++- .../yaphet/chapa/client/ChapaClientApi.java | 33 +++ .../yaphet/chapa/client/ChapaClientImpl.java | 68 ----- .../com/yaphet/chapa/client/IChapaClient.java | 90 ++++++ .../provider/BaseRetrofitClientProvider.java | 66 +++++ .../DefaultRetrofitClientProvider.java | 67 +++++ .../RetrierRetrofitClientProvider.java | 97 ++++++ .../provider/RetrofitClientProvider.java | 13 + .../java/com/yaphet/chapa/model/Bank.java | 100 ++++++- .../chapa/model/InitializeResponse.java | 76 +++++ .../chapa/model/InitializeResponseData.java | 55 ---- .../java/com/yaphet/chapa/model/PostData.java | 44 ++- .../{ResponseData.java => Response.java} | 16 +- .../com/yaphet/chapa/model/ResponseBanks.java | 54 ++++ .../com/yaphet/chapa/model/SplitTypeEnum.java | 6 + .../com/yaphet/chapa/model/SubAccountDto.java | 89 ++++++ .../chapa/model/SubAccountResponse.java | 72 +++++ .../chapa/model/SubAccountResponseData.java | 55 ---- .../yaphet/chapa/model/VerifyResponse.java | 249 ++++++++++++++++ .../chapa/model/VerifyResponseData.java | 218 -------------- .../utility/LocalDateTimeDeserializer.java | 8 +- .../com/yaphet/chapa/utility/StringUtils.java | 40 +++ .../java/com/yaphet/chapa/utility/Util.java | 128 ++------ .../com/yaphet/chapa/utility/Validate.java | 27 ++ .../java/com/yaphet/chapa/ChapaExample.java | 68 +++++ src/test/java/com/yaphet/chapa/ChapaTest.java | 275 ------------------ src/test/java/com/yaphet/chapa/UtilTest.java | 21 -- 29 files changed, 1403 insertions(+), 1005 deletions(-) create mode 100644 src/main/java/com/yaphet/chapa/client/ChapaClientApi.java delete mode 100644 src/main/java/com/yaphet/chapa/client/ChapaClientImpl.java create mode 100644 src/main/java/com/yaphet/chapa/client/IChapaClient.java create mode 100644 src/main/java/com/yaphet/chapa/client/provider/BaseRetrofitClientProvider.java create mode 100644 src/main/java/com/yaphet/chapa/client/provider/DefaultRetrofitClientProvider.java create mode 100644 src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java create mode 100644 src/main/java/com/yaphet/chapa/client/provider/RetrofitClientProvider.java create mode 100644 src/main/java/com/yaphet/chapa/model/InitializeResponse.java delete mode 100644 src/main/java/com/yaphet/chapa/model/InitializeResponseData.java rename src/main/java/com/yaphet/chapa/model/{ResponseData.java => Response.java} (70%) create mode 100644 src/main/java/com/yaphet/chapa/model/ResponseBanks.java create mode 100644 src/main/java/com/yaphet/chapa/model/SplitTypeEnum.java create mode 100644 src/main/java/com/yaphet/chapa/model/SubAccountDto.java create mode 100644 src/main/java/com/yaphet/chapa/model/SubAccountResponse.java delete mode 100644 src/main/java/com/yaphet/chapa/model/SubAccountResponseData.java create mode 100644 src/main/java/com/yaphet/chapa/model/VerifyResponse.java delete mode 100644 src/main/java/com/yaphet/chapa/model/VerifyResponseData.java create mode 100644 src/main/java/com/yaphet/chapa/utility/StringUtils.java create mode 100644 src/main/java/com/yaphet/chapa/utility/Validate.java create mode 100644 src/test/java/com/yaphet/chapa/ChapaExample.java delete mode 100644 src/test/java/com/yaphet/chapa/ChapaTest.java delete mode 100644 src/test/java/com/yaphet/chapa/UtilTest.java diff --git a/pom.xml b/pom.xml index b189d37..30c382a 100644 --- a/pom.xml +++ b/pom.xml @@ -34,9 +34,14 @@ - com.mashape.unirest - unirest-java - 1.4.9 + com.squareup.retrofit2 + retrofit + 2.7.2 + + + com.squareup.retrofit2 + converter-gson + 2.9.0 com.google.code.gson @@ -67,6 +72,21 @@ 1.5.0 test + + com.squareup.okhttp3 + logging-interceptor + 3.12.0 + + + io.github.resilience4j + resilience4j-retry + 1.7.1 + + + io.github.resilience4j + resilience4j-retrofit + 1.7.1 + diff --git a/src/main/java/com/yaphet/chapa/Chapa.java b/src/main/java/com/yaphet/chapa/Chapa.java index eeb04ef..4bb6c18 100644 --- a/src/main/java/com/yaphet/chapa/Chapa.java +++ b/src/main/java/com/yaphet/chapa/Chapa.java @@ -1,14 +1,23 @@ package com.yaphet.chapa; -import com.yaphet.chapa.client.ChapaClient; -import com.yaphet.chapa.client.ChapaClientImpl; +import com.yaphet.chapa.client.IChapaClient; import com.yaphet.chapa.exception.ChapaException; -import com.yaphet.chapa.model.*; -import com.yaphet.chapa.utility.Util; +import com.yaphet.chapa.model.Bank; +import com.yaphet.chapa.model.Customization; +import com.yaphet.chapa.model.InitializeResponse; +import com.yaphet.chapa.model.PostData; +import com.yaphet.chapa.model.ResponseBanks; +import com.yaphet.chapa.model.SubAccountDto; +import com.yaphet.chapa.model.SubAccountResponse; +import com.yaphet.chapa.model.VerifyResponse; +import com.yaphet.chapa.utility.StringUtils; -import java.util.HashMap; -import java.util.List; import java.util.Map; +import java.util.Optional; + +import static com.yaphet.chapa.utility.StringUtils.EMPTY; +import static com.yaphet.chapa.utility.Util.isAnyNull; +import static com.yaphet.chapa.utility.Util.putIfNotNull; /** * The Chapa class is responsible for making GET and POST request to Chapa API to initialize @@ -16,94 +25,37 @@ */ public class Chapa { - private static String responseBody; - private static int statusCode; - private final ChapaClient chapaClient; - private final String VERSION = "v1"; - private final String BASE_URL = "https://api.chapa.co/" + VERSION; - private final String SECRETE_KEY; + private final String secreteKey; - /** - * @param secreteKey A secrete key provided from Chapa. - */ - public Chapa(String secreteKey) { // TODO: consider deprecating this since it makes it hard to test this class - this.SECRETE_KEY = secreteKey; - this.chapaClient = new ChapaClientImpl(); - } + private final IChapaClient chapaClient; - /** - * @param secreteKey A secrete key provided from Chapa. - * @param chapaClient Implementation of {@link ChapaClient} interface. - */ - public Chapa(ChapaClient chapaClient, String secreteKey) { - this.chapaClient = chapaClient; - this.SECRETE_KEY = secreteKey; + private Chapa(Builder builder) { + this.chapaClient = builder.client; + this.secreteKey = builder.secretKey; } - /** *

This method is used to initialize payment in Chapa. It is an overloaded method * of {@link #initialize(String)}.


* - * @param postData Object of {@link PostData} instantiated with - * post fields. - * @return An object of {@link InitializeResponseData} containing - * response data from Chapa API. - * @throws Throwable Throws an exception for failed request to Chapa API. + * @param postData Object of {@link PostData} instantiated with post fields. + * @return An object of {@link InitializeResponse} containing response data from Chapa API. + * @throws ChapaException Throws an exception for failed request to Chapa API. */ - public InitializeResponseData initialize(PostData postData) throws Throwable { // TODO: consider creating custom exception handler and wrap any exception thrown by http client - Map fields = new HashMap<>(); - fields.put("amount", postData.getAmount().toString()); - fields.put("currency", postData.getCurrency()); - fields.put("email", postData.getEmail()); - fields.put("first_name", postData.getFirstName()); - fields.put("last_name", postData.getLastName()); - fields.put("tx_ref", postData.getTxRef()); - - Customization customization = postData.getCustomization(); - String callbackUrl = postData.getCallbackUrl(); - String returnUrl = postData.getReturnUrl(); - String subAccountId = postData.getSubAccountId(); - - if (Util.notNullAndEmpty(subAccountId)) { - fields.put("subaccount[id]", subAccountId); - } - - if (Util.notNullAndEmpty(callbackUrl)) { - fields.put("callback_url", callbackUrl); - } - - if (Util.notNullAndEmpty(returnUrl)) { - fields.put("return_url", returnUrl); - } - - if (customization != null) { - // TODO: consider directly adding all values to fields map - if (Util.notNullAndEmpty(customization.getTitle())) { - fields.put("customization[title]", customization.getTitle()); - } - - if (Util.notNullAndEmpty(customization.getDescription())) { - fields.put("customization[description]", customization.getDescription()); - } - - if (Util.notNullAndEmpty(customization.getLogo())) { - fields.put("customization[logo]", customization.getLogo()); - } - } - - responseBody = chapaClient.post(BASE_URL + "/transaction/initialize", fields, SECRETE_KEY); - statusCode = chapaClient.getStatusCode(); - - if(!Util.is2xxSuccessful(chapaClient.getStatusCode())) { - throw new ChapaException(responseBody); + public InitializeResponse initialize(PostData postData) throws ChapaException { + Map fields = postData.getAsMap(); + + putIfNotNull(fields, "return_url", postData.getReturnUrl()); + putIfNotNull(fields, "callback_url", postData.getCallbackUrl()); + putIfNotNull(fields, "subaccounts[id]", postData.getSubAccountId()); + putIfNotNull(fields, "customization[title]", Optional.ofNullable(postData.getCustomization()).map(Customization::getTitle).orElse(EMPTY)); + putIfNotNull(fields, "customization[description]", Optional.ofNullable(postData.getCustomization()).map(Customization::getDescription).orElse(EMPTY)); + putIfNotNull(fields, "phone_number", Optional.ofNullable(postData.getPhoneNumber()).orElse(EMPTY)); + + if (fields.isEmpty() || isAnyNull(fields, "amount", "currency", "tx_ref")) { + throw new ChapaException("Wrong or empty payload"); } - - InitializeResponseData initializeResponseData = Util.jsonToInitializeResponseData(responseBody) - .setRawJson(responseBody) - .setStatusCode(statusCode); - - return initializeResponseData; + return chapaClient.initialize(secreteKey, fields); } /** @@ -111,121 +63,82 @@ public InitializeResponseData initialize(PostData postData) throws Throwable { / * of {@link #initialize(PostData)}.


* * @param jsonData A json string containing post fields. - * @return An object of {@link InitializeResponseData} containing - * response data from Chapa API. - * @throws Throwable Throws an exception for failed request to Chapa API. + * @return An object of {@link InitializeResponse} containing + * response data from Chapa API. + * @throws ChapaException Throws an exception for failed request to Chapa API. */ - public InitializeResponseData initialize(String jsonData) throws Throwable { - responseBody = chapaClient.post(BASE_URL + "/transaction/initialize", jsonData, SECRETE_KEY); - statusCode = chapaClient.getStatusCode(); - - if(!Util.is2xxSuccessful(chapaClient.getStatusCode())) { - throw new ChapaException(responseBody); - } - - InitializeResponseData initializeResponseData = Util.jsonToInitializeResponseData(responseBody) - .setRawJson(responseBody) - .setStatusCode(statusCode); - - return initializeResponseData; + public InitializeResponse initialize(String jsonData) throws ChapaException { + return chapaClient.initialize(secreteKey, jsonData); } /** * @param transactionRef A transaction reference which was associated * with tx_ref field in post data. This field uniquely * identifies a transaction. - * @return An object of {@link VerifyResponseData} containing - * response data from Chapa API. - * @throws Throwable Throws an exception for failed request to Chapa API. + * @return An object of {@link VerifyResponse} containing + * response data from Chapa API. + * @throws ChapaException Throws an exception for failed request to Chapa API. */ - public VerifyResponseData verify(String transactionRef) throws Throwable { - if (!Util.notNullAndEmpty(transactionRef)) { - throw new IllegalArgumentException("Transaction reference can't be null or empty"); + public VerifyResponse verify(String transactionRef) throws ChapaException { + if (StringUtils.isBlank(transactionRef)) { + throw new ChapaException("Transaction reference can't be null or empty"); } - responseBody = chapaClient.get(BASE_URL + "/transaction/verify/" + transactionRef, SECRETE_KEY); - statusCode = chapaClient.getStatusCode(); - - if(!Util.is2xxSuccessful(chapaClient.getStatusCode())) { - throw new ChapaException(responseBody); - } - - VerifyResponseData verifyResponseData = Util.jsonToVerifyResponseData(responseBody) - .setRawJson(responseBody) - .setStatusCode(statusCode); - - return verifyResponseData; + return chapaClient.verify(secreteKey, transactionRef); } /** * @return A list of {@link Bank} containing all banks supported by Chapa. - * @throws Throwable Throws an exception for failed request to Chapa API. + * @throws ChapaException Throws an exception for failed request to Chapa API. */ - public List banks() throws Throwable { - responseBody = chapaClient.get(BASE_URL + "/banks", SECRETE_KEY); - statusCode = chapaClient.getStatusCode(); - - if(!Util.is2xxSuccessful(chapaClient.getStatusCode())) { - throw new ChapaException(responseBody); - } - - return Util.extractBanks(responseBody); + public ResponseBanks getBanks() throws ChapaException { + return chapaClient.getBanks(secreteKey); } /** *

This method is used to create a sub account in Chapa. It is an overloaded method * of {@link #createSubAccount(String)}.


* - * @param subAccount An object of {@link SubAccount} containing - * sub account details. - * @return An object of {@link SubAccountResponseData} containing - * response data from Chapa API. - * @throws Throwable Throws an exception for failed request to Chapa API. + * @param subAccountDto An object of {@link SubAccountDto} containing sub-account details. + * @return An object of {@link SubAccountResponse} containing response data from Chapa API. + * @throws ChapaException Throws an exception for failed request to Chapa API. */ - public SubAccountResponseData createSubAccount(SubAccount subAccount) throws Throwable { - Map fields = new HashMap<>(); - fields.put("business_name", subAccount.getBusinessName()); - fields.put("account_name", subAccount.getAccountName()); - fields.put("account_number", subAccount.getAccountNumber()); - fields.put("bank_code", subAccount.getBankCode()); - fields.put("split_type", subAccount.getSplitType().name().toLowerCase()); - fields.put("split_value", subAccount.getSplitValue()); - responseBody = chapaClient.post(BASE_URL + "/subaccount", fields, SECRETE_KEY); - statusCode = chapaClient.getStatusCode(); - - if(!Util.is2xxSuccessful(chapaClient.getStatusCode())) { - throw new ChapaException(responseBody); - } - - SubAccountResponseData subAccountResponseData = Util.jsonToSubAccountResponseData(responseBody) - .setRawJson(responseBody) - .setStatusCode(statusCode); - - return subAccountResponseData; + public SubAccountResponse createSubAccount(SubAccountDto subAccountDto) throws ChapaException { + return chapaClient.createSubAccount(secreteKey, subAccountDto.getAsMap()); } /** *

This method is used to create a sub account in Chapa. It is an overloaded method - * of {@link #createSubAccount(SubAccount)}.


+ * of {@link #createSubAccount(SubAccountDto)}.


* * @param jsonData A json string containing sub account details. - * @return An object of {@link SubAccountResponseData} containing - * response data from Chapa API. - * @throws Throwable Throws an exception for failed request to Chapa API. + * @return An object of {@link SubAccountResponse} containing response data from Chapa API. + * @throws ChapaException Throws an exception for failed request to Chapa API. */ - public SubAccountResponseData createSubAccount(String jsonData) throws Throwable { - responseBody = chapaClient.post(BASE_URL + "/subaccount", jsonData, SECRETE_KEY); - statusCode = chapaClient.getStatusCode(); + public SubAccountResponse createSubAccount(String jsonData) throws ChapaException { + return chapaClient.createSubAccount(secreteKey, jsonData); + } + + public static class Builder { + private IChapaClient client; + private String secretKey; - if(!Util.is2xxSuccessful(chapaClient.getStatusCode())) { - throw new ChapaException(responseBody); + public Builder client(IChapaClient client) { + if (client == null) { + throw new IllegalArgumentException("Client can't be null"); + } + this.client = client; + return this; } - SubAccountResponseData subAccountResponseData = Util.jsonToSubAccountResponseData(responseBody) - .setRawJson(responseBody) - .setStatusCode(statusCode); + public Builder secretKey(String secretKey) { + this.secretKey = secretKey; + return this; + } - return subAccountResponseData; + public Chapa build() { + return new Chapa(this); + } } } diff --git a/src/main/java/com/yaphet/chapa/client/ChapaClient.java b/src/main/java/com/yaphet/chapa/client/ChapaClient.java index 493e097..23b88f4 100644 --- a/src/main/java/com/yaphet/chapa/client/ChapaClient.java +++ b/src/main/java/com/yaphet/chapa/client/ChapaClient.java @@ -1,20 +1,102 @@ package com.yaphet.chapa.client; +import com.yaphet.chapa.exception.ChapaException; +import com.yaphet.chapa.model.InitializeResponse; +import com.yaphet.chapa.model.ResponseBanks; +import com.yaphet.chapa.model.SubAccountResponse; +import com.yaphet.chapa.model.VerifyResponse; +import okhttp3.ResponseBody; +import retrofit2.Response; + +import java.io.IOException; import java.util.Map; +import java.util.Optional; + +import static com.yaphet.chapa.utility.Util.jsonToMap; + +public class ChapaClient implements IChapaClient { + + public static final String BEARER = "Bearer "; + + protected ChapaClientApi chapaClientApi; + + public ChapaClient(ChapaClientApi client) { + if (client == null) { + throw new IllegalArgumentException("Chapa client can't be null"); + } + this.chapaClientApi = client; + } + + @Override + public InitializeResponse initialize(final String secretKey, Map fields) throws ChapaException { + try { + Response response = chapaClientApi.initialize(BEARER + secretKey, fields).execute(); + if (!response.isSuccessful()) { + throw new ChapaException(extractErrorMessageOrDefault(response.errorBody(), "Unable to Initialize transaction.")); + } + return response.body(); + } catch (Exception e) { + throw new ChapaException("Unable to Initialize transaction.", e); + } + } + + @Override + public InitializeResponse initialize(final String secretKey, final String body) throws ChapaException { + return this.initialize(secretKey, jsonToMap(body)); + } -/** - *

A client to interact with Chapa API.


- * - * Implement this interface to create your own client or use the default implementation {@link ChapaClientImpl}. - */ -public interface ChapaClient { + @Override + public VerifyResponse verify(final String secretKey, final String transactionReference) throws ChapaException { + try { + Response response = chapaClientApi.verify(BEARER + secretKey, transactionReference).execute(); + if (!response.isSuccessful()) { + throw new ChapaException(extractErrorMessageOrDefault(response.errorBody(), "Unable to verify transaction.")); + } + return response.body(); + } catch (Exception e) { + throw new ChapaException("Unable to verify transaction.", e); + } + } + @Override + public ResponseBanks getBanks(final String secretKey) throws ChapaException { + try { + Response response = chapaClientApi.banks(BEARER + secretKey).execute(); + if (!response.isSuccessful()) { + throw new ChapaException(extractErrorMessageOrDefault(response.errorBody(), "Unable to get bank details.")); + } + return response.body() != null ? response.body() : null; + } catch (Exception e) { + throw new ChapaException("Unable to get bank details.", e); + } + } - String post(String url, Map fields, String secreteKey) throws Throwable; + @Override + public SubAccountResponse createSubAccount(final String secretKey, Map fields) throws ChapaException { + try { + Response response = chapaClientApi.createSubAccount(BEARER + secretKey, fields).execute(); + if (!response.isSuccessful()) { + throw new ChapaException(extractErrorMessageOrDefault(response.errorBody(), "Unable to create sub account.")); + } + return response.body(); + } catch (Exception e) { + throw new ChapaException("Unable to create sub account.", e); + } + } - String post(String url, String body, String secreteKey) throws Throwable; + @Override + public SubAccountResponse createSubAccount(final String secretKey, final String body) throws ChapaException { + return this.createSubAccount(secretKey, jsonToMap(body)); + } - String get(String url, String secreteKey) throws Throwable; + private String extractErrorMessageOrDefault(ResponseBody errorMessage, String defaultMessage) { + return Optional.ofNullable(errorMessage).map(t -> { + try { + return t.string(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).orElse(defaultMessage); + } - int getStatusCode(); } diff --git a/src/main/java/com/yaphet/chapa/client/ChapaClientApi.java b/src/main/java/com/yaphet/chapa/client/ChapaClientApi.java new file mode 100644 index 0000000..3f247f9 --- /dev/null +++ b/src/main/java/com/yaphet/chapa/client/ChapaClientApi.java @@ -0,0 +1,33 @@ +package com.yaphet.chapa.client; + +import com.yaphet.chapa.model.InitializeResponse; +import com.yaphet.chapa.model.ResponseBanks; +import com.yaphet.chapa.model.SubAccountResponse; +import com.yaphet.chapa.model.VerifyResponse; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; +import retrofit2.http.Path; + +import java.util.Map; + + +/** + * Chapa default retrofit client + */ +public interface ChapaClientApi { + + @POST("transaction/initialize") + Call initialize(@Header("Authorization") String authorizationHeader, @Body Map body); + + @GET("transaction/verify/{tx_ref}") + Call verify(@Header("Authorization") String authorizationHeader, @Path("tx_ref") String transactionReference); + + @GET("banks") + Call banks(@Header("Authorization") String authorizationHeader); + + @POST("subaccount") + Call createSubAccount(@Header("Authorization") String authorizationHeader, @Body Map body); +} diff --git a/src/main/java/com/yaphet/chapa/client/ChapaClientImpl.java b/src/main/java/com/yaphet/chapa/client/ChapaClientImpl.java deleted file mode 100644 index 7613c02..0000000 --- a/src/main/java/com/yaphet/chapa/client/ChapaClientImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.yaphet.chapa.client; - -import java.util.Map; - -import com.mashape.unirest.http.HttpResponse; -import com.mashape.unirest.http.JsonNode; -import org.apache.http.HttpHeaders; - -import com.mashape.unirest.http.Unirest; -import com.mashape.unirest.request.body.MultipartBody; -import com.mashape.unirest.request.body.RequestBodyEntity; - -/** - *

A default implementation of {@link ChapaClient}.


- * - *

Uses Unirest to make HTTP requests.

- * - * @see Unirest - */ -public class ChapaClientImpl implements ChapaClient { - - private static final String authorizationHeader = HttpHeaders.AUTHORIZATION; - private static final String acceptEncodingHeader = HttpHeaders.ACCEPT_ENCODING; - - private int statusCode; - - @Override - public String post(String url, Map fields, String secreteKey) throws Throwable { - MultipartBody request = Unirest.post(url) - .header(acceptEncodingHeader, "application/json") - .header(authorizationHeader, "Bearer " + secreteKey) - .fields(fields); - - HttpResponse jsonNodeHttpResponse = request.asJson(); - - statusCode = jsonNodeHttpResponse.getStatus(); - return jsonNodeHttpResponse.getBody().toString(); - } - - @Override - public String post(String url, String body, String secreteKey) throws Throwable { - RequestBodyEntity request = Unirest.post(url) - .header(acceptEncodingHeader, "application/json") - .header(authorizationHeader, "Bearer " + secreteKey) - .body(body); - - HttpResponse jsonNodeHttpResponse = request.asJson(); - - statusCode = jsonNodeHttpResponse.getStatus(); - return jsonNodeHttpResponse.getBody().toString(); - } - - @Override - public String get(String url, String secreteKey) throws Throwable { - HttpResponse httpResponse = Unirest.get(url) - .header(acceptEncodingHeader, "application/json") - .header(authorizationHeader, "Bearer " + secreteKey) - .asJson(); - - statusCode = httpResponse.getStatus(); - return httpResponse.getBody().toString(); - } - - @Override - public int getStatusCode() { - return statusCode; - } -} diff --git a/src/main/java/com/yaphet/chapa/client/IChapaClient.java b/src/main/java/com/yaphet/chapa/client/IChapaClient.java new file mode 100644 index 0000000..db9d5f7 --- /dev/null +++ b/src/main/java/com/yaphet/chapa/client/IChapaClient.java @@ -0,0 +1,90 @@ +package com.yaphet.chapa.client; + + +import com.yaphet.chapa.exception.ChapaException; +import com.yaphet.chapa.model.InitializeResponse; +import com.yaphet.chapa.model.ResponseBanks; +import com.yaphet.chapa.model.SubAccountResponse; +import com.yaphet.chapa.model.VerifyResponse; + +import java.util.Map; + +/** + * Chapa default retrofit client interface + *

+ * Custom implementation can be created by implementing this interface + */ +public interface IChapaClient { + + /** + * Initializes transaction + * + * @see Chapa Dcoumentation + * + * @param secretKey + * @param fields + * @return InitializeResponse + * @throws ChapaException + */ + InitializeResponse initialize(final String secretKey, Map fields) throws ChapaException; + + /** + * An alias to {@link #initialize(String, Map)} + * + * @see Chapa Dcoumentation + * + * @param secretKey + * @param body + * @return InitializeResponse + * @throws ChapaException + */ + InitializeResponse initialize(final String secretKey, final String body) throws ChapaException; + + /** + * Verify a transaction + * + * @see Chapa Dcoumentation + * + * @param secretKey + * @param transactionReference + * @return VerifyResponse + * @throws ChapaException + */ + VerifyResponse verify(String secretKey, String transactionReference) throws ChapaException; + + /** + * Get all supported banks + * + * @see Chapa Dcoumentation + * + * @param secretKey + * @return ResponseBanks + * @throws ChapaException + */ + ResponseBanks getBanks(String secretKey) throws ChapaException; + + /** + * + * Create sub account for split payment + * + * @see Chapa Dcoumentation + * + * @param secretKey + * @param fields + * @return SubAccountResponse + * @throws ChapaException + */ + SubAccountResponse createSubAccount(String secretKey, Map fields) throws ChapaException; + + /** + * Create sub account for split payment, an alternative method to {@link #createSubAccount(String, Map)} + * + * @see Chapa Dcoumentation + * + * @param body + * @param secretKey + * @return SubAccountResponse + * @throws ChapaException + */ + SubAccountResponse createSubAccount(String secretKey, String body) throws ChapaException; +} diff --git a/src/main/java/com/yaphet/chapa/client/provider/BaseRetrofitClientProvider.java b/src/main/java/com/yaphet/chapa/client/provider/BaseRetrofitClientProvider.java new file mode 100644 index 0000000..b7fc69f --- /dev/null +++ b/src/main/java/com/yaphet/chapa/client/provider/BaseRetrofitClientProvider.java @@ -0,0 +1,66 @@ +package com.yaphet.chapa.client.provider; + +import com.yaphet.chapa.client.ChapaClientApi; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; + +import java.util.concurrent.TimeUnit; + +public abstract class BaseRetrofitClientProvider implements RetrofitClientProvider { + + private boolean debug; + private HttpLoggingInterceptor loggingInterceptor; + + protected OkHttpClient httpClient; + + protected BaseRetrofitClientProvider(long timeoutMillis) { + this.httpClient = createHttpClient(timeoutMillis); + } + + @Override + public RetrofitClientProvider setDebug(boolean debug) { + if (this.debug != debug) { + if (debug) { + if (loggingInterceptor == null) { + loggingInterceptor = new HttpLoggingInterceptor(); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + } + httpClient = httpClient.newBuilder().addInterceptor(loggingInterceptor).build(); + } else { + httpClient.newBuilder().interceptors().remove(loggingInterceptor); + loggingInterceptor = null; + } + } + this.debug = debug; + return this; + } + + @Override + public RetrofitClientProvider setClient(OkHttpClient client) { + if (client == null) { + throw new IllegalArgumentException("Client can't be null"); + } + this.httpClient = client; + return this; + } + + public boolean isDebug() { + return debug; + } + + public OkHttpClient getHttpClient() { + return httpClient; + } + + public ChapaClientApi create() { + return provideRetrofitBuilder().create(ChapaClientApi.class); + } + + private static OkHttpClient createHttpClient(long timeoutMillis) { + return new OkHttpClient.Builder() + .connectTimeout(timeoutMillis, TimeUnit.MILLISECONDS) + .readTimeout(timeoutMillis, TimeUnit.MILLISECONDS) + .writeTimeout(timeoutMillis, TimeUnit.MILLISECONDS) + .build(); + } +} diff --git a/src/main/java/com/yaphet/chapa/client/provider/DefaultRetrofitClientProvider.java b/src/main/java/com/yaphet/chapa/client/provider/DefaultRetrofitClientProvider.java new file mode 100644 index 0000000..64b1ff2 --- /dev/null +++ b/src/main/java/com/yaphet/chapa/client/provider/DefaultRetrofitClientProvider.java @@ -0,0 +1,67 @@ +package com.yaphet.chapa.client.provider; + +import com.yaphet.chapa.utility.StringUtils; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +/** + * + * Provides retrofit client to make calls to chapa api. + *

+ * This implementation will not retry on failures. + *

+ *  * Example usage:
+ *  * public class CustomChapaClient implements IChapaClient {
+ *  *
+ *  *     private ChapaClientApi chapaClientApi;
+ *  *     .
+ *  *     .
+ *  *     private void buildApiClient() {
+ *  *          if (isBlank(baseUrl)) throw new ChapaException("Unable to create a client. Api baseUrl can't be empty");
+ *  *          chapaClientApi = new DefaultRetrofitClientProvider.Builder().timeout(10000).baseUrl("https://chapa.example.com").build().createChapaClient();
+ *  *     }
+ *  * }
+ *  * 
+ */ +public class DefaultRetrofitClientProvider extends BaseRetrofitClientProvider { + + private static final long DEFAULT_TIMEOUT = 10000; + + private final String baseUrl; + + private DefaultRetrofitClientProvider(Builder builder) { + super(builder.timeoutMillis); + if (StringUtils.isBlank(builder.baseUrl)) { + throw new IllegalArgumentException("Api baseUrl can't be null"); + } + this.baseUrl = builder.baseUrl; + } + + @Override + public Retrofit provideRetrofitBuilder() { + return new Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .client(httpClient) + .build(); + } + + public static class Builder { + private String baseUrl = ""; + private long timeoutMillis = DEFAULT_TIMEOUT; + + public Builder timeout(long timeoutMillis) { + this.timeoutMillis = timeoutMillis; + return this; + } + + public Builder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + public DefaultRetrofitClientProvider build() { + return new DefaultRetrofitClientProvider(this); + } + } +} diff --git a/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java b/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java new file mode 100644 index 0000000..acf309b --- /dev/null +++ b/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java @@ -0,0 +1,97 @@ +package com.yaphet.chapa.client.provider; + +import com.yaphet.chapa.utility.StringUtils; +import io.github.resilience4j.core.IntervalFunction; +import io.github.resilience4j.retrofit.RetryCallAdapter; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +/** + * Provides retrofit client to make calls to chapa api.
+ *

+ * This implementation will retry calls to chapa api upto 3 (default) times. You can set the number of retries too.
+ *

+ * Retry will be done with an ExponentialBackoff strategy.
+ *

+ * Timeouts are all set to 10_000 millis
+ * + * + *

+ * Example usage:
+ * public class CustomChapaClient implements IChapaClient {
+ *
+ *     private ChapaClientApi chapaClientApi;
+ *     .
+ *     .
+ *     private void buildApiClient() {
+ *          if (isBlank(baseUrl)) throw new ChapaException("Unable to create a client. Api baseUrl can't be empty");
+ *          chapaClientApi = new RetrierRetrofitClientProvider.Builder().timeout(10000).retryCount(3).baseUrl("https://chapa.example.com").build().create();
+ *     }
+ * }
+ * 
+ */ +public class RetrierRetrofitClientProvider extends BaseRetrofitClientProvider { + + private static final long DEFAULT_TIMEOUT = 10000; + private static final int DEFAULT_RETRY_COUNT = 3; + private static final double BACKOFF_MULTIPLIER = 2.0; + + private final RetryConfig retryConfig; + private final String baseUrl; + + private RetrierRetrofitClientProvider(Builder builder) { + super(builder.timeoutMillis); + this.retryConfig = createRetryConfig(builder.timeoutMillis, builder.maxRetries); + if (StringUtils.isBlank(builder.baseUrl)) { + throw new IllegalArgumentException("Api baseUrl can't be null"); + } + this.baseUrl = builder.baseUrl; + } + + private static RetryConfig createRetryConfig(long initialInterval, int maxAttempts) { + return RetryConfig.custom() + .maxAttempts(maxAttempts) + .intervalFunction(IntervalFunction.ofExponentialBackoff(initialInterval, BACKOFF_MULTIPLIER)) + .retryOnResult(response -> response instanceof Response && !((Response) response).isSuccessful()) + .failAfterMaxAttempts(true) + .build(); + } + + @Override + public Retrofit provideRetrofitBuilder() { + return new Retrofit.Builder() + .addCallAdapterFactory(RetryCallAdapter.of(Retry.of("chapa-retry", retryConfig))) + .baseUrl(this.baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .client(httpClient) + .build(); + } + + public static class Builder { + private long timeoutMillis = DEFAULT_TIMEOUT; + private int maxRetries = DEFAULT_RETRY_COUNT; + private String baseUrl = ""; + + public Builder timeout(long timeoutMillis) { + this.timeoutMillis = timeoutMillis; + return this; + } + + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + public Builder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + public RetrierRetrofitClientProvider build() { + return new RetrierRetrofitClientProvider(this); + } + } +} diff --git a/src/main/java/com/yaphet/chapa/client/provider/RetrofitClientProvider.java b/src/main/java/com/yaphet/chapa/client/provider/RetrofitClientProvider.java new file mode 100644 index 0000000..61b993b --- /dev/null +++ b/src/main/java/com/yaphet/chapa/client/provider/RetrofitClientProvider.java @@ -0,0 +1,13 @@ +package com.yaphet.chapa.client.provider; + +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +public interface RetrofitClientProvider { + + Retrofit provideRetrofitBuilder(); + + RetrofitClientProvider setDebug(boolean debug); + + RetrofitClientProvider setClient(OkHttpClient client); +} diff --git a/src/main/java/com/yaphet/chapa/model/Bank.java b/src/main/java/com/yaphet/chapa/model/Bank.java index 079f0a7..3cfb298 100644 --- a/src/main/java/com/yaphet/chapa/model/Bank.java +++ b/src/main/java/com/yaphet/chapa/model/Bank.java @@ -1,17 +1,42 @@ package com.yaphet.chapa.model; +import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; +import com.yaphet.chapa.utility.LocalDateTimeDeserializer; + +import java.time.LocalDateTime; public class Bank { private String id; + private String name; + @SerializedName("country_id") private int countryId; + + private Integer active; + + @SerializedName("acct_length") + private Integer accountLength; + + @SerializedName("is_mobilemoney") + private Integer isMobileMoney; + + @SerializedName("is_rtgs") + private Integer isRtgs; + + private String swift; + + private String currency; + @SerializedName("created_at") - private String createdAt; + @JsonAdapter(LocalDateTimeDeserializer.class) + private LocalDateTime createdAt; + @SerializedName("updated_at") - private String updatedAt; + @JsonAdapter(LocalDateTimeDeserializer.class) + private LocalDateTime updatedAt; public String getId() { return id; @@ -40,21 +65,80 @@ public Bank setCountryId(int countryId) { return this; } - public String getCreatedAt() { + public Integer getActive() { + return active; + } + + public void setActive(Integer active) { + this.active = active; + } + + public Integer getAccountLength() { + return accountLength; + } + + public void setAccountLength(Integer accountLength) { + this.accountLength = accountLength; + } + + public Integer getIsMobileMoney() { + return isMobileMoney; + } + + public void setIsMobileMoney(Integer isMobileMoney) { + this.isMobileMoney = isMobileMoney; + } + + public Integer getIsRtgs() { + return isRtgs; + } + + public void setIsRtgs(Integer isRtgs) { + this.isRtgs = isRtgs; + } + + public String getSwift() { + return swift; + } + + public void setSwift(String swift) { + this.swift = swift; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public LocalDateTime getCreatedAt() { return createdAt; } - public Bank setCreatedAt(String createdAt) { + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; - return this; } - public String getUpdatedAt() { + public LocalDateTime getUpdatedAt() { return updatedAt; } - public Bank setUpdatedAt(String updatedAt) { + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; - return this; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Bank{"); + sb.append("name='").append(name).append('\''); + sb.append(", countryId=").append(countryId); + sb.append(", active=").append(active); + sb.append(", accountLength=").append(accountLength); + sb.append(", swift='").append(swift).append('\''); + sb.append(", currency='").append(currency).append('\''); + sb.append('}'); + return sb.toString(); } } diff --git a/src/main/java/com/yaphet/chapa/model/InitializeResponse.java b/src/main/java/com/yaphet/chapa/model/InitializeResponse.java new file mode 100644 index 0000000..2891ea2 --- /dev/null +++ b/src/main/java/com/yaphet/chapa/model/InitializeResponse.java @@ -0,0 +1,76 @@ +package com.yaphet.chapa.model; + +import com.google.gson.annotations.SerializedName; + +public class InitializeResponse extends Response { + + private Data data; + + public InitializeResponse() { + } + + public InitializeResponse(String rawJson, String message, String status, int statusCode, Data data) { + super(rawJson, message, status, statusCode); + this.data = data; + } + + @Override + public InitializeResponse setMessage(String message) { + super.setMessage(message); + return this; + } + + @Override + public InitializeResponse setStatus(String status) { + super.setStatus(status); + return this; + } + + @Override + public InitializeResponse setStatusCode(int statusCode) { + super.setStatusCode(statusCode); + return this; + } + + @Override + public InitializeResponse setRawJson(String rawJson) { + super.setRawJson(rawJson); + return this; + } + + public InitializeResponse setData(Data data) { + this.data = data; + return this; + } + + public Data getData() { + return data; + } + + public static class Data { + @SerializedName("checkout_url") + private String checkOutUrl; + + public String getCheckOutUrl() { + return checkOutUrl; + } + + public void setCheckOutUrl(String checkOutUrl) { + this.checkOutUrl = checkOutUrl; + } + + @Override + public String toString() { + return "Data {" + "checkOutUrl='" + checkOutUrl + "'}"; + } + } + + @Override + public String toString() { + return "InitializeResponse{" + "status=" + this.getStatus() + + ", statusCode=" + this.getStatusCode() + + ", message=" + this.getMessage() + + ", data=" + data + + "}"; + } +} diff --git a/src/main/java/com/yaphet/chapa/model/InitializeResponseData.java b/src/main/java/com/yaphet/chapa/model/InitializeResponseData.java deleted file mode 100644 index d08409b..0000000 --- a/src/main/java/com/yaphet/chapa/model/InitializeResponseData.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.yaphet.chapa.model; - -import com.google.gson.annotations.SerializedName; - -public class InitializeResponseData extends ResponseData { - - private Data data; - - public InitializeResponseData() { - } - - public InitializeResponseData(String rawJson, String message, String status, int statusCode, Data data) { - super(rawJson, message, status, statusCode); - this.data = data; - } - - public InitializeResponseData setMessage(String message) { - return (InitializeResponseData) super.setMessage(message); - } - - public InitializeResponseData setStatus(String status) { - return (InitializeResponseData) super.setStatus(status); - } - - public InitializeResponseData setStatusCode(int statusCode) { - return (InitializeResponseData) super.setStatusCode(statusCode); - } - - public InitializeResponseData setRawJson(String rawJson) { - return (InitializeResponseData) super.setRawJson(rawJson); - } - - public InitializeResponseData setData(Data data) { - this.data = data; - return this; - } - - public Data getData() { - return data; - } - - public static class Data { - @SerializedName("checkout_url") - private String checkOutUrl; - - public String getCheckOutUrl() { - return checkOutUrl; - } - - public InitializeResponseData.Data setCheckOutUrl(String checkOutUrl) { - this.checkOutUrl = checkOutUrl; - return this; - } - } -} diff --git a/src/main/java/com/yaphet/chapa/model/PostData.java b/src/main/java/com/yaphet/chapa/model/PostData.java index fddacca..7999a35 100644 --- a/src/main/java/com/yaphet/chapa/model/PostData.java +++ b/src/main/java/com/yaphet/chapa/model/PostData.java @@ -1,8 +1,13 @@ package com.yaphet.chapa.model; import com.google.gson.annotations.SerializedName; +import com.yaphet.chapa.utility.Validate; import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +import static com.yaphet.chapa.utility.Util.putIfNotNull; /** * The PostData class is an object representation of JSON form data @@ -10,7 +15,7 @@ */ public class PostData { - private BigDecimal amount; + private String amount; private String currency; private String email; @SerializedName("first_name") @@ -26,12 +31,18 @@ public class PostData { @SerializedName("subaccounts[id]") private String subAccountId; private Customization customization; + @SerializedName("phone_number") + private String phoneNumber; - public BigDecimal getAmount() { + public String getAmount() { return amount; } - public PostData setAmount(BigDecimal amount) { + public BigDecimal getAmountAsBigDecimal() { + return new BigDecimal(amount); + } + + public PostData setAmount(String amount) { this.amount = amount; return this; } @@ -50,6 +61,9 @@ public String getEmail() { } public PostData setEmail(String email) { + if (!Validate.isValidEmail(email)) { + throw new IllegalArgumentException("Invalid email"); + } this.email = email; return this; } @@ -116,4 +130,28 @@ public PostData setCustomization(Customization customization) { this.customization = customization; return this; } + + public String getPhoneNumber() { + return phoneNumber; + } + + public PostData setPhoneNumber(String phoneNumber) { + if (!Validate.isValidPhoneNumber(phoneNumber)) { + throw new IllegalArgumentException("Invalid isValidPhoneNumber number"); + } + this.phoneNumber = phoneNumber; + return this; + } + + public Map getAsMap() { + Map postData = new HashMap<>(); + putIfNotNull(postData, "amount", amount); + putIfNotNull(postData, "currency", currency); + putIfNotNull(postData, "email", email); + putIfNotNull(postData, "first_name", firstName); + putIfNotNull(postData, "last_name", lastName); + putIfNotNull(postData, "tx_ref", txRef); + putIfNotNull(postData, "phone_number", phoneNumber); + return postData; + } } diff --git a/src/main/java/com/yaphet/chapa/model/ResponseData.java b/src/main/java/com/yaphet/chapa/model/Response.java similarity index 70% rename from src/main/java/com/yaphet/chapa/model/ResponseData.java rename to src/main/java/com/yaphet/chapa/model/Response.java index ab9cd9c..ed960f0 100644 --- a/src/main/java/com/yaphet/chapa/model/ResponseData.java +++ b/src/main/java/com/yaphet/chapa/model/Response.java @@ -1,20 +1,20 @@ package com.yaphet.chapa.model; /** - * The ResponseData class is an abstract class that + * The Response class is an abstract class that * represents the response data from Chapa API. */ -public abstract class ResponseData { +public abstract class Response { private String message; private String status; private int statusCode; private String rawJson; - public ResponseData() { + protected Response() { } - public ResponseData(String rawJson, String message, String status, int statusCode) { + protected Response(String rawJson, String message, String status, int statusCode) { this.message = message; this.status = status; this.statusCode = statusCode; @@ -25,7 +25,7 @@ public String getMessage() { return message; } - public ResponseData setMessage(String message) { + public Response setMessage(String message) { this.message = message; return this; } @@ -34,7 +34,7 @@ public String getStatus() { return status; } - public ResponseData setStatus(String status) { + public Response setStatus(String status) { this.status = status; return this; } @@ -43,7 +43,7 @@ public int getStatusCode() { return statusCode; } - public ResponseData setStatusCode(int statusCode) { + public Response setStatusCode(int statusCode) { this.statusCode = statusCode; return this; } @@ -60,7 +60,7 @@ public String asString() { return rawJson; } - public ResponseData setRawJson(String rawJson) { + public Response setRawJson(String rawJson) { this.rawJson = rawJson; return this; } diff --git a/src/main/java/com/yaphet/chapa/model/ResponseBanks.java b/src/main/java/com/yaphet/chapa/model/ResponseBanks.java new file mode 100644 index 0000000..daa1d00 --- /dev/null +++ b/src/main/java/com/yaphet/chapa/model/ResponseBanks.java @@ -0,0 +1,54 @@ +package com.yaphet.chapa.model; + +import java.util.ArrayList; +import java.util.List; + +public class ResponseBanks extends Response { + + private List data; + + public List getData() { + if(data == null) { + return new ArrayList<>(); + } + return data; + } + + @Override + public ResponseBanks setMessage(String message) { + super.setMessage(message); + return this; + } + + @Override + public ResponseBanks setStatus(String status) { + super.setStatus(status); + return this; + } + + @Override + public ResponseBanks setStatusCode(int statusCode) { + super.setStatusCode(statusCode); + return this; + } + + @Override + public ResponseBanks setRawJson(String rawJson) { + super.setRawJson(rawJson); + return this; + } + + public ResponseBanks setData(List data) { + this.data = data; + return this; + } + + @Override + public String toString() { + return "ResponseBanks{" + "status=" + this.getStatus() + + ", statusCode=" + this.getStatusCode() + + ", message=" + this.getMessage() + + ", data=" + data + + '}'; + } +} diff --git a/src/main/java/com/yaphet/chapa/model/SplitTypeEnum.java b/src/main/java/com/yaphet/chapa/model/SplitTypeEnum.java new file mode 100644 index 0000000..86f4735 --- /dev/null +++ b/src/main/java/com/yaphet/chapa/model/SplitTypeEnum.java @@ -0,0 +1,6 @@ +package com.yaphet.chapa.model; + +public enum SplitTypeEnum { + PERCENTAGE, + FLAT +} diff --git a/src/main/java/com/yaphet/chapa/model/SubAccountDto.java b/src/main/java/com/yaphet/chapa/model/SubAccountDto.java new file mode 100644 index 0000000..b5d61e3 --- /dev/null +++ b/src/main/java/com/yaphet/chapa/model/SubAccountDto.java @@ -0,0 +1,89 @@ +package com.yaphet.chapa.model; + +import com.google.gson.annotations.SerializedName; + +import java.util.HashMap; +import java.util.Map; + +import static com.yaphet.chapa.utility.Util.putIfNotNull; + +public class SubAccountDto { + + @SerializedName("business_name") + private String businessName; + @SerializedName("bank_code") + private String bankCode; + @SerializedName("account_name") + private String accountName; + @SerializedName("account_number") + private String accountNumber; + @SerializedName("split_type") + private SplitTypeEnum splitType; + @SerializedName("split_value") + private Double splitValue; + + public String getBusinessName() { + return businessName; + } + + public SubAccountDto setBusinessName(String businessName) { + this.businessName = businessName; + return this; + } + + public String getBankCode() { + return bankCode; + } + + public SubAccountDto setBankCode(String bankCode) { + this.bankCode = bankCode; + return this; + } + + public String getAccountName() { + return accountName; + } + + public SubAccountDto setAccountName(String accountName) { + this.accountName = accountName; + return this; + } + + public String getAccountNumber() { + return accountNumber; + } + + public SubAccountDto setAccountNumber(String accountNumber) { + this.accountNumber = accountNumber; + return this; + } + + public SplitTypeEnum getSplitType() { + return splitType; + } + + public SubAccountDto setSplitType(SplitTypeEnum splitType) { + this.splitType = splitType; + return this; + } + + public Double getSplitValue() { + return splitValue; + } + + public SubAccountDto setSplitValue(Double splitValue) { + this.splitValue = splitValue; + return this; + } + + public Map getAsMap() { + Map account = new HashMap<>(); + putIfNotNull(account, "business_name", businessName); + putIfNotNull(account, "account_name", accountName); + putIfNotNull(account, "account_number", accountNumber); + putIfNotNull(account, "bank_code", bankCode); + putIfNotNull(account, "split_type", splitType.name().toLowerCase()); + putIfNotNull(account, "split_value", splitValue != null ? splitValue.toString() : null); + return account; + } +} diff --git a/src/main/java/com/yaphet/chapa/model/SubAccountResponse.java b/src/main/java/com/yaphet/chapa/model/SubAccountResponse.java new file mode 100644 index 0000000..0bfa337 --- /dev/null +++ b/src/main/java/com/yaphet/chapa/model/SubAccountResponse.java @@ -0,0 +1,72 @@ +package com.yaphet.chapa.model; + +import com.google.gson.annotations.SerializedName; + +public class SubAccountResponse extends Response { + + private Data data; + + public SubAccountResponse() { + } + + public SubAccountResponse(String rawJson, String message, String status, int statusCode, Data data) { + super(rawJson, message, status, statusCode); + this.data = data; + } + + public SubAccountResponse setData(Data data) { + this.data = data; + return this; + } + + @Override + public SubAccountResponse setMessage(String message) { + super.setMessage(message); + return this; + } + + @Override + public SubAccountResponse setStatus(String status) { + super.setStatus(status); + return this; + } + + @Override + public SubAccountResponse setStatusCode(int statusCode) { + super.setStatusCode(statusCode); + return this; + } + + @Override + public SubAccountResponse setRawJson(String rawJson) { + super.setRawJson(rawJson); + return this; + } + + public Data getData() { + return data; + } + + public static class Data { + @SerializedName("subaccounts[id]") + private String subAccountId; + + public String getSubAccountId() { + return subAccountId; + } + + public Data setSubAccountId(String subAccountId) { + this.subAccountId = subAccountId; + return this; + } + } + + @Override + public String toString() { + return "SubAccountResponse{" + "status=" + this.getStatus() + + ", statusCode=" + this.getStatusCode() + + ", message=" + this.getMessage() + + ", data=" + data + + '}'; + } +} diff --git a/src/main/java/com/yaphet/chapa/model/SubAccountResponseData.java b/src/main/java/com/yaphet/chapa/model/SubAccountResponseData.java deleted file mode 100644 index 0b6b9d4..0000000 --- a/src/main/java/com/yaphet/chapa/model/SubAccountResponseData.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.yaphet.chapa.model; - -import com.google.gson.annotations.SerializedName; - -public class SubAccountResponseData extends ResponseData { - - private Data data; - - public SubAccountResponseData() { - } - - public SubAccountResponseData(String rawJson, String message, String status, int statusCode, Data data) { - super(rawJson, message, status, statusCode); - this.data = data; - } - - public SubAccountResponseData setMessage(String message) { - return (SubAccountResponseData) super.setMessage(message); - } - - public SubAccountResponseData setStatus(String status) { - return (SubAccountResponseData) super.setStatus(status); - } - - public SubAccountResponseData setStatusCode(int statusCode) { - return (SubAccountResponseData) super.setStatusCode(statusCode); - } - - public SubAccountResponseData setRawJson(String rawJson) { - return (SubAccountResponseData) super.setRawJson(rawJson); - } - - public SubAccountResponseData setData(Data data) { - this.data = data; - return this; - } - - public Data getData() { - return data; - } - - public static class Data { - @SerializedName("subaccounts[id]") - private String subAccountId; - - public String getSubAccountId() { - return subAccountId; - } - - public Data setSubAccountId(String subAccountId) { - this.subAccountId = subAccountId; - return this; - } - } -} diff --git a/src/main/java/com/yaphet/chapa/model/VerifyResponse.java b/src/main/java/com/yaphet/chapa/model/VerifyResponse.java new file mode 100644 index 0000000..889db93 --- /dev/null +++ b/src/main/java/com/yaphet/chapa/model/VerifyResponse.java @@ -0,0 +1,249 @@ +package com.yaphet.chapa.model; + +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.yaphet.chapa.utility.LocalDateTimeDeserializer; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +public class VerifyResponse extends Response { + + private Data data; + + public VerifyResponse() { + } + + public VerifyResponse(String rawJson, String message, String status, int statusCode, Data data) { + super(rawJson, message, status, statusCode); + this.data = data; + } + + @Override + public VerifyResponse setMessage(String message) { + super.setMessage(message); + return this; + } + + @Override + public VerifyResponse setStatus(String status) { + super.setStatus(status); + return this; + } + + @Override + public VerifyResponse setStatusCode(int statusCode) { + super.setStatusCode(statusCode); + return this; + } + + @Override + public VerifyResponse setRawJson(String rawJson) { + super.setRawJson(rawJson); + return this; + } + + public VerifyResponse setData(Data data) { + this.data = data; + return this; + } + + public Data getData() { + return data; + } + + public static class Data { + @SerializedName("first_name") + private String firstName; + @SerializedName("last_name") + private String lastName; + private String email; + private String currency; + private BigDecimal amount; + private BigDecimal charge; + private String mode; + private String method; + private String type; + private String status; + private String reference; + @SerializedName("tx_ref") + private String txRef; + private Customization customization; + private String meta; + @SerializedName("created_at") + @JsonAdapter(LocalDateTimeDeserializer.class) + private LocalDateTime createdAt; + @SerializedName("updated_at") + @JsonAdapter(LocalDateTimeDeserializer.class) + private LocalDateTime updatedAt; + + @SerializedName("first_name") + public String getFirstName() { + return firstName; + } + + public Data setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public String getLastName() { + return lastName; + } + + public Data setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public String getEmail() { + return email; + } + + public Data setEmail(String email) { + this.email = email; + return this; + } + + public String getCurrency() { + return currency; + } + + public Data setCurrency(String currency) { + this.currency = currency; + return this; + } + + public BigDecimal getAmount() { + return amount; + } + + public Data setAmount(BigDecimal amount) { + this.amount = amount; + return this; + } + + public BigDecimal getCharge() { + return charge; + } + + public Data setCharge(BigDecimal charge) { + this.charge = charge; + return this; + } + + public String getMode() { + return mode; + } + + public Data setMode(String mode) { + this.mode = mode; + return this; + } + + public String getMethod() { + return method; + } + + public Data setMethod(String method) { + this.method = method; + return this; + } + + public String getType() { + return type; + } + + public Data setType(String type) { + this.type = type; + return this; + } + + public String getStatus() { + return status; + } + + public Data setStatus(String status) { + this.status = status; + return this; + } + + public String getReference() { + return reference; + } + + public Data setReference(String reference) { + this.reference = reference; + return this; + } + + public String getTxRef() { + return txRef; + } + + public Data setTxRef(String txRef) { + this.txRef = txRef; + return this; + } + + public Customization getCustomization() { + return customization; + } + + public Data setCustomization(Customization customization) { + this.customization = customization; + return this; + } + + public String getMeta() { + return meta; + } + + public Data setMeta(String meta) { + this.meta = meta; + return this; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public Data setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public Data setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + @Override + public String toString() { + return "Data{" + "firstName='" + firstName + '\'' + + ", currency='" + currency + '\'' + + ", amount=" + amount + + ", charge=" + charge + + ", mode='" + mode + '\'' + + ", method='" + method + '\'' + + ", type='" + type + '\'' + + ", status='" + status + '\'' + + ", reference='" + reference + '\'' + + ", txRef='" + txRef + '\'' + + '}'; + } + } + + @Override + public String toString() { + return "VerifyResponse{" + "status=" + this.getStatus() + + ", statusCode=" + this.getStatusCode() + + ", message=" + this.getMessage() + + ", data=" + data + + '}'; + } +} diff --git a/src/main/java/com/yaphet/chapa/model/VerifyResponseData.java b/src/main/java/com/yaphet/chapa/model/VerifyResponseData.java deleted file mode 100644 index c9bcb0d..0000000 --- a/src/main/java/com/yaphet/chapa/model/VerifyResponseData.java +++ /dev/null @@ -1,218 +0,0 @@ -package com.yaphet.chapa.model; - -import com.google.gson.annotations.JsonAdapter; -import com.google.gson.annotations.SerializedName; -import com.yaphet.chapa.utility.LocalDateTimeDeserializer; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -public class VerifyResponseData extends ResponseData { - - private Data data; - - public VerifyResponseData() { - } - - public VerifyResponseData(String rawJson, String message, String status, int statusCode, Data data) { - super(rawJson, message, status, statusCode); - this.data = data; - } - - public VerifyResponseData setMessage(String message) { - return (VerifyResponseData) super.setMessage(message); - } - - public VerifyResponseData setStatus(String status) { - return (VerifyResponseData) super.setStatus(status); - } - - public VerifyResponseData setStatusCode(int statusCode) { - return (VerifyResponseData) super.setStatusCode(statusCode); - } - - public VerifyResponseData setRawJson(String rawJson) { - return (VerifyResponseData) super.setRawJson(rawJson); - } - - public VerifyResponseData setData(Data data) { - this.data = data; - return this; - } - - public Data getData() { - return data; - } - - public static class Data { - - @SerializedName("first_name") - private String firstName; - @SerializedName("last_name") - private String lastName; - private String email; - private String currency; - private BigDecimal amount; - private BigDecimal charge; - private String mode; - private String method; - private String type; - private String status; - private String reference; - @SerializedName("tx_ref") - private String txRef; - private Customization customization; - private String meta; - @SerializedName("created_at") - @JsonAdapter(LocalDateTimeDeserializer.class) - private LocalDateTime createdAt; - @SerializedName("updated_at") - @JsonAdapter(LocalDateTimeDeserializer.class) - private LocalDateTime updatedAt; - - @SerializedName("first_name") - public String getFirstName() { - return firstName; - } - - public VerifyResponseData.Data setFirstName(String firstName) { - this.firstName = firstName; - return this; - } - - public String getLastName() { - return lastName; - } - - public VerifyResponseData.Data setLastName(String lastName) { - this.lastName = lastName; - return this; - } - - public String getEmail() { - return email; - } - - public VerifyResponseData.Data setEmail(String email) { - this.email = email; - return this; - } - - public String getCurrency() { - return currency; - } - - public VerifyResponseData.Data setCurrency(String currency) { - this.currency = currency; - return this; - } - - public BigDecimal getAmount() { - return amount; - } - - public VerifyResponseData.Data setAmount(BigDecimal amount) { - this.amount = amount; - return this; - } - - public BigDecimal getCharge() { - return charge; - } - - public VerifyResponseData.Data setCharge(BigDecimal charge) { - this.charge = charge; - return this; - } - - public String getMode() { - return mode; - } - - public VerifyResponseData.Data setMode(String mode) { - this.mode = mode; - return this; - } - - public String getMethod() { - return method; - } - - public VerifyResponseData.Data setMethod(String method) { - this.method = method; - return this; - } - - public String getType() { - return type; - } - - public VerifyResponseData.Data setType(String type) { - this.type = type; - return this; - } - - public String getStatus() { - return status; - } - - public VerifyResponseData.Data setStatus(String status) { - this.status = status; - return this; - } - - public String getReference() { - return reference; - } - - public VerifyResponseData.Data setReference(String reference) { - this.reference = reference; - return this; - } - - public String getTxRef() { - return txRef; - } - - public VerifyResponseData.Data setTxRef(String txRef) { - this.txRef = txRef; - return this; - } - - public Customization getCustomization() { - return customization; - } - - public VerifyResponseData.Data setCustomization(Customization customization) { - this.customization = customization; - return this; - } - - public String getMeta() { - return meta; - } - - public VerifyResponseData.Data setMeta(String meta) { - this.meta = meta; - return this; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public VerifyResponseData.Data setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - return this; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public VerifyResponseData.Data setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - return this; - } - } -} diff --git a/src/main/java/com/yaphet/chapa/utility/LocalDateTimeDeserializer.java b/src/main/java/com/yaphet/chapa/utility/LocalDateTimeDeserializer.java index 67c0f07..8dae897 100644 --- a/src/main/java/com/yaphet/chapa/utility/LocalDateTimeDeserializer.java +++ b/src/main/java/com/yaphet/chapa/utility/LocalDateTimeDeserializer.java @@ -9,16 +9,10 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -/** - * Custom deserializer for LocalDateTime objects. - */ public class LocalDateTimeDeserializer implements JsonDeserializer { @Override public LocalDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - String dateTimeString = json.getAsString(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"); - LocalDateTime dateTime = LocalDateTime.parse(dateTimeString, formatter); - return dateTime; + return LocalDateTime.parse(json.getAsString(), DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")); } } diff --git a/src/main/java/com/yaphet/chapa/utility/StringUtils.java b/src/main/java/com/yaphet/chapa/utility/StringUtils.java new file mode 100644 index 0000000..2b12629 --- /dev/null +++ b/src/main/java/com/yaphet/chapa/utility/StringUtils.java @@ -0,0 +1,40 @@ +package com.yaphet.chapa.utility; + +public final class StringUtils { + public static final String EMPTY = ""; + + public static boolean isEmpty(String str) { + return str == null || str.isEmpty(); + } + + public static boolean isBlank(String str) { + return str == null || str.trim().isEmpty(); + } + + public static boolean isNotBlank(String str) { + return !isBlank(str); + } + + public static String nullToEmpty(String str) { + return str == null ? "" : str; + } + + public static String defaultIfEmpty(String str, String defaultStr) { + return isEmpty(str) ? defaultStr : str; + } + + public static String reverse(String str) { + return isEmpty(str) ? str : new StringBuilder(str).reverse().toString(); + } + + public static String capitalize(String str) { + if (isEmpty(str)) { + return str; + } + return Character.toUpperCase(str.charAt(0)) + (str.length() > 1 ? str.substring(1) : ""); + } + + private StringUtils() { + } + +} \ No newline at end of file diff --git a/src/main/java/com/yaphet/chapa/utility/Util.java b/src/main/java/com/yaphet/chapa/utility/Util.java index c972235..b70c9ae 100644 --- a/src/main/java/com/yaphet/chapa/utility/Util.java +++ b/src/main/java/com/yaphet/chapa/utility/Util.java @@ -1,131 +1,43 @@ package com.yaphet.chapa.utility; -import java.lang.reflect.Type; -import java.math.BigDecimal; -import java.time.Clock; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.yaphet.chapa.Chapa; + import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; import java.util.Map; -import java.util.UUID; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.reflect.TypeToken; -import com.yaphet.chapa.model.*; +import static com.yaphet.chapa.utility.StringUtils.isBlank; /** - * The Util class serves as a helper class for the main {@link com.yaphet.chapa.Chapa} class. + * The Util class serves as a helper class for the main {@link Chapa} class. */ public class Util { - private static final Clock CLOCK = Clock.systemDefaultZone(); - private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yy-HH-mm-ss"); - private final static Gson JSON_MAPPER = new Gson(); - - /** - * @param jsonData A json string to be mapped to a {@link PostData} object. - * @return A {@link PostData} object which contains post fields of the - * provided JSON data. - */ - public static PostData jsonToPostData(String jsonData) { - if (!notNullAndEmpty(jsonData)) { - throw new IllegalArgumentException("Can't map null or empty json to PostData object"); - } - - Map newMap = jsonToMap(jsonData); - JsonObject jsonObject = JSON_MAPPER.fromJson(jsonData, JsonObject.class); - Type bankListType = new TypeToken>() {}.getType(); - Map customizations = JSON_MAPPER.fromJson(jsonObject.get("customizations"), bankListType); - Customization customization = new Customization() - .setTitle(customizations.get("customization[title]")) - .setTitle(customizations.get("customization[description]")) - .setTitle(customizations.get("customization[logo]")) - ; - return new PostData() - .setAmount(new BigDecimal(String.valueOf(newMap.get("amount")))) - .setCurrency(newMap.get("currency")) - .setEmail(newMap.get("email")) - .setFirstName(newMap.get("first_name")) - .setLastName(newMap.get("last_name")) - .setTxRef(newMap.get("tx_ref")) - .setCallbackUrl(newMap.get("callback_url")) - .setCustomization(customization); - } - - /** - * @param jsonData A json string to be mapped to a {@link SubAccount} object. - * @return A {@link SubAccount} object which contains post fields of the - * provided JSON data. - */ - public static SubAccount jsonToSubAccount(String jsonData) { - if (!notNullAndEmpty(jsonData)) { - throw new IllegalArgumentException("Can't map null or empty json to SubAccount object"); - } - - return JSON_MAPPER.fromJson(jsonData, SubAccount.class); - } + private final static Gson JSON_MAPPER = new GsonBuilder() + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer()) + .create(); /** * @param jsonData a json string to be mapped to a Map object. * @return A Map object which contains post fields of the provided * JSON data. */ - public static Map jsonToMap(String jsonData) { + public static Map jsonToMap(String jsonData) { return JSON_MAPPER.fromJson(jsonData, Map.class); } - /** - * @param jsonData a json string to be mapped to an {@link InitializeResponseData} object. - * @return An {@link InitializeResponseData} object which contains response fields of the provided - * JSON data. - */ - public static InitializeResponseData jsonToInitializeResponseData(String jsonData) { - return JSON_MAPPER.fromJson(jsonData, InitializeResponseData.class); - } - - /** - * @param jsonData a json string to be mapped to a {@link VerifyResponseData} object. - * @return A {@link VerifyResponseData} object which contains response fields of the provided - * JSON data. - */ - public static VerifyResponseData jsonToVerifyResponseData(String jsonData) { - return JSON_MAPPER.fromJson(jsonData, VerifyResponseData.class); - } - - /** - * @param jsonData a json string to be mapped to a {@link SubAccountResponseData} object. - * @return A {@link SubAccountResponseData} object which contains response fields of the provided - * JSON data. - */ - public static SubAccountResponseData jsonToSubAccountResponseData(String jsonData) { - return JSON_MAPPER.fromJson(jsonData, SubAccountResponseData.class); - } - - /** - * @param jsonData a json string to be mapped to a list of {@link Bank} objects. - * @return A list of {@link Bank} objects each containing details of a bank. - */ - public static List extractBanks(String jsonData) { - JsonObject jsonObject = JSON_MAPPER.fromJson(jsonData, JsonObject.class); - Type bankListType = new TypeToken>() {}.getType(); - - return JSON_MAPPER.fromJson(jsonObject.get("data"), bankListType); - } - - /** - * @return A random string followed by the current date/time value (dd-MM-yy-HH-mm-ss). - */ - public static String generateToken() { - final LocalDateTime now = LocalDateTime.now(CLOCK); - return UUID.randomUUID().toString().substring(0, 8) + "_" + FORMATTER.format(now); + public static void putIfNotNull(Map fields, String key, String value) { + if (isBlank(value)) return; + fields.put(key, value); } - public static boolean notNullAndEmpty(String value) { - return value != null && !value.isEmpty(); - } - - public static boolean is2xxSuccessful(int statucCode) { - return (statucCode / 100) == 2; + public static boolean isAnyNull(Map fields, String... keys) { + if (keys == null) return true; + if (fields == null || fields.isEmpty()) return false; + for (String key : keys) { + if (!fields.containsKey(key) || fields.get(key) == null) return true; + } + return false; } } diff --git a/src/main/java/com/yaphet/chapa/utility/Validate.java b/src/main/java/com/yaphet/chapa/utility/Validate.java new file mode 100644 index 0000000..3e3193a --- /dev/null +++ b/src/main/java/com/yaphet/chapa/utility/Validate.java @@ -0,0 +1,27 @@ +package com.yaphet.chapa.utility; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.yaphet.chapa.utility.StringUtils.isNotBlank; + +public final class Validate { + + private Validate() { + throw new IllegalStateException(""); + } + + public static boolean isValidEmail(final String token) { + return isNotBlank(token) && validateRegex(token, "[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,5}"); + } + + public static boolean isValidPhoneNumber(final String token) { + return isNotBlank(token) && validateRegex(token, "^0[79]\\d{8}$"); + } + + private static boolean validateRegex(final String token, final String regex) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(token); + return matcher.matches(); + } +} diff --git a/src/test/java/com/yaphet/chapa/ChapaExample.java b/src/test/java/com/yaphet/chapa/ChapaExample.java new file mode 100644 index 0000000..c9415de --- /dev/null +++ b/src/test/java/com/yaphet/chapa/ChapaExample.java @@ -0,0 +1,68 @@ +package com.yaphet.chapa; + +import com.yaphet.chapa.client.ChapaClient; +import com.yaphet.chapa.client.ChapaClientApi; +import com.yaphet.chapa.client.provider.RetrierRetrofitClientProvider; +import com.yaphet.chapa.exception.ChapaException; +import com.yaphet.chapa.model.Customization; +import com.yaphet.chapa.model.PostData; +import com.yaphet.chapa.model.ResponseBanks; +import com.yaphet.chapa.model.SplitTypeEnum; +import com.yaphet.chapa.model.SubAccountDto; + +import java.util.UUID; + +public class ChapaExample { + + public static void main(String[] args) throws ChapaException { + ChapaClientApi chapaClientRetrofit = new RetrierRetrofitClientProvider.Builder() + .baseUrl("https://api.chapa.co/v1/") + .maxRetries(3) + .timeout(10000) + .build() + .create(); + + Chapa chapa = new Chapa.Builder() + .client(new ChapaClient(chapaClientRetrofit)) + .secretKey("CHASECK_TEST-....") + .build(); + + Customization customization = new Customization() + .setTitle("E-commerce") + .setDescription("It is time to pay") + .setLogo("https://mylogo.com/log.png"); + PostData postData = new PostData() + .setAmount("100") + .setCurrency("ETB") + .setFirstName("Abebe") + .setLastName("Bikila") + .setEmail("abebe@bikila.com") + .setTxRef(UUID.randomUUID().toString()) + .setCallbackUrl("https://chapa.co") + .setReturnUrl("https://chapa.co") + .setSubAccountId("testSubAccountId") + .setCustomization(customization); + + SubAccountDto subAccountDto = new SubAccountDto() + .setBusinessName("Abebe Suq") + .setAccountName("Abebe Bikila") + .setAccountNumber("0123456789") + .setBankCode("853d0598-9c01-41ab-ac99-48eab4da1513") + .setSplitType(SplitTypeEnum.PERCENTAGE) + .setSplitValue(0.2); + + // list of banks + ResponseBanks banks = chapa.getBanks(); + if ((banks == null || banks.getData() == null)) { + System.out.println("Bank response: " + banks); + } else { + banks.getData().forEach(System.out::println); + } + // create subaccount + System.out.println("Create SubAccount response: " + chapa.createSubAccount(subAccountDto)); + // initialize payment + System.out.println("Initialize response: " + chapa.initialize(postData)); + // verify payment + System.out.println("Verify response: " + chapa.verify(postData.getTxRef())); + } +} \ No newline at end of file diff --git a/src/test/java/com/yaphet/chapa/ChapaTest.java b/src/test/java/com/yaphet/chapa/ChapaTest.java deleted file mode 100644 index 7130493..0000000 --- a/src/test/java/com/yaphet/chapa/ChapaTest.java +++ /dev/null @@ -1,275 +0,0 @@ -package com.yaphet.chapa; - -import com.google.gson.Gson; -import com.yaphet.chapa.client.ChapaClientImpl; -import com.yaphet.chapa.model.*; -import com.yaphet.chapa.utility.Util; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.skyscreamer.jsonassert.JSONAssert; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - - -@ExtendWith(MockitoExtension.class) -class ChapaTest { - - private final Gson gson = new Gson(); - private ChapaClientImpl chapaClient; - private Chapa underTest; - private PostData postData; - private String postDataString; - private SubAccount subAccount; - private String subAccountString; - - @BeforeEach - void setUp() { - chapaClient = mock(ChapaClientImpl.class); - underTest = new Chapa(chapaClient, "secrete-key"); - Customization customization = new Customization() - .setTitle("E-commerce") - .setDescription("It is time to pay") - .setLogo("https://mylogo.com/log.png"); - postData = new PostData() - .setAmount(new BigDecimal("100")) - .setCurrency("ETB") - .setFirstName("Abebe") - .setLastName("Bikila") - .setEmail("abebe@bikila.com") - .setTxRef(Util.generateToken()) - .setCallbackUrl("https://chapa.co") - .setReturnUrl("https://chapa.co") - .setSubAccountId("testSubAccountId") - .setCustomization(customization); - postDataString = " { " + - "'amount': '100', " + - "'currency': 'ETB'," + - "'email': 'abebe@bikila.com'," + - "'first_name': 'Abebe'," + - "'last_name': 'Bikila'," + - "'tx_ref': 'tx-myecommerce12345'," + - "'callback_url': 'https://chapa.co'," + - "'return_url': 'https://chapa.co'," + - "'subaccount[id]': 'testSubAccountId'," + - "'customizations':{'customization[title]':'E-commerce','customization[description]':'It is time to pay','customization[logo]':'https://mylogo.com/log.png'}" + - " }"; - subAccount = new SubAccount() - .setBusinessName("Abebe Suq") - .setAccountName("Abebe Bikila") - .setAccountNumber("0123456789") - .setBankCode("001") - .setSplitType(SplitType.PERCENTAGE) - .setSplitValue(0.2); - subAccountString = "{'business_name':'Abebe Suq','bank_code':'001','account_name':'Abebe Bikila','account_number':'0123456789','split_type':'PERCENTAGE','split_value':0.2}"; - } - - // This is not really a unit test ): - @Test - public void shouldInitializeTransaction_asString() throws Throwable { - // given - String expectedResponse = "{\"data\":{\"checkout_url\":\"https://checkout.chapa.co/checkout/payment/somestring\"},\"message\":\"Hosted Link\",\"status\":\"success\"}"; - - // when - when(chapaClient.post(anyString(), anyMap(), anyString())).thenReturn(expectedResponse); - when(chapaClient.getStatusCode()).thenReturn(200); - String actualResponse = underTest.initialize(postData).asString(); - - // then - verify(chapaClient).post(anyString(), anyMap(), anyString()); - JSONAssert.assertEquals(expectedResponse, actualResponse, true); - } - - @Test - public void shouldInitializeTransaction_asResponseData() throws Throwable { - // given - InitializeResponseData expectedResponseData = new InitializeResponseData() - .setMessage("Transaction reference has been used before") - .setStatus("failed") - .setStatusCode(200) - .setData(new InitializeResponseData.Data().setCheckOutUrl("https://checkout.chapa.co/checkout/payment/somestring")); - - String expectedResponse = "{\"data\":{\"checkout_url\":\"https://checkout.chapa.co/checkout/payment/somestring\"},\"message\":\"Transaction reference has been used before\",\"status\":\"failed\"}"; - - // when - when(chapaClient.post(anyString(), anyMap(), anyString())).thenReturn(expectedResponse); - when(chapaClient.getStatusCode()).thenReturn(200); - InitializeResponseData actualResponseData = underTest.initialize(postData); - - // then - verify(chapaClient).post(anyString(), anyMap(), anyString()); - JSONAssert.assertEquals(gson.toJson(expectedResponseData), gson.toJson(actualResponseData), false); - } - - @Test - public void shouldInitializeTransaction_With_Json_Input() throws Throwable { - // given - String expectedResponse = "{\"data\":{\"checkout_url\":\"https://checkout.chapa.co/checkout/payment/somestring\"},\"message\":\"Hosted Link\",\"status\":\"success\"}"; - - // when - when(chapaClient.post(anyString(), anyString(), anyString())).thenReturn(expectedResponse); - when(chapaClient.getStatusCode()).thenReturn(200); - String actualResponse = underTest.initialize(postDataString).asString(); - - // then - verify(chapaClient).post(anyString(), anyString(), anyString()); - JSONAssert.assertEquals(expectedResponse, actualResponse, true); - } - - @Test - public void shouldVerifyTransaction_asString() throws Throwable { - // given - String expectedResponse = "{\"data\":null,\"message\":\"Payment not paid yet\",\"status\":\"null\"}"; - - // when - when(chapaClient.get(anyString(), anyString())).thenReturn(expectedResponse); - when(chapaClient.getStatusCode()).thenReturn(200); - String actualResponse = underTest.verify("test-transaction").asString(); - - // then - verify(chapaClient).get(anyString(), anyString()); - JSONAssert.assertEquals(expectedResponse, actualResponse, true); - } - - @Test - public void shouldVerifyTransaction_asResponseData() throws Throwable { - // given - VerifyResponseData expectedResponseData = new VerifyResponseData() - .setMessage("Payment not paid yet") - .setStatus("null") - .setStatusCode(200) - .setData(null); - String expectedResponse = "{\"data\":null,\"message\":\"Payment not paid yet\",\"status\":\"null\"}"; - - // when - when(chapaClient.get(anyString(), anyString())).thenReturn(expectedResponse); - when(chapaClient.getStatusCode()).thenReturn(200); - VerifyResponseData actualResponseData = underTest.verify("test-transaction"); - - // then - verify(chapaClient).get(anyString(), anyString()); - JSONAssert.assertEquals(gson.toJson(expectedResponseData), gson.toJson(actualResponseData), false); - } - - - @Test - public void shouldGetListOfBanks() throws Throwable { - // given - String expectedResponse = "{\"data\":[{\"updated_at\":\"2022-07-04T21:34:03.000000Z\",\"name\":\"Awash Bank\",\"created_at\":\"2022-03-17T04:21:30.000000Z\",\"id\":\"80a510ea-7497-4499-8b49-ac13a3ab7d07\",\"country_id\":1}],\"message\":\"Banks retrieved\"}"; - List expectedBanks = new ArrayList<>(); - expectedBanks.add(new Bank() - .setId("80a510ea-7497-4499-8b49-ac13a3ab7d07") - .setName("Awash Bank") - .setCountryId(1) - .setCreatedAt("2022-03-17T04:21:30.000000Z") - .setUpdatedAt("2022-07-04T21:34:03.000000Z")); - // when - when(chapaClient.get(anyString(), anyString())).thenReturn(expectedResponse); - when(chapaClient.getStatusCode()).thenReturn(200); - List actualBanks = underTest.banks(); - - // then - verify(chapaClient).get(anyString(), anyString()); - JSONAssert.assertEquals(gson.toJson(expectedBanks), gson.toJson(actualBanks), true); - } - - @Test - public void shouldCreateSubAccount_asString() throws Throwable { - // given - String expectedResponse = "{\"data\":null,\"message\":\"The Bank Code is incorrect please check if it does exist with our getbanks endpoint.\",\"status\":\"failed\"}"; - - // when - when(chapaClient.post(anyString(), anyMap(), anyString())).thenReturn(expectedResponse); - when(chapaClient.getStatusCode()).thenReturn(200); - String actualResponse = underTest.createSubAccount(subAccount).asString(); - - // then - verify(chapaClient).post(anyString(), anyMap(), anyString()); - JSONAssert.assertEquals(expectedResponse, actualResponse, true); - } - - @Test - public void shouldCreateSubAccount_With_Json_Input() throws Throwable { - // given - String expectedResponse = "{\"data\":null,\"message\":\"The Bank Code is incorrect please check if it does exist with our getbanks endpoint.\",\"status\":\"failed\"}"; - - // when - when(chapaClient.post(anyString(), anyString(), anyString())).thenReturn(expectedResponse); - when(chapaClient.getStatusCode()).thenReturn(200); - String actualResponse = underTest.createSubAccount(subAccountString).asString(); - - // then - verify(chapaClient).post(anyString(), anyString(), anyString()); - JSONAssert.assertEquals(expectedResponse, actualResponse, true); - } - - @Test - public void shouldCreateSubAccount_asResponseData() throws Throwable { - // given - SubAccountResponseData expectedResponseData = new SubAccountResponseData() - .setMessage("The Bank Code is incorrect please check if it does exist with our getbanks endpoint.") - .setStatus("failed") - .setStatusCode(200) - .setData(null); - String expectedResponse = "{\"data\":null,\"message\":\"The Bank Code is incorrect please check if it does exist with our getbanks endpoint.\",\"status\":\"failed\"}"; - - // when - when(chapaClient.post(anyString(), anyMap(), anyString())).thenReturn(expectedResponse); - when(chapaClient.getStatusCode()).thenReturn(200); - SubAccountResponseData actualResponse = underTest.createSubAccount(subAccount); - - // then - verify(chapaClient).post(anyString(), anyMap(), anyString()); - JSONAssert.assertEquals(gson.toJson(expectedResponseData), gson.toJson(actualResponse), false); - } - - - // This should not run in the pipeline - @Test - @Disabled - public void testDefault() throws Throwable { - // given - Customization customization = new Customization() - .setTitle("E-commerce") - .setDescription("It is time to pay") - .setLogo("https://mylogo.com/log.png"); - PostData postData = new PostData() - .setAmount(new BigDecimal("100")) - .setCurrency("ETB") - .setFirstName("Abebe") - .setLastName("Bikila") - .setEmail("abebe@bikila") - .setTxRef(Util.generateToken()) - .setCallbackUrl("https://chapa.co") - .setReturnUrl("https://chapa.co") - .setSubAccountId("testSubAccountId") - .setCustomization(customization); - subAccount = new SubAccount() - .setBusinessName("Abebe Suq") - .setAccountName("Abebe Bikila") - .setAccountNumber("0123456789") - .setBankCode("96e41186-29ba-4e30-b013-2ca36d7e7025") - .setSplitType(SplitType.PERCENTAGE) - .setSplitValue(0.2); - - String formData = gson.toJson(postData); - System.out.println(formData); - - Chapa chapa = new Chapa(""); - List banks = chapa.banks(); - banks.forEach(bank -> System.out.println("Bank name: " + bank.getName() + " Bank Code: " + bank.getId())); - System.out.println("Create SubAccount response: " + chapa.createSubAccount(subAccount).asString()); - System.out.println("Initialize response with PostData: " + chapa.initialize(postData).asString()); - System.out.println("Initialize response with JsonData: " + chapa.initialize(formData).asString()); - System.out.println("Verify response: " + chapa.verify(postData.getTxRef()).asString()); - } - -} \ No newline at end of file diff --git a/src/test/java/com/yaphet/chapa/UtilTest.java b/src/test/java/com/yaphet/chapa/UtilTest.java deleted file mode 100644 index 3a00cd9..0000000 --- a/src/test/java/com/yaphet/chapa/UtilTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.yaphet.chapa; - -import com.yaphet.chapa.model.Customization; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; - -import java.util.HashMap; -import java.util.Map; - -@Disabled -class UtilTest { - private final Map customizations = new HashMap<>(); - private final Customization customization = new Customization(); - - @BeforeEach - public void setUp() { - customization.setTitle("E-commerce"); - customization.setDescription("It is time to pay"); - customization.setLogo("https://mylogo.com/log.png"); - } -} \ No newline at end of file From 06984f45cfc114fe667358adc3a1565bfcfa5f51 Mon Sep 17 00:00:00 2001 From: Amanuel Date: Sun, 26 Jan 2025 20:11:53 +0100 Subject: [PATCH 2/3] Chapa client can now retry calls - Refactor to be able to better configure api client - Api ClientProvider is able to log calls with the flag debug=true - Api ClientProvider can accept custom OkHttpClient built by the user (provider.setClient(clientHere)) - Introduce well test retrofit2 as a client provider --- .../provider/BaseRetrofitClientProvider.java | 5 +- .../RetrierRetrofitClientProvider.java | 27 ++- .../com/yaphet/chapa/ChapaClientTest.java | 206 ++++++++++++++++++ .../java/com/yaphet/chapa/ChapaExample.java | 1 + src/test/java/com/yaphet/chapa/ChapaTest.java | 179 +++++++++++++++ .../chapa/DateTimeDeserializerTest.java | 42 ++++ .../com/yaphet/chapa/model/PostDataTest.java | 39 ++++ .../yaphet/chapa/utility/ValidateTest.java | 54 +++++ 8 files changed, 544 insertions(+), 9 deletions(-) create mode 100644 src/test/java/com/yaphet/chapa/ChapaClientTest.java create mode 100644 src/test/java/com/yaphet/chapa/ChapaTest.java create mode 100644 src/test/java/com/yaphet/chapa/DateTimeDeserializerTest.java create mode 100644 src/test/java/com/yaphet/chapa/model/PostDataTest.java create mode 100644 src/test/java/com/yaphet/chapa/utility/ValidateTest.java diff --git a/src/main/java/com/yaphet/chapa/client/provider/BaseRetrofitClientProvider.java b/src/main/java/com/yaphet/chapa/client/provider/BaseRetrofitClientProvider.java index b7fc69f..bcbd761 100644 --- a/src/main/java/com/yaphet/chapa/client/provider/BaseRetrofitClientProvider.java +++ b/src/main/java/com/yaphet/chapa/client/provider/BaseRetrofitClientProvider.java @@ -37,10 +37,9 @@ public RetrofitClientProvider setDebug(boolean debug) { @Override public RetrofitClientProvider setClient(OkHttpClient client) { - if (client == null) { - throw new IllegalArgumentException("Client can't be null"); + if (client != null) { + this.httpClient = client; } - this.httpClient = client; return this; } diff --git a/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java b/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java index acf309b..e398ee1 100644 --- a/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java +++ b/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java @@ -5,6 +5,7 @@ import io.github.resilience4j.retrofit.RetryCallAdapter; import io.github.resilience4j.retry.Retry; import io.github.resilience4j.retry.RetryConfig; +import okhttp3.OkHttpClient; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; @@ -39,16 +40,15 @@ public class RetrierRetrofitClientProvider extends BaseRetrofitClientProvider { private static final int DEFAULT_RETRY_COUNT = 3; private static final double BACKOFF_MULTIPLIER = 2.0; - private final RetryConfig retryConfig; private final String baseUrl; + private final RetryConfig retryConfig; private RetrierRetrofitClientProvider(Builder builder) { super(builder.timeoutMillis); - this.retryConfig = createRetryConfig(builder.timeoutMillis, builder.maxRetries); - if (StringUtils.isBlank(builder.baseUrl)) { - throw new IllegalArgumentException("Api baseUrl can't be null"); - } + super.setClient(builder.client); + super.setDebug(builder.debug); this.baseUrl = builder.baseUrl; + this.retryConfig = createRetryConfig(builder.timeoutMillis, builder.maxRetries); } private static RetryConfig createRetryConfig(long initialInterval, int maxAttempts) { @@ -71,9 +71,11 @@ public Retrofit provideRetrofitBuilder() { } public static class Builder { + private String baseUrl = ""; private long timeoutMillis = DEFAULT_TIMEOUT; private int maxRetries = DEFAULT_RETRY_COUNT; - private String baseUrl = ""; + private boolean debug; + private OkHttpClient client; public Builder timeout(long timeoutMillis) { this.timeoutMillis = timeoutMillis; @@ -86,10 +88,23 @@ public Builder maxRetries(int maxRetries) { } public Builder baseUrl(String baseUrl) { + if (StringUtils.isBlank(baseUrl)) { + throw new IllegalArgumentException("Api baseUrl can't be null"); + } this.baseUrl = baseUrl; return this; } + public Builder debug(boolean debug) { + this.debug = debug; + return this; + } + + public Builder client(OkHttpClient client) { + this.client = client; + return this; + } + public RetrierRetrofitClientProvider build() { return new RetrierRetrofitClientProvider(this); } diff --git a/src/test/java/com/yaphet/chapa/ChapaClientTest.java b/src/test/java/com/yaphet/chapa/ChapaClientTest.java new file mode 100644 index 0000000..51268b2 --- /dev/null +++ b/src/test/java/com/yaphet/chapa/ChapaClientTest.java @@ -0,0 +1,206 @@ +package com.yaphet.chapa; + +import com.google.gson.Gson; +import com.yaphet.chapa.client.ChapaClient; +import com.yaphet.chapa.client.ChapaClientApi; +import com.yaphet.chapa.client.provider.DefaultRetrofitClientProvider; +import com.yaphet.chapa.exception.ChapaException; +import com.yaphet.chapa.model.InitializeResponse; +import com.yaphet.chapa.model.SubAccountResponse; +import com.yaphet.chapa.model.VerifyResponse; +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import retrofit2.Call; +import retrofit2.Response; + +import java.util.HashMap; + +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ChapaClientTest { + + private final String secretKey = "secretKey"; + + @Mock private Call call; + @Mock private ChapaClientApi chapaClientApi; + @InjectMocks private ChapaClient client; + + @BeforeEach + public void init() { + MockitoAnnotations.initMocks(this); + } + + @Test + void initialize_failOnNetworkCall() { + Assertions.assertThrows(ChapaException.class, () -> new ChapaClient(new DefaultRetrofitClientProvider.Builder().baseUrl("http://chapa.example.com/v1/").build().create()).initialize(secretKey, new HashMap<>())); + } + + @Test + void initialize_failOnFailedResponse() throws Exception { + //given + when(chapaClientApi.initialize(anyString(), anyMap())).thenReturn(call); + when(call.execute()).thenReturn(Response.error(500, ResponseBody.create(MediaType.parse("application/json"), ""))); + + //assert + Assertions.assertThrows(ChapaException.class, () -> client.initialize(secretKey, new HashMap<>())); + } + + @Test + void initialize_Success() throws Exception { + //given + when(chapaClientApi.initialize(anyString(), anyMap())).thenReturn(call); + when(call.execute()).thenReturn(Response.success(getInitializeResponseData())); + + //assert + InitializeResponse response = client.initialize(secretKey, new HashMap<>()); + verify(chapaClientApi).initialize(anyString(), anyMap()); + Assertions.assertNotNull(response); + Assertions.assertEquals(response.getMessage(), "Hosted Link"); + Assertions.assertNotNull(response.getData()); + } + + @Test + void initialize2_Success() throws Exception { + //given + when(chapaClientApi.initialize(anyString(), anyMap())).thenReturn(call); + when(call.execute()).thenReturn(Response.success(getInitializeResponseData())); + + //assert + InitializeResponse response = client.initialize(secretKey, "{\"ignore\": \"ignore\"}"); + verify(chapaClientApi).initialize(anyString(), anyMap()); + Assertions.assertNotNull(response); + Assertions.assertEquals(response.getMessage(), "Hosted Link"); + Assertions.assertNotNull(response.getData()); + } + + @Test + void verify_failOnFailedResponse() throws Exception { + //given + when(chapaClientApi.verify(anyString(), anyString())).thenReturn(call); + when(call.execute()).thenReturn(Response.error(500, ResponseBody.create(MediaType.parse("application/json"), ""))); + + //assert + Assertions.assertThrows(ChapaException.class, () -> client.verify(secretKey, "")); + } + + @Test + void verify_Success() throws Exception { + //given + when(chapaClientApi.verify(anyString(), anyString())).thenReturn(call); + when(call.execute()).thenReturn(Response.success(getVerifyResponseData())); + + //assert + VerifyResponse response = client.verify(secretKey, "ignore"); + verify(chapaClientApi).verify(anyString(), anyString()); + + Assertions.assertNotNull(response); + Assertions.assertEquals(response.getMessage(), "Payment details"); + Assertions.assertNotNull(response.getData()); + } + + @Test + void getBanks_failOnFailedResponse() throws Exception { + //given + when(chapaClientApi.banks(anyString())).thenReturn(call); + when(call.execute()).thenReturn(Response.error(500, ResponseBody.create(MediaType.parse("application/json"), ""))); + + //assert + Assertions.assertThrows(ChapaException.class, () -> client.getBanks(secretKey)); + } + + @Test + void createSubAccount_failOnFailedResponse() throws Exception { + //given + when(chapaClientApi.createSubAccount(anyString(), anyMap())).thenReturn(call); + when(call.execute()).thenReturn(Response.error(500, ResponseBody.create(MediaType.parse("application/json"), ""))); + + //assert + Assertions.assertThrows(ChapaException.class, () -> client.createSubAccount(secretKey, new HashMap<>())); + } + + @Test + void createSubAccount_Success() throws Exception { + //given + when(chapaClientApi.createSubAccount(anyString(), anyMap())).thenReturn(call); + when(call.execute()).thenReturn(Response.success(getSubAccountResponseData())); + + // verify + SubAccountResponse response = client.createSubAccount(secretKey, new HashMap<>()); + verify(chapaClientApi).createSubAccount(anyString(), anyMap()); + + Assertions.assertNotNull(response); + Assertions.assertEquals(response.getMessage(), "Subaccount created succesfully"); + } + + @Test + void createSubAccount_Success2() throws Exception { + //given + when(chapaClientApi.createSubAccount(anyString(), anyMap())).thenReturn(call); + when(call.execute()).thenReturn(Response.success(getSubAccountResponseData())); + + // verify + SubAccountResponse response = client.createSubAccount(secretKey, "{\"ignore\": \"ignore\"}"); + verify(chapaClientApi).createSubAccount(anyString(), anyMap()); + + Assertions.assertNotNull(response); + Assertions.assertEquals(response.getMessage(), "Subaccount created succesfully"); + } + + + private static SubAccountResponse getSubAccountResponseData() { + return new Gson().fromJson("{\n" + + " \"message\": \"Subaccount created succesfully\",\n" + + " \"status\": \"success\",\n" + + " \"data\": {\n" + + " \"subaccounts[id]\": \"837b4e5e-57c8-4e39-b2df-66e7886b8bdb\"\n" + + " }\n" + + "}", SubAccountResponse.class); + } + + + private static VerifyResponse getVerifyResponseData() { + return new Gson().fromJson("{\n" + + " \"message\": \"Payment details\",\n" + + " \"status\": \"success\",\n" + + " \"data\": {\n" + + " \"first_name\": \"Bilen\",\n" + + " \"last_name\": \"Gizachew\",\n" + + " \"email\": \"abebech_bekele@gmail.com\",\n" + + " \"currency\": \"ETB\",\n" + + " \"amount\": 100,\n" + + " \"charge\": 3.5,\n" + + " \"mode\": \"test\",\n" + + " \"method\": \"test\",\n" + + " \"type\": \"API\",\n" + + " \"status\": \"success\",\n" + + " \"reference\": \"6jnheVKQEmy\",\n" + + " \"tx_ref\": \"chewatatest-6669\",\n" + + " \"customization\": {\n" + + " \"title\": \"Payment for my favourite merchant\",\n" + + " \"description\": \"I love online payments\",\n" + + " \"logo\": null\n" + + " },\n" + + " \"meta\": null,\n" + + " \"created_at\": \"2023-02-02T07:05:23.000000Z\",\n" + + " \"updated_at\": \"2023-02-02T07:05:23.000000Z\"\n" + + " }\n" + + "}", VerifyResponse.class); + } + + + private static InitializeResponse getInitializeResponseData() { + return new Gson().fromJson("{\"data\":{\"checkout_url\":\"https://checkout.chapa.co/checkout/payment/somestring\"},\"message\":\"Hosted Link\",\"status\":\"success\"}", InitializeResponse.class); + } +} \ No newline at end of file diff --git a/src/test/java/com/yaphet/chapa/ChapaExample.java b/src/test/java/com/yaphet/chapa/ChapaExample.java index c9415de..14ebdfe 100644 --- a/src/test/java/com/yaphet/chapa/ChapaExample.java +++ b/src/test/java/com/yaphet/chapa/ChapaExample.java @@ -19,6 +19,7 @@ public static void main(String[] args) throws ChapaException { .baseUrl("https://api.chapa.co/v1/") .maxRetries(3) .timeout(10000) + .debug(true) .build() .create(); diff --git a/src/test/java/com/yaphet/chapa/ChapaTest.java b/src/test/java/com/yaphet/chapa/ChapaTest.java new file mode 100644 index 0000000..6ed8817 --- /dev/null +++ b/src/test/java/com/yaphet/chapa/ChapaTest.java @@ -0,0 +1,179 @@ +package com.yaphet.chapa; + +import com.yaphet.chapa.client.ChapaClient; +import com.yaphet.chapa.client.ChapaClientApi; +import com.yaphet.chapa.client.IChapaClient; +import com.yaphet.chapa.client.provider.RetrierRetrofitClientProvider; +import com.yaphet.chapa.exception.ChapaException; +import com.yaphet.chapa.model.Bank; +import com.yaphet.chapa.model.Customization; +import com.yaphet.chapa.model.InitializeResponse; +import com.yaphet.chapa.model.PostData; +import com.yaphet.chapa.model.ResponseBanks; +import com.yaphet.chapa.model.SplitTypeEnum; +import com.yaphet.chapa.model.SubAccountDto; +import com.yaphet.chapa.model.SubAccountResponse; +import com.yaphet.chapa.model.VerifyResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@ExtendWith(MockitoExtension.class) +class ChapaTest { + + private PostData postData; + private String postDataString; + private SubAccountDto subAccountDto; + + @Mock private IChapaClient chapaClient; + private Chapa chapa; + + @BeforeEach + void setUp() { + chapa = new Chapa.Builder() + .client(chapaClient) + .secretKey("CHASECK_TEST-....") + .build(); + + Customization customization = new Customization() + .setTitle("E-commerce") + .setDescription("It is time to pay") + .setLogo("https://mylogo.com/log.png"); + postData = new PostData() + .setAmount("100") + .setCurrency("ETB") + .setFirstName("Abebe") + .setLastName("Bikila") + .setEmail("abebe@bikila.com") + .setTxRef(UUID.randomUUID().toString()) + .setCallbackUrl("https://chapa.co") + .setReturnUrl("https://chapa.co") + .setSubAccountId("testSubAccountId") + .setCustomization(customization); + postDataString = " { " + + "'amount': '100', " + + "'currency': 'ETB'," + + "'email': 'abebe@bikila.com'," + + "'first_name': 'Abebe'," + + "'last_name': 'Bikila'," + + "'tx_ref': 'tx-myecommerce12345'," + + "'callback_url': 'https://chapa.co'," + + "'return_url': 'https://chapa.co'," + + "'subaccount[id]': 'testSubAccountId'," + + "'customizations':{'customization[title]':'E-commerce','customization[description]':'It is time to pay','customization[logo]':'https://mylogo.com/log.png'}" + + " }"; + subAccountDto = new SubAccountDto() + .setBusinessName("Abebe Suq") + .setAccountName("Abebe Bikila") + .setAccountNumber("0123456789") + .setBankCode("001") + .setSplitType(SplitTypeEnum.PERCENTAGE) + .setSplitValue(0.2); + } + + @Test + public void initializeTransaction_Fail() { + // verify + Assertions.assertThrows(ChapaException.class, () -> chapa.initialize(new PostData())); + } + + @Test + public void initializeTransaction_success() throws ChapaException { + // when + InitializeResponse.Data data = new InitializeResponse.Data(); + data.setCheckOutUrl(""); + InitializeResponse response = new InitializeResponse("","","", 200, data); + when(chapaClient.initialize(anyString(), anyMap())).thenReturn(response); + + // verify + InitializeResponse responseData = chapa.initialize(postData); + + assertNotNull(responseData); + assertNotNull(responseData.getData().getCheckOutUrl()); + } + + @Test + public void initializeTransaction_success2() throws ChapaException { + // when + InitializeResponse.Data data = new InitializeResponse.Data(); + data.setCheckOutUrl(""); + InitializeResponse response = new InitializeResponse("","","", 200, data); + when(chapaClient.initialize(anyString(), anyString())).thenReturn(response); + + // verify + InitializeResponse responseData = chapa.initialize(postDataString); + + assertNotNull(responseData); + assertNotNull(responseData.getData().getCheckOutUrl()); + } + + + @Test + public void getBank_success() throws ChapaException { + // when + when(chapaClient.getBanks(anyString())).thenReturn(new ResponseBanks().setData(Collections.singletonList(new Bank()))); + + // verify + ResponseBanks responseData = chapa.getBanks(); + + assertNotNull(responseData); + assertFalse(responseData.getData().isEmpty()); + } + + @Test + public void verifyTransaction_fail() { + Assertions.assertThrows(ChapaException.class, () -> chapa.verify("")); + } + + @Test + public void verifyTransaction_success() throws ChapaException { + // given + VerifyResponse expectedResponseData = new VerifyResponse() + .setMessage("Payment not paid yet") + .setStatus("null") + .setStatusCode(200) + .setData(null); + + // when + when(chapaClient.verify(anyString(), anyString())).thenReturn(expectedResponseData); + VerifyResponse actualResponseData = chapa.verify("test-transaction"); + + // verify + verify(chapaClient).verify(anyString(), anyString()); + assertEquals(actualResponseData.getMessage(), "Payment not paid yet"); + assertEquals(actualResponseData.getStatusCode(), 200); + } + + @Test + public void createSubAccount_success() throws Throwable { + // given + SubAccountResponse expectedResponseData = new SubAccountResponse() + .setMessage("The Bank Code is incorrect please check if it does exist with our getbanks endpoint.") + .setStatus("failed") + .setStatusCode(200); + + // when + when(chapaClient.createSubAccount(anyString(), anyMap())).thenReturn(expectedResponseData); + SubAccountResponse actualResponse = chapa.createSubAccount(subAccountDto); + + // then + verify(chapaClient).createSubAccount(anyString(), anyMap()); + assertEquals(actualResponse.getMessage(), "The Bank Code is incorrect please check if it does exist with our getbanks endpoint."); + assertEquals(actualResponse.getStatusCode(), 200); + } +} \ No newline at end of file diff --git a/src/test/java/com/yaphet/chapa/DateTimeDeserializerTest.java b/src/test/java/com/yaphet/chapa/DateTimeDeserializerTest.java new file mode 100644 index 0000000..8a45753 --- /dev/null +++ b/src/test/java/com/yaphet/chapa/DateTimeDeserializerTest.java @@ -0,0 +1,42 @@ +package com.yaphet.chapa; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.yaphet.chapa.utility.LocalDateTimeDeserializer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +@ExtendWith(MockitoExtension.class) +class DateTimeDeserializerTest { + + @Test + void deserialize() { + //given + String json = "{\"created_at\":\"2023-02-02T07:05:23.000000Z\"}"; + + Gson gson = new GsonBuilder() + .registerTypeAdapter(LocalDateTime.class, new com.yaphet.chapa.utility.LocalDateTimeDeserializer()) + .create(); + PrimitiveLocalDateTime target = gson.fromJson(json, PrimitiveLocalDateTime.class); + + // verify + Assertions.assertNotNull(target); + Assertions.assertEquals(target.getCreatedAt(), LocalDateTime.parse("2023-02-02T07:05:23")); + } + + private static class PrimitiveLocalDateTime { + @SerializedName("created_at") + @JsonAdapter(LocalDateTimeDeserializer.class) + private LocalDateTime createdAt; + + public LocalDateTime getCreatedAt() { + return createdAt; + } + } +} diff --git a/src/test/java/com/yaphet/chapa/model/PostDataTest.java b/src/test/java/com/yaphet/chapa/model/PostDataTest.java new file mode 100644 index 0000000..3d62007 --- /dev/null +++ b/src/test/java/com/yaphet/chapa/model/PostDataTest.java @@ -0,0 +1,39 @@ +package com.yaphet.chapa.model; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class PostDataTest { + + @Test + void test_correct_amount_parsing() { + // given + PostData postData = new PostData(); + postData.setAmount("100.0001"); + + // when + Map postDataAsMap = postData.getAsMap(); + + // validate + assertEquals("100.0001", postDataAsMap.get("amount")); + } + + @Test + void dont_use_new_BigDecimal_from_Double_that_will_have_precision_issue() { + // given + PostData postData = new PostData(); + postData.setAmount(new BigDecimal(100.0001).toString()); + + // when + Map postDataAsMap = postData.getAsMap(); + + // validate + assertNotEquals("100.0001", postDataAsMap.get("amount")); + } + +} \ No newline at end of file diff --git a/src/test/java/com/yaphet/chapa/utility/ValidateTest.java b/src/test/java/com/yaphet/chapa/utility/ValidateTest.java new file mode 100644 index 0000000..dfbea11 --- /dev/null +++ b/src/test/java/com/yaphet/chapa/utility/ValidateTest.java @@ -0,0 +1,54 @@ +package com.yaphet.chapa.utility; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ValidateTest { + + @Test + void test_valid_email() { + // given + String email = "chapa@test.com"; + + // then + boolean result = Validate.isValidEmail(email); + + // expect + assertTrue(result); + } + + @Test + void test_invalid_email() { + // given + String email = "chapa@test.invalid"; + + // then + boolean result = Validate.isValidEmail(email); + + // expect + assertFalse(result); + } + + @Test + void isInValidPhoneNumber() { + // given + String phoneNumber = "0910121212111"; + + // then + boolean result = Validate.isValidPhoneNumber(phoneNumber); + + // expect + assertFalse(result); + } + + @Test + void isValidPhoneNumber() { + String phoneNumber = "0910121212"; + + boolean isValidPhone = Validate.isValidPhoneNumber(phoneNumber); + + assertTrue(isValidPhone); + } +} \ No newline at end of file From ac531708119c0e5af52e89800112eb5e7802d68d Mon Sep 17 00:00:00 2001 From: Amanuel Date: Mon, 27 Jan 2025 23:45:41 +0100 Subject: [PATCH 3/3] update README.md --- README.md | 191 +++++++++++------- src/main/java/com/yaphet/chapa/Chapa.java | 18 ++ .../RetrierRetrofitClientProvider.java | 4 +- 3 files changed, 136 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index b5385c1..5c9bef8 100644 --- a/README.md +++ b/README.md @@ -43,40 +43,74 @@ Or add the below gradle dependency to your `build.gradle` file. ## Usage +> **Note** : This doc might not fully cover chapa-api. Please refer to the chapa developer doc for more. And contributions are welcome too. Thanks. + + Instantiate a `Chapa` class. ```java Chapa chapa = new Chapa("your-secrete-key"); ``` -Or if you want to use your own implementation of `ChapaClient` interface. +This will use the default implementation of `ChapaClient` interface. Internally, the SDK uses `DefaultRetrofitClientProvider` to make HTTP requests to Chapa API. This will not do retries on failed requests. If you want to implement your own client, you can do so by implementing the `ChapaClient` interface. + +As an alternative, a retriable client is available. This client will retry failed requests up to 3 times(or more if configured). ```java -Chapa chapa = new Chapa("your-secrete-key", new MyCustomChapaClient()); +ChapaClientApi chapaClientRetrofit = new RetrierRetrofitClientProvider.Builder() + .baseUrl("https://api.chapa.co/v1/") + .maxRetries(5) + .timeout(10000) + .debug(true) // log request and response + .build() + .create(); + +Chapa chapa = new Chapa.Builder() + .client(new ChapaClient(chapaClientRetrofit)) + .secretKey("your-secrete-key") + .build(); ``` -Note: `MyCustomChapaClient` must implement `ChapaClient` interface. +> **Note:** Retry will be done with an [ExponentialBackoff](https://en.wikipedia.org/wiki/Exponential_backoff) strategy (with multiplier 1.5). -To initialize a transaction, you can specify your information by either using our `PostData` class. +If this does not meet your requirements, you can implement the `IChapaClient` interface and create your own custom implementation to use your favorite HTTP client. -Note: Starting from version 1.1.0 you have to specify customization fields as a `Map` object. +```java +public class MyCustomChapaClient implements IChapaClient { + // Implement the methods from IChapaClient interface +} +``` +Then, you can use your custom implementation like this: +```java +Chapa chapa = new Chapa.Builder() + .client(new MyCustomChapaClient()) + .secretKey("your-secrete-key") + .build(); +``` + +## Methods +To initialize a transaction, you simply need to specify your information by either using our `PostData` class. ```java Customization customization = new Customization() - .setTitle("E-commerce") - .setDescription("It is time to pay") - .setLogo("https://mylogo.com/log.png"); + .setTitle("E-commerce") + .setDescription("It is time to pay") + .setLogo("https://mylogo.com/log.png"); PostData postData = new PostData() - .setAmount(new BigDecimal("100")) - .setCurrency("ETB") - .setFirstName("Abebe") - .setLastName("Bikila") - .setEmail("abebe@bikila") - .setTxRef(Util.generateToken()) - .setCallbackUrl("https://chapa.co") - .setReturnUrl("https://chapa.co") - .setSubAccountId("testSubAccountId") - .setCustomization(customization); + .setAmount("100") + .setCurrency("ETB") + .setFirstName("Abebe") + .setLastName("Bikila") + .setEmail("abebe@bikila.com") + .setTxRef(UUID.randomUUID().toString()) + .setCallbackUrl("https://chapa.co") + .setReturnUrl("https://chapa.co") + .setSubAccountId("testSubAccountId") + .setCustomization(customization); + + ... + + chapa.initialize(postData); ``` -Or, you can use a string JSON data. +Or, as a string JSON data. ```java -String formData = " { " + +String postDataString = " { " + "'amount': '100', " + "'currency': 'ETB'," + "'email': 'abebe@bikila.com'," + @@ -91,35 +125,34 @@ String formData = " { " + " 'customization[logo]':'https://mylogo.com/log.png'" + " }" + " }"; + + chapa.initialize(postDataString) ``` -Initialize payment +Intitialize payment ```java -InitializeResponseData responseData = chapa.initialize(postData); -// Get the response message -System.out.println(responseData.getMessage()); -// Get the checkout URL from the response JSON -System.out.println(responseData.getData().getCheckOutUrl()); -// Get the raw response JSON -System.out.println(responseData.getRawJson()); +InitializeResponseData responseData = chapa.initialize(postData) ``` Verify payment ```java -// Get the verification response data -VerifyResponseData verifyResponseData = chapa.verify("tx-myecommerce12345"); +VerifyResponseData actualResponseData = chapa.verify("tx-ref"); ``` -Get the list of banks +Get list of banks ```java List banks = chapa.getBanks(); ``` To create a subaccount, you can specify your information by either using our `Subaccount` class. ```java -SubAccount subAccount = new SubAccount() - .setBusinessName("Abebe Suq") - .setAccountName("Abebe Bikila") - .setAccountNumber("0123456789") - .setBankCode("96e41186-29ba-4e30-b013-2ca36d7e7025") - .setSplitType(SplitType.PERCENTAGE) - .setSplitValue(0.2); +SubAccount subAccount = new SubAccountDto() + .setBusinessName("Abebe Suq") + .setAccountName("Abebe Bikila") + .setAccountNumber("0123456789") + .setBankCode("001") + .setSplitType(SplitTypeEnum.PERCENTAGE) + .setSplitValue(0.2); + + ... + + SubAccountResponseData response = chapa.createSubAccount(subAccountDto); ``` Or, you can use a string JSON data. ```java @@ -131,68 +164,76 @@ String subAccount = " { " + "'split_type': 'percentage'," + "'split_value': '0.2'" + " }"; + + ... + + SubAccountResponseData actualResponse = chapa.createSubAccount(subAccount); ``` Create subaccount ```java -SubAccountResponseData subAccountResponseData = chapa.createSubAccount(subAccount); -// Get SubAccount id from the response JSOn -System.out.println(subAccountResponseData.getData().getSubAccountId()); + SubAccountResponseData actualResponse = chapa.createSubAccount(subAccountDto); ``` ## Example ```java -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import io.github.yaphet17.chapa.Chapa; -import io.github.yaphet17.chapa.PostData; -import io.github.yaphet17.chapa.SubAccount; -import io.github.yaphet17.chapa.SplitType; -import io.github.yaphet17.chapa.Bank; public class ChapaExample { - public static void main(String[] args) { - Chapa chapa = new Chapa("your-secrete-key"); - - Customization customization = new Customization() + public static void main(String[] args) throws ChapaException { + ChapaClientApi chapaClientRetrofit = new RetrierRetrofitClientProvider.Builder() + .baseUrl("https://api.chapa.co/v1/") + .maxRetries(3) + .timeout(10000) + .debug(true) + .build() + .create(); + + Chapa chapa = new Chapa.Builder() + .client(new ChapaClient(chapaClientRetrofit)) + .secretKey("CHASECK_TEST-....") + .build(); + + Customization customization = new Customization() .setTitle("E-commerce") .setDescription("It is time to pay") .setLogo("https://mylogo.com/log.png"); - - PostData postData = new PostData() - .setAmount(new BigDecimal("100")) + PostData postData = new PostData() + .setAmount("100") .setCurrency("ETB") .setFirstName("Abebe") .setLastName("Bikila") - .setEmail("abebe@bikila") - .setTxRef(Util.generateToken()) + .setEmail("abebe@bikila.com") + .setTxRef(UUID.randomUUID().toString()) .setCallbackUrl("https://chapa.co") .setReturnUrl("https://chapa.co") .setSubAccountId("testSubAccountId") .setCustomization(customization); - - SubAccount subAccount = new SubAccount() + + SubAccountDto subAccountDto = new SubAccountDto() .setBusinessName("Abebe Suq") .setAccountName("Abebe Bikila") .setAccountNumber("0123456789") - .setBankCode("96e41186-29ba-4e30-b013-2ca36d7e7025") - .setSplitType(SplitType.PERCENTAGE) + .setBankCode("853d0598-9c01-41ab-ac99-48eab4da1513") + .setSplitType(SplitTypeEnum.PERCENTAGE) .setSplitValue(0.2); - - InitializeResponseData responseData = chapa.initialize(postData); - VerifyResponseData verifyResponseData = chapa.verify("tx-myecommerce12345"); - SubAccountResponseData subAccountResponseData = chapa.createSubAccount(subAccount); - - } - } + // list of banks + ResponseBanks banks = chapa.getBanks(); + if ((banks == null || banks.getData() == null)) { + System.out.println("Bank response: " + banks); + } else { + banks.getData().forEach(System.out::println); + } + // create subaccount + System.out.println("Create SubAccount response: " + chapa.createSubAccount(subAccountDto)); + // initialize payment + System.out.println("Initialize response: " + chapa.initialize(postData)); + // verify payment + System.out.println("Verify response: " + chapa.verify(postData.getTxRef())); + } +} ``` ## Contribution -If you find any bugs or have any suggestions, please feel free to open an issue or pull request. +Please feel free to open an issue or pull request. ## License -This open-source library is licensed under the terms of the MIT License. - -Enjoy! +MIT \ No newline at end of file diff --git a/src/main/java/com/yaphet/chapa/Chapa.java b/src/main/java/com/yaphet/chapa/Chapa.java index 4bb6c18..7887810 100644 --- a/src/main/java/com/yaphet/chapa/Chapa.java +++ b/src/main/java/com/yaphet/chapa/Chapa.java @@ -1,6 +1,8 @@ package com.yaphet.chapa; +import com.yaphet.chapa.client.ChapaClient; import com.yaphet.chapa.client.IChapaClient; +import com.yaphet.chapa.client.provider.DefaultRetrofitClientProvider; import com.yaphet.chapa.exception.ChapaException; import com.yaphet.chapa.model.Bank; import com.yaphet.chapa.model.Customization; @@ -29,6 +31,22 @@ public class Chapa { private final IChapaClient chapaClient; + /** + * @deprecated use {@link Builder} to create an instance of {@link Chapa} + * + * Left for backward compatibility. + */ + @Deprecated + public Chapa(String secretKey) { + this.chapaClient = new ChapaClient( + new DefaultRetrofitClientProvider.Builder() + .baseUrl("https://api.chapa.co/v1/") + .timeout(10000) + .build() + .create()); + this.secreteKey = secretKey; + } + private Chapa(Builder builder) { this.chapaClient = builder.client; this.secreteKey = builder.secretKey; diff --git a/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java b/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java index e398ee1..7d52ddc 100644 --- a/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java +++ b/src/main/java/com/yaphet/chapa/client/provider/RetrierRetrofitClientProvider.java @@ -37,8 +37,8 @@ public class RetrierRetrofitClientProvider extends BaseRetrofitClientProvider { private static final long DEFAULT_TIMEOUT = 10000; - private static final int DEFAULT_RETRY_COUNT = 3; - private static final double BACKOFF_MULTIPLIER = 2.0; + private static final int DEFAULT_RETRY_COUNT = 2; + private static final double BACKOFF_MULTIPLIER = 1.5; private final String baseUrl; private final RetryConfig retryConfig;