diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..de5a7dd
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,34 @@
+name: Build and Test
+
+on:
+ push:
+ branches:
+ - 'main'
+
+ pull_request:
+ branches:
+ - 'main'
+ - 'feature/**'
+
+jobs:
+ build:
+ name: Build and Test
+ runs-on: ubuntu-latest
+ env:
+ apiKey: ${{ secrets.API_KEY }}
+ secretKey: ${{secrets.SECRET_KEY}}
+ contractCode: ${{secrets.CONTRACT_CODE}}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Java 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '11'
+ cache: maven
+
+ - name: Build and test
+ run: mvn --batch-mode clean verify
\ No newline at end of file
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..bf7ed69
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,42 @@
+name: Publish to Maven Central
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '11'
+ cache: maven
+
+ server-id: central
+ server-username: CENTRAL_USERNAME
+ server-password: CENTRAL_PASSWORD
+
+ - name: Import GPG key
+ uses: crazy-max/ghaction-import-gpg@v6
+ with:
+ gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
+ passphrase: ${{ secrets.GPG_PASSPHRASE }}
+
+ - name: Publish
+ run: mvn --batch-mode clean deploy -DskipTests
+ env:
+ CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
+ CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
+ GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }}
+ GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
+
diff --git a/pom.xml b/pom.xml
index 5b3593d..93f1269 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,6 +55,12 @@
1.18.30
provided
+
+ org.wiremock
+ wiremock
+ 3.13.1
+ test
+
com.squareup.okhttp3
okhttp
@@ -101,22 +107,6 @@
-
-
- org.apache.maven.plugins
- maven-gpg-plugin
- 3.1.0
-
-
- sign-artifacts
- verify
-
- sign
-
-
-
-
-
org.sonatype.central
central-publishing-maven-plugin
@@ -175,6 +165,50 @@
${maven.compiler.target}
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.1.0
+
+
+ ${gpg.keyname}
+ ${gpg.passphrase}
+
+
+
+
+ sign-artifacts
+ deploy
+
+ sign
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+
+
+ **/*MockTest.java
+
+
+
+
diff --git a/src/main/java/com/monnify/Monnify.java b/src/main/java/com/monnify/Monnify.java
index 482680e..ac3337f 100644
--- a/src/main/java/com/monnify/Monnify.java
+++ b/src/main/java/com/monnify/Monnify.java
@@ -69,6 +69,22 @@ public static String getBaseUrl() {
ensureInitialized();
return BASE_URL;
}
+ /**
+ * Allows tests to override the API base URL.
+ *
+ * This method is package-private so it is not exposed as part
+ * of the public SDK API.
+ */
+ public static void setBaseUrlForTesting(String baseUrl) {
+ if (!initialized) {
+ throw new MonnifyException(
+ "Monnify must be initialized before setting the test base URL."
+ );
+ }
+
+ BASE_URL = baseUrl;
+ }
+
private static void ensureInitialized() {
if (!initialized) {
diff --git a/src/main/java/com/monnify/services/auth/AuthService.java b/src/main/java/com/monnify/services/auth/AuthService.java
index 6ad18ec..37d91ea 100644
--- a/src/main/java/com/monnify/services/auth/AuthService.java
+++ b/src/main/java/com/monnify/services/auth/AuthService.java
@@ -73,4 +73,13 @@ public static synchronized String getToken() {
private static String encodeToBase64(String apiKey, String secretKey) {
return Base64.getEncoder().encodeToString(String.format("%s:%s", apiKey, secretKey).getBytes(StandardCharsets.UTF_8));
}
+ /**
+ * Resets cached authentication state.
+ *
+ * This method is intended for testing only.
+ */
+ public static void resetForTesting() {
+ authToken = null;
+ expiryTime = 0;
+ }
}
diff --git a/src/test/java/com/monnify/BaseMonnifyMockTest.java b/src/test/java/com/monnify/BaseMonnifyMockTest.java
new file mode 100644
index 0000000..1fc23d7
--- /dev/null
+++ b/src/test/java/com/monnify/BaseMonnifyMockTest.java
@@ -0,0 +1,72 @@
+package com.monnify;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.google.gson.Gson;
+import com.monnify.services.auth.AuthService;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+
+public abstract class BaseMonnifyMockTest {
+
+ protected static WireMockServer wireMockServer;
+
+ protected static final String API_KEY = "MK_TEST_API_KEY";
+ protected static final String SECRET_KEY = "test-secret-key";
+ protected static final String ACCESS_TOKEN = "mock-access-token";
+
+ private static volatile boolean initialized = false;
+
+ @BeforeAll
+ static synchronized void setUpWireMock() {
+
+ if (initialized) {
+ return;
+ }
+
+ wireMockServer = new WireMockServer(8089);
+ wireMockServer.start();
+
+ configureFor("localhost", 8089);
+
+ Monnify.initialize(API_KEY, SECRET_KEY);
+ Monnify.setBaseUrlForTesting("http://localhost:8089");
+
+ initialized = true;
+ }
+
+ @BeforeEach
+ void resetWireMock() {
+ reset();
+ AuthService.resetForTesting();
+ mockAuthentication();
+ }
+
+ protected void mockAuthentication() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v1/auth/login"))
+ .withHeader("Authorization", matching("Basic .*"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json"
+ )
+ .withBody(
+ "{"
+ + "\"requestSuccessful\": true,"
+ + "\"responseMessage\": \"success\","
+ + "\"responseCode\": \"0\","
+ + "\"responseBody\": {"
+ + "\"accessToken\": \"mock-access-token\","
+ + "\"expiresIn\": 3600"
+ + "}"
+ + "}"
+ )
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/AuthServiceMockTest.java b/src/test/java/com/monnify/mocktests/AuthServiceMockTest.java
new file mode 100644
index 0000000..b37df0c
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/AuthServiceMockTest.java
@@ -0,0 +1,110 @@
+package com.monnify.mocktests;
+
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.services.auth.AuthService;
+import org.junit.jupiter.api.Test;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+class AuthServiceMockTest extends BaseMonnifyMockTest {
+ @Test
+ void shouldReturnAccessTokenWhenApiReturnsSuccess() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v1/auth/login"))
+ .withHeader("Authorization", matching("Basic .*"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\": true,"
+ + "\"responseMessage\": \"success\","
+ + "\"responseCode\": \"0\","
+ + "\"responseBody\": {"
+ + "\"accessToken\": \"mock-access-token\","
+ + "\"expiresIn\": 3600"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ String token = AuthService.getToken();
+ assertNotNull(token);
+ assertEquals(ACCESS_TOKEN, token);
+ verify(1, postRequestedFor(urlPathEqualTo("/api/v1/auth/login"))
+ );
+ }
+ @Test
+ void shouldSendCorrectAuthorizationHeader() {
+ stubFor(
+ post(urlPathEqualTo("/api/v1/auth/login"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json"
+ )
+ .withBody(
+ "{"
+ + "\"requestSuccessful\": true,"
+ + "\"responseMessage\": \"success\","
+ + "\"responseCode\": \"0\","
+ + "\"responseBody\": {"
+ + "\"accessToken\": \"mock-access-token\","
+ + "\"expiresIn\": 3600"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+
+ AuthService.getToken();
+ String credentials = API_KEY + ":" + SECRET_KEY;
+
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ verify(postRequestedFor(urlPathEqualTo("/api/v1/auth/login"))
+ .withHeader("Authorization", equalTo("Basic " + encodedCredentials)));
+ }
+
+
+ @Test
+ void shouldUsePostMethodForAuthentication() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v1/auth/login"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json"
+ )
+ .withBody(
+ "{"
+ + "\"requestSuccessful\": true,"
+ + "\"responseMessage\": \"success\","
+ + "\"responseCode\": \"0\","
+ + "\"responseBody\": {"
+ + "\"accessToken\": \"mock-access-token\","
+ + "\"expiresIn\": 3600"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+
+ AuthService.getToken();
+ verify(1, postRequestedFor(urlPathEqualTo("/api/v1/auth/login")));
+ verify(0, getRequestedFor(urlPathEqualTo("/api/v1/auth/login")));
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/BankServiceMockTest.java b/src/test/java/com/monnify/mocktests/BankServiceMockTest.java
new file mode 100644
index 0000000..c334201
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/BankServiceMockTest.java
@@ -0,0 +1,175 @@
+package com.monnify.mocktests;
+
+
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.bank.BankResponse;
+import com.monnify.services.bank.BankService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class BankServiceMockTest extends BaseMonnifyMockTest {
+
+ private BankService bankService;
+
+ @BeforeEach
+ void setUp() {
+ bankService = new BankService();
+ }
+
+ @Test
+ void shouldReturnBanksWhenApiReturnsSuccess() {
+
+ stubFor(get(urlPathEqualTo("/api/v1/banks"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{" + "\"requestSuccessful\": true,"
+ + "\"responseMessage\": \"success\","
+ + "\"responseCode\": \"0\","
+ + "\"responseBody\": ["
+ + "{"
+ + "\"code\": \"044\","
+ + "\"name\": \"Access Bank\","
+ + "\"ussdTemplate\": \"*901*\","
+ + "\"baseUssdCode\": \"*901#\","
+ + "\"transferUssdTemplate\": \"*901*AMOUNT*ACCOUNT#\""
+ + "},"
+ + "{"
+ + "\"code\": \"058\","
+ + "\"name\": \"Guaranty Trust Bank\","
+ + "\"ussdTemplate\": \"*737*\","
+ + "\"baseUssdCode\": \"*737#\","
+ + "\"transferUssdTemplate\": \"*737*AMOUNT*ACCOUNT#\""
+ + "}"
+ + "]"
+ + "}")));
+
+ MonnifyBaseResponse> response = bankService.getBanks();
+ assertNotNull(response);
+ assertEquals("0", response.getResponseCode());
+ assertEquals("success", response.getResponseMessage());
+ assertNotNull(response.getResponseBody());
+ assertEquals(2, response.getResponseBody().size());
+ BankResponse firstBank = response.getResponseBody().get(0);
+ assertEquals("044", firstBank.code);
+ assertEquals("Access Bank", firstBank.getName());
+ assertEquals("*901*", firstBank.getUssdTemplate());
+ assertEquals("*901#", firstBank.getBaseUssdCode());
+ assertEquals("*901*AMOUNT*ACCOUNT#", firstBank.getTransferUssdTemplate());
+ BankResponse secondBank = response.getResponseBody().get(1);
+ assertEquals("058", secondBank.code);
+
+ assertEquals("Guaranty Trust Bank", secondBank.getName());
+
+ verify(
+ 1,
+ getRequestedFor(
+ urlPathEqualTo("/api/v1/banks")
+ )
+ );
+ }
+
+
+ @Test
+ void shouldReturnEmptyListWhenApiReturnsNoBanks() {
+
+ stubFor(get(urlPathEqualTo("/api/v1/banks"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{"
+ + "\"requestSuccessful\": true,"
+ + "\"responseMessage\": \"success\","
+ + "\"responseCode\": \"0\","
+ + "\"responseBody\": []"
+ + "}")));
+
+ MonnifyBaseResponse> response = bankService.getBanks();
+ assertNotNull(response);
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertTrue(response.getResponseBody().isEmpty());
+ verify(1, getRequestedFor(urlPathEqualTo("/api/v1/banks")));
+ }
+
+
+ @Test
+ void shouldReturnBanksWithUssdShortCode() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v1/sdk/transactions/banks"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\": true,"
+ + "\"responseMessage\": \"success\","
+ + "\"responseCode\": \"0\","
+ + "\"responseBody\": ["
+ + "{"
+ + "\"code\": \"044\","
+ + "\"name\": \"Access Bank\","
+ + "\"ussdTemplate\": \"*901*\","
+ + "\"baseUssdCode\": \"*901#\","
+ + "\"transferUssdTemplate\": \"*901*AMOUNT*ACCOUNT#\""
+ + "}"
+ + "]"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> response = bankService.getBanksWithUssdShortCode();
+ assertNotNull(response);
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertEquals(1, response.getResponseBody().size());
+ BankResponse bank = response.getResponseBody().get(0);
+ assertEquals("044", bank.code);
+ assertEquals("Access Bank", bank.getName());
+ assertEquals("*901*", bank.getUssdTemplate());
+ assertEquals("*901#", bank.getBaseUssdCode());
+ assertEquals("*901*AMOUNT*ACCOUNT#", bank.getTransferUssdTemplate());
+
+ verify(1, getRequestedFor(urlPathEqualTo("/api/v1/sdk/transactions/banks")));
+ }
+
+
+ @Test
+ void shouldReturnEmptyListForBanksWithUssdShortCode() {
+
+ stubFor(get(urlPathEqualTo("/api/v1/sdk/transactions/banks"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\": true,"
+ + "\"responseMessage\": \"success\","
+ + "\"responseCode\": \"0\","
+ + "\"responseBody\": []"
+ + "}")));
+
+ MonnifyBaseResponse> response = bankService.getBanksWithUssdShortCode();
+ assertNotNull(response);
+ assertNotNull(response.getResponseBody());
+ assertTrue(response.getResponseBody().isEmpty());
+ verify(1, getRequestedFor(urlPathEqualTo("/api/v1/sdk/transactions/banks")));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/DirectDebitServiceMockTest.java b/src/test/java/com/monnify/mocktests/DirectDebitServiceMockTest.java
new file mode 100644
index 0000000..778d1d1
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/DirectDebitServiceMockTest.java
@@ -0,0 +1,488 @@
+package com.monnify.mocktests;
+
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.exceptions.MonnifyValidationException;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.directdebit.*;
+import com.monnify.services.directdebit.DirectDebitService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class DirectDebitServiceMockTest extends BaseMonnifyMockTest {
+
+ private DirectDebitService directDebitService;
+
+ @BeforeEach
+ void init() {
+ directDebitService = new DirectDebitService();
+ }
+ private MandateRequest createValidMandateRequest() {
+ return MandateRequest.builder()
+ .contractCode("CONTRACT123")
+ .mandateReference("MANDATE-REF-123")
+ .autoRenew(true)
+ .customerCancellation(true)
+ .customerName("Halima Ismail")
+ .customerPhoneNumber("08012345678")
+ .customerEmailAddress("halima@example.com")
+ .customerAddress("Lagos, Nigeria")
+ .customerAccountName("Halima Ismail")
+ .customerAccountNumber("1234567890")
+ .customerAccountBankCode("044")
+ .mandateDescription("Monthly subscription payment")
+ .mandateStartDate("2026-08-13T00:00:00")
+ .mandateEndDate("2027-08-13T00:00:00")
+ .mandateAmount(new BigDecimal("10000.00"))
+ .debitAmount(new BigDecimal("1000.00"))
+ .build();
+ }
+
+ @Test
+ void shouldCreateMandateSuccessfully() {
+
+ stubFor(post(urlPathEqualTo("/api/v1/direct-debit/mandate/create"))
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"mandateCode\":\"MANDATE123\","
+ + "\"status\":\"PENDING_AUTHORIZATION\""
+ + "}"
+ + "}"
+ )));
+
+ MandateRequest request = createValidMandateRequest();
+ MonnifyBaseResponse result = directDebitService.createMandate(request);
+
+ assertNotNull(result);
+ assertEquals("0", result.getResponseCode());
+ assertTrue(result.isRequestSuccessful());
+
+ verify(1, postRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/create"
+ )));
+ }
+
+
+ @Test
+ void shouldUsePostForCreateMandate() {
+
+ stubFor(post(urlPathEqualTo("/api/v1/direct-debit/mandate/create"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json"
+ )
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{}"
+ + "}"
+ )
+ )
+ );
+
+
+ MandateRequest request = createValidMandateRequest();
+
+
+ directDebitService.createMandate(request);
+
+ verify(1, postRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/create")));
+ verify(0, getRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/create")));
+ }
+
+ @Test
+ void shouldGetMandateSuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo(
+ "/api/v1/direct-debit/mandate/"
+ ))
+ .withQueryParam(
+ "mandateReferences",
+ equalTo("MANDATE123")
+ )
+ .withHeader(
+ "Authorization",
+ equalTo("Bearer mock-access-token")
+ )
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json"
+ )
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":["
+ + "{"
+ + "\"mandateCode\":\"MANDATE123\","
+ + "\"status\":\"ACTIVE\""
+ + "}"
+ + "]"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> result = directDebitService.getMandate("MANDATE123");
+
+ assertNotNull(result);
+ assertEquals("0", result.getResponseCode());
+ assertTrue(result.isRequestSuccessful());
+
+ assertNotNull(result.getResponseBody());
+ assertEquals(1, result.getResponseBody().size());
+
+ verify(1, getRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/"))
+ .withQueryParam("mandateReferences", equalTo("MANDATE123")));
+ }
+
+ @Test
+ void shouldRejectEmptyMandateReference() {
+ assertThrows(MonnifyValidationException.class, () -> directDebitService.getMandate(""));
+ verify(0, getRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/")));
+ }
+
+ @Test
+ void shouldRejectNullMandateReference() {
+ assertThrows(MonnifyValidationException.class, () -> directDebitService.getMandate(null));
+ }
+ @Test
+ void shouldDebitMandateSuccessfully() {
+
+ /*
+ * Mock the debit mandate endpoint.
+ */
+ stubFor(
+ post(urlPathEqualTo("/api/v1/direct-debit/mandate/debit"))
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"transactionStatus\":\"SUCCESS\","
+ + "\"responseMessage\":\"Debit successful\","
+ + "\"transactionReference\":\"TXN123456\","
+ + "\"paymentReference\":\"PAY123\","
+ + "\"debitAmount\":1000.00,"
+ + "\"narration\":\"Payment for subscription\","
+ + "\"mandateCode\":\"MANDATE123\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+ MandateDebitRequest request = new MandateDebitRequest();
+ request.setPaymentReference("PAY123");
+ request.setMandateCode("MANDATE123");
+ request.setDebitAmount(new BigDecimal("1000.00"));
+ request.setNarration("Payment for subscription");
+ request.setCustomerEmail("customer@example.com");
+ MonnifyBaseResponse result = directDebitService.debitMandate(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertEquals("success", result.getResponseMessage());
+
+ assertNotNull(result.getResponseBody());
+
+ MandateDebitResponse response = result.getResponseBody();
+
+ assertEquals("SUCCESS", response.getTransactionStatus());
+ assertEquals("Debit successful", response.getResponseMessage());
+ assertEquals("TXN123456", response.getTransactionReference());
+ assertEquals("PAY123", response.getPaymentReference());
+ assertEquals(new BigDecimal("1000.00"), response.getDebitAmount());
+ assertEquals("Payment for subscription", response.getNarration());
+ assertEquals("MANDATE123", response.getMandateCode());
+ verify(1, postRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/debit")));
+ verify(postRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/debit")).withHeader("Authorization", equalTo("Bearer mock-access-token")));
+ verify(0, getRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/debit")));
+ }
+
+ @Test
+ void shouldGetDebitStatusSuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v1/direct-debit/mandate/debit-status"))
+ .withQueryParam("paymentReference", equalTo("PAY123"))
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"transactionStatus\":\"SUCCESS\","
+ + "\"responseMessage\":\"Debit successful\","
+ + "\"transactionReference\":\"TXN123456\","
+ + "\"paymentReference\":\"PAY123\","
+ + "\"debitAmount\":1000.00,"
+ + "\"narration\":\"Payment for subscription\","
+ + "\"mandateCode\":\"MANDATE123\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse result = directDebitService.getDebitStatus("PAY123");
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertEquals("success", result.getResponseMessage());
+ assertNotNull(result.getResponseBody());
+
+ MandateDebitResponse response = result.getResponseBody();
+
+ assertEquals("SUCCESS", response.getTransactionStatus());
+ assertEquals("Debit successful", response.getResponseMessage());
+ assertEquals("TXN123456", response.getTransactionReference());
+ assertEquals("PAY123", response.getPaymentReference());
+ assertEquals(new BigDecimal("1000.00"), response.getDebitAmount());
+ assertEquals("Payment for subscription", response.getNarration());
+ assertEquals("MANDATE123", response.getMandateCode());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/debit-status"))
+ .withQueryParam("paymentReference", equalTo("PAY123"))
+ );
+
+ verify(
+ getRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/debit-status"))
+ .withQueryParam("paymentReference", equalTo("PAY123"))
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ );
+ }
+ @Test
+ void shouldEncodePaymentReference() {
+
+ String paymentReference = "PAY 123";
+
+ stubFor(
+ get(urlPathEqualTo("/api/v1/direct-debit/mandate/debit-status"))
+ .withQueryParam("paymentReference", equalTo("PAY 123"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"paymentReference\":\"PAY 123\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse result =
+ directDebitService.getDebitStatus(paymentReference);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/debit-status"))
+ .withQueryParam("paymentReference", equalTo("PAY 123"))
+ );
+ }
+
+ @Test
+ void shouldRejectEmptyPaymentReference() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> directDebitService.getDebitStatus("")
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/debit-status"))
+ );
+ }
+
+ @Test
+ void shouldRejectNullPaymentReference() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> directDebitService.getDebitStatus(null)
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/debit-status"))
+ );
+ }
+
+// ============================================================
+// CANCEL MANDATE
+// ============================================================
+
+ @Test
+ void shouldCancelMandateSuccessfully() {
+
+ stubFor(
+ patch(urlPathEqualTo("/api/v1/direct-debit/mandate/cancel-mandate/MANDATE123"))
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"mandateCode\":\"MANDATE123\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse result =
+ directDebitService.cancelMandate("MANDATE123");
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertEquals("success", result.getResponseMessage());
+
+ verify(
+ 1,
+ patchRequestedFor(
+ urlPathEqualTo("/api/v1/direct-debit/mandate/cancel-mandate/MANDATE123")
+ )
+ );
+
+ verify(
+ patchRequestedFor(
+ urlPathEqualTo("/api/v1/direct-debit/mandate/cancel-mandate/MANDATE123")
+ )
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ );
+ }
+
+ @Test
+ void shouldRejectEmptyMandateCode() {
+
+ assertThrows(MonnifyValidationException.class, () -> directDebitService.cancelMandate(""));
+
+ verify(0, patchRequestedFor(
+ urlPathMatching("/api/v1/direct-debit/mandate/cancel-mandate/.*")
+ )
+ );
+ }
+
+ @Test
+ void shouldRejectNullMandateCode() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> directDebitService.cancelMandate(null)
+ );
+
+ verify(
+ 0,
+ patchRequestedFor(
+ urlPathMatching("/api/v1/direct-debit/mandate/cancel-mandate/.*")
+ )
+ );
+ }
+
+ @Test
+ void shouldReturnErrorResponseWhenCreateMandateFails() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v1/direct-debit/mandate/create"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":false,"
+ + "\"responseMessage\":\"Invalid mandate\","
+ + "\"responseCode\":\"99\","
+ + "\"responseBody\":null"
+ + "}"
+ )
+ )
+ );
+
+ MandateRequest request = new MandateRequest();
+
+ request.setContractCode("CONTRACT123");
+ request.setMandateReference("MANDATE-REF-123");
+ request.setCustomerName("Test Customer");
+ request.setCustomerPhoneNumber("08012345678");
+ request.setCustomerEmailAddress("customer@example.com");
+ request.setCustomerAddress("123 Test Street");
+ request.setCustomerAccountName("Test Customer");
+ request.setCustomerAccountNumber("0123456789");
+ request.setCustomerAccountBankCode("058");
+ request.setMandateDescription("Subscription payment");
+ request.setMandateStartDate("2026-08-13T00:00:00");
+ request.setMandateEndDate("2027-08-13T00:00:00");
+ request.setMandateAmount(new BigDecimal("1000.00"));
+ request.setDebitAmount(new BigDecimal("1000.00"));
+ request.setAutoRenew(false);
+ request.setCustomerCancellation(false);
+
+ MonnifyBaseResponse result =
+ directDebitService.createMandate(request);
+
+ assertNotNull(result);
+ assertFalse(result.isRequestSuccessful());
+ assertEquals("99", result.getResponseCode());
+ assertEquals("Invalid mandate", result.getResponseMessage());
+
+ verify(
+ 1,
+ postRequestedFor(urlPathEqualTo("/api/v1/direct-debit/mandate/create"))
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/DisbursementMockTest.java b/src/test/java/com/monnify/mocktests/DisbursementMockTest.java
new file mode 100644
index 0000000..ad75a5f
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/DisbursementMockTest.java
@@ -0,0 +1,727 @@
+package com.monnify.mocktests;
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
+
+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.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.exceptions.MonnifyValidationException;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.SearchResponse;
+import com.monnify.models.disbursement.BatchDisbursementRequest;
+import com.monnify.models.disbursement.SingleDisbursementRequest;
+import com.monnify.models.disbursement.SearchTransactionsRequest;
+import com.monnify.models.disbursement.OnValidationFailure;
+import com.monnify.models.disbursement.SingleDisbursementResponse;
+import com.monnify.models.disbursement.DisbursementTransaction;
+import com.monnify.models.disbursement.BatchDisbursementResponse;
+import com.monnify.models.disbursement.TransferDetails;
+import com.monnify.models.disbursement.DisbursementSummaryResponse;
+
+import com.monnify.models.wallet.WalletBalanceResponse;
+import com.monnify.services.disbursement.DisbursementService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+public class DisbursementMockTest extends BaseMonnifyMockTest {
+ private DisbursementService disbursementService;
+
+ @BeforeEach
+ void setUpService() {
+ disbursementService = new DisbursementService();
+ }
+
+ @Test
+ void shouldDisburseSingleSuccessfully() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v2/disbursements/single"))
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"amount\":1000.00,"
+ + "\"reference\":\"DISB123\","
+ + "\"status\":\"SUCCESS\","
+ + "\"totalFee\":10.00,"
+ + "\"destinationAccountName\":\"Test Customer\","
+ + "\"destinationBankName\":\"Test Bank\","
+ + "\"destinationAccountNumber\":\"0123456789\","
+ + "\"destinationBankCode\":\"058\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ SingleDisbursementRequest request = new SingleDisbursementRequest();
+ request.setAmount(new BigDecimal("1000.00"));
+ request.setReference("DISB123");
+ request.setNarration("Test disbursement");
+ request.setDestinationBankCode("058");
+ request.setDestinationAccountNumber("0123456789");
+ request.setCurrency("NGN");
+ request.setSourceAccountNumber("1234567890");
+ request.setDestinationAccountName("Test Customer");
+ request.setAsync(false);
+
+ MonnifyBaseResponse result =
+ disbursementService.disburseSingle(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertEquals("success", result.getResponseMessage());
+ assertNotNull(result.getResponseBody());
+ assertEquals(new BigDecimal("1000.00"), result.getResponseBody().getAmount());
+ assertEquals("DISB123", result.getResponseBody().getReference());
+ assertEquals("SUCCESS", result.getResponseBody().getStatus());
+ assertEquals(new BigDecimal("10.00"), result.getResponseBody().getTotalFee());
+ assertEquals("Test Customer", result.getResponseBody().getDestinationAccountName());
+ assertEquals("Test Bank", result.getResponseBody().getDestinationBankName());
+ assertEquals("0123456789", result.getResponseBody().getDestinationAccountNumber());
+ assertEquals("058", result.getResponseBody().getDestinationBankCode());
+
+ verify(
+ 1,
+ postRequestedFor(urlPathEqualTo("/api/v2/disbursements/single"))
+ );
+
+ verify(
+ postRequestedFor(urlPathEqualTo("/api/v2/disbursements/single"))
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/single"))
+ );
+ }
+
+ @Test
+ void shouldRejectInvalidSingleDisbursementRequest() {
+
+ SingleDisbursementRequest request = new SingleDisbursementRequest();
+
+ assertThrows(
+ Exception.class,
+ () -> disbursementService.disburseSingle(request)
+ );
+
+ verify(
+ 0,
+ postRequestedFor(urlPathEqualTo("/api/v2/disbursements/single"))
+ );
+ }
+
+ @Test
+ void shouldReturnErrorResponseWhenSingleDisbursementFails() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v2/disbursements/single"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":false,"
+ + "\"responseMessage\":\"Insufficient balance\","
+ + "\"responseCode\":\"99\","
+ + "\"responseBody\":null"
+ + "}"
+ )
+ )
+ );
+
+ SingleDisbursementRequest request = new SingleDisbursementRequest();
+ request.setAmount(new BigDecimal("1000.00"));
+ request.setReference("DISB123");
+ request.setNarration("Test disbursement");
+ request.setDestinationBankCode("058");
+ request.setDestinationAccountNumber("0123456789");
+ request.setCurrency("NGN");
+ request.setSourceAccountNumber("1234567890");
+ request.setDestinationAccountName("Test Customer");
+ request.setAsync(false);
+
+ MonnifyBaseResponse result =
+ disbursementService.disburseSingle(request);
+
+ assertNotNull(result);
+ assertFalse(result.isRequestSuccessful());
+ assertEquals("99", result.getResponseCode());
+ assertEquals("Insufficient balance", result.getResponseMessage());
+
+ verify(
+ 1,
+ postRequestedFor(urlPathEqualTo("/api/v2/disbursements/single"))
+ );
+ }
+
+ @Test
+ void shouldDisburseBatchSuccessfully() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v2/disbursements/batch"))
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"totalAmount\":2000.00,"
+ + "\"totalFee\":20.00,"
+ + "\"batchReference\":\"BATCH123\","
+ + "\"batchStatus\":\"SUCCESS\","
+ + "\"totalTransactionsCount\":2"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ DisbursementTransaction transaction1 = new DisbursementTransaction();
+ transaction1.setAmount(new BigDecimal("1000.00"));
+ transaction1.setReference("TXN001");
+ transaction1.setNarration("Payment one");
+ transaction1.setDestinationBankCode("058");
+ transaction1.setDestinationAccountNumber("0123456789");
+ transaction1.setCurrency("NGN");
+
+ DisbursementTransaction transaction2 = new DisbursementTransaction();
+ transaction2.setAmount(new BigDecimal("1000.00"));
+ transaction2.setReference("TXN002");
+ transaction2.setNarration("Payment two");
+ transaction2.setDestinationBankCode("058");
+ transaction2.setDestinationAccountNumber("9876543210");
+ transaction2.setCurrency("NGN");
+
+ BatchDisbursementRequest request = new BatchDisbursementRequest();
+ request.setTitle("Test Batch");
+ request.setBatchReference("BATCH123");
+ request.setNarration("Batch disbursement");
+ request.setSourceAccountNumber("1234567890");
+ request.setOnValidationFailure(OnValidationFailure.values()[0]);
+ request.setTransactionList(java.util.Arrays.asList(transaction1, transaction2));
+
+ MonnifyBaseResponse result =
+ disbursementService.disburseBatch(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertEquals("success", result.getResponseMessage());
+ assertNotNull(result.getResponseBody());
+ assertEquals(new BigDecimal("2000.00"), result.getResponseBody().getTotalAmount());
+ assertEquals(new BigDecimal("20.00"), result.getResponseBody().getTotalFee());
+ assertEquals("BATCH123", result.getResponseBody().getBatchReference());
+ assertEquals("SUCCESS", result.getResponseBody().getBatchStatus());
+ assertEquals(2, result.getResponseBody().getTotalTransactionsCount());
+
+ verify(
+ 1,
+ postRequestedFor(urlPathEqualTo("/api/v2/disbursements/batch"))
+ );
+
+ verify(
+ postRequestedFor(urlPathEqualTo("/api/v2/disbursements/batch"))
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ );
+ }
+
+ @Test
+ void shouldRejectInvalidBatchDisbursementRequest() {
+
+ BatchDisbursementRequest request = new BatchDisbursementRequest();
+
+ assertThrows(
+ Exception.class,
+ () -> disbursementService.disburseBatch(request)
+ );
+
+ verify(
+ 0,
+ postRequestedFor(urlPathEqualTo("/api/v2/disbursements/batch"))
+ );
+ }
+
+ @Test
+ void shouldGetDisbursementSummarySuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v2/disbursements/single/summary"))
+ .withQueryParam("reference", equalTo("DISB123"))
+ .withHeader("Authorization", equalTo("Bearer mock-access-token"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse result =
+ disbursementService.getDisbursementSummary("DISB123");
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertEquals("success", result.getResponseMessage());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/single/summary"))
+ .withQueryParam("reference", equalTo("DISB123"))
+ );
+ }
+
+ @Test
+ void shouldRejectEmptyDisbursementReference() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> disbursementService.getDisbursementSummary("")
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/single/summary"))
+ );
+ }
+
+ @Test
+ void shouldRejectNullDisbursementReference() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> disbursementService.getDisbursementSummary(null)
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/single/summary"))
+ );
+ }
+
+ @Test
+ void shouldGetAllSingleDisbursementsSuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v2/disbursements/single/transactions"))
+ .withQueryParam("pageNo", equalTo("0"))
+ .withQueryParam("pageSize", equalTo("10"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> result =
+ disbursementService.getAllSingleDisbursements(10, 0);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/single/transactions"))
+ .withQueryParam("pageNo", equalTo("0"))
+ .withQueryParam("pageSize", equalTo("10"))
+ );
+ }
+
+ @Test
+ void shouldGetAllBulkDisbursementsSuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v2/disbursements/bulk/transactions"))
+ .withQueryParam("pageNo", equalTo("0"))
+ .withQueryParam("pageSize", equalTo("10"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> result =
+ disbursementService.getAllBulkDisbursements(10, 0);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/bulk/transactions"))
+ .withQueryParam("pageNo", equalTo("0"))
+ .withQueryParam("pageSize", equalTo("10"))
+ );
+ }
+
+ @Test
+ void shouldGetBulkTransferTransactionsSuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v2/disbursements/bulk/BATCH123/transactions"))
+ .withQueryParam("pageNo", equalTo("0"))
+ .withQueryParam("pageSize", equalTo("10"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> result =
+ disbursementService.getBulkTransferTransactions("BATCH123", 10, 0);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/bulk/BATCH123/transactions"))
+ .withQueryParam("pageNo", equalTo("0"))
+ .withQueryParam("pageSize", equalTo("10"))
+ );
+ }
+
+ @Test
+ void shouldRejectEmptyBatchReferenceForBulkTransactions() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> disbursementService.getBulkTransferTransactions("", 10, 0)
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathMatching("/api/v2/disbursements/bulk/.*/transactions"))
+ );
+ }
+
+ @Test
+ void shouldRejectNullBatchReferenceForBulkTransactions() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> disbursementService.getBulkTransferTransactions(null, 10, 0)
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathMatching("/api/v2/disbursements/bulk/.*/transactions"))
+ );
+ }
+
+ @Test
+ void shouldEncodeBatchReference() {
+
+ String batchReference = "BATCH 123";
+
+ stubFor(
+ get(urlPathMatching("/api/v2/disbursements/bulk/BATCH\\+123/transactions"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> result =
+ disbursementService.getBulkTransferTransactions(batchReference, null, null);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathMatching("/api/v2/disbursements/bulk/.*/transactions"))
+ );
+ }
+
+ @Test
+ void shouldGetBulkTransferStatusSuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v2/disbursements/batch/summary"))
+ .withQueryParam("reference", equalTo("BATCH123"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> result =
+ disbursementService.getBulkTransferStatus("BATCH123");
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/batch/summary"))
+ .withQueryParam("reference", equalTo("BATCH123"))
+ );
+ }
+
+ @Test
+ void shouldRejectEmptyBatchReferenceForBulkStatus() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> disbursementService.getBulkTransferStatus("")
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/batch/summary"))
+ );
+ }
+
+ @Test
+ void shouldRejectNullBatchReferenceForBulkStatus() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> disbursementService.getBulkTransferStatus(null)
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/batch/summary"))
+ );
+ }
+
+ @Test
+ void shouldSearchTransactionsSuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v2/disbursements/search-transactions"))
+ .withQueryParam("sourceAccountNumber", equalTo("1234567890"))
+ .withQueryParam("pageSize", equalTo("10"))
+ .withQueryParam("pageNo", equalTo("0"))
+ .withQueryParam("startDate", equalTo("2026-08-01"))
+ .withQueryParam("endDate", equalTo("2026-08-13"))
+ .withQueryParam("amountFrom", equalTo("100"))
+ .withQueryParam("amountTo", equalTo("10000"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{}"
+ + "}"
+ )
+ )
+ );
+
+ SearchTransactionsRequest request = new SearchTransactionsRequest();
+ request.setSourceAccountNumber("1234567890");
+ request.setPageSize(10);
+ request.setPageNo(0);
+ request.setStartDate("2026-08-01");
+ request.setEndDate("2026-08-13");
+ request.setAmountFrom("100");
+ request.setAmountTo("10000");
+
+ MonnifyBaseResponse> result = disbursementService.searchTransactions(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/search-transactions"))
+ .withQueryParam("sourceAccountNumber", equalTo("1234567890"))
+ .withQueryParam("pageSize", equalTo("10"))
+ .withQueryParam("pageNo", equalTo("0"))
+ .withQueryParam("startDate", equalTo("2026-08-01"))
+ .withQueryParam("endDate", equalTo("2026-08-13"))
+ .withQueryParam("amountFrom", equalTo("100"))
+ .withQueryParam("amountTo", equalTo("10000"))
+ );
+ }
+
+ @Test
+ void shouldSearchTransactionsWithoutOptionalParameters() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v2/disbursements/search-transactions"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{}"
+ + "}"
+ )
+ )
+ );
+
+ SearchTransactionsRequest request = new SearchTransactionsRequest();
+
+ MonnifyBaseResponse> result =
+ disbursementService.searchTransactions(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/search-transactions"))
+ );
+ }
+
+ @Test
+ void shouldGetWalletBalanceSuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v2/disbursements/wallet-balance"))
+ .withQueryParam("accountNumber", equalTo("1234567890"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse result =
+ disbursementService.getWalletBalance("1234567890");
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+
+ verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/wallet-balance"))
+ .withQueryParam("accountNumber", equalTo("1234567890"))
+ );
+ }
+
+ @Test
+ void shouldRejectEmptyWalletAccountNumber() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> disbursementService.getWalletBalance("")
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/wallet-balance"))
+ );
+ }
+
+ @Test
+ void shouldRejectNullWalletAccountNumber() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> disbursementService.getWalletBalance(null)
+ );
+
+ verify(
+ 0,
+ getRequestedFor(urlPathEqualTo("/api/v2/disbursements/wallet-balance"))
+ );
+ }
+}
diff --git a/src/test/java/com/monnify/mocktests/InvoiceServiceMockTest.java b/src/test/java/com/monnify/mocktests/InvoiceServiceMockTest.java
new file mode 100644
index 0000000..494d78a
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/InvoiceServiceMockTest.java
@@ -0,0 +1,537 @@
+package com.monnify.mocktests;
+
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.exceptions.MonnifyValidationException;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.SearchResponse;
+import com.monnify.models.invoice.InvoiceRequest;
+import com.monnify.models.invoice.InvoiceResponse;
+import com.monnify.services.invoice.InvoiceService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+
+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.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class InvoiceServiceMockTest extends BaseMonnifyMockTest {
+
+ private InvoiceService invoiceService;
+
+ @BeforeEach
+ void setUp() {
+ invoiceService = new InvoiceService();
+ }
+
+ @Test
+ void shouldCreateInvoiceSuccessfully() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v1/invoice/create"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"amount\":5000,"
+ + "\"invoiceReference\":\"INV123\","
+ + "\"invoiceStatus\":\"PENDING\","
+ + "\"description\":\"Test invoice\","
+ + "\"contractCode\":\"CONTRACT123\","
+ + "\"customerEmail\":\"customer@example.com\","
+ + "\"customerName\":\"Test Customer\","
+ + "\"expiryDate\":\"2026-08-20T23:59:59\","
+ + "\"createdBy\":\"test-user\","
+ + "\"createdOn\":\"2026-08-13T10:00:00\","
+ + "\"checkoutUrl\":\"https://checkout.example.com/INV123\","
+ + "\"accountNumber\":\"1234567890\","
+ + "\"accountName\":\"Test Customer\","
+ + "\"bankName\":\"Test Bank\","
+ + "\"bankCode\":\"058\","
+ + "\"redirectUrl\":\"https://example.com/callback\","
+ + "\"transactionReference\":\"TXN123\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ InvoiceRequest request = createValidInvoiceRequest();
+
+ MonnifyBaseResponse result =
+ invoiceService.createInvoice(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertEquals("success", result.getResponseMessage());
+
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ new BigDecimal("5000"),
+ result.getResponseBody().getAmount()
+ );
+
+ assertEquals(
+ "INV123",
+ result.getResponseBody().getInvoiceReference()
+ );
+
+ assertEquals(
+ "PENDING",
+ result.getResponseBody().getInvoiceStatus()
+ );
+
+ assertEquals(
+ "customer@example.com",
+ result.getResponseBody().getCustomerEmail()
+ );
+
+ assertEquals(
+ "Test Customer",
+ result.getResponseBody().getCustomerName()
+ );
+
+ assertEquals(
+ "1234567890",
+ result.getResponseBody().getAccountNumber()
+ );
+
+ assertEquals(
+ "058",
+ result.getResponseBody().getBankCode()
+ );
+
+ assertEquals(
+ "TXN123",
+ result.getResponseBody().getTransactionReference()
+ );
+
+ verify(
+ 1,
+ postRequestedFor(
+ urlPathEqualTo("/api/v1/invoice/create")
+ )
+ );
+ }
+
+ @Test
+ void shouldRejectInvalidCreateInvoiceRequest() {
+
+ InvoiceRequest request = new InvoiceRequest();
+
+ assertThrows(
+ Exception.class,
+ () -> invoiceService.createInvoice(request)
+ );
+
+ verify(
+ 0,
+ postRequestedFor(
+ urlPathEqualTo("/api/v1/invoice/create")
+ )
+ );
+ }
+
+ @Test
+ void shouldAttachInvoiceToReservedAccountSuccessfully() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v1/invoice/create"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"amount\":5000,"
+ + "\"invoiceReference\":\"INV123\","
+ + "\"invoiceStatus\":\"PENDING\","
+ + "\"accountReference\":\"ACC123\","
+ + "\"accountNumber\":\"1234567890\","
+ + "\"accountName\":\"Test Customer\","
+ + "\"bankName\":\"Test Bank\","
+ + "\"bankCode\":\"058\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ InvoiceRequest request = createValidInvoiceRequest();
+ request.setAccountReference("ACC123");
+
+ MonnifyBaseResponse result =
+ invoiceService.attachInvoiceToReservedAccount(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "INV123",
+ result.getResponseBody().getInvoiceReference()
+ );
+
+ assertEquals(
+ "1234567890",
+ result.getResponseBody().getAccountNumber()
+ );
+
+ verify(
+ 1,
+ postRequestedFor(
+ urlPathEqualTo("/api/v1/invoice/create")
+ )
+ );
+ }
+
+ @Test
+ void shouldRejectInvoiceWithoutAccountReference() {
+
+ InvoiceRequest request = createValidInvoiceRequest();
+ request.setAccountReference(null);
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> invoiceService.attachInvoiceToReservedAccount(request)
+ );
+
+ verify(
+ 0,
+ postRequestedFor(
+ urlPathEqualTo("/api/v1/invoice/create")
+ )
+ );
+ }
+
+ @Test
+ void shouldRejectEmptyAccountReference() {
+
+ InvoiceRequest request = createValidInvoiceRequest();
+ request.setAccountReference("");
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> invoiceService.attachInvoiceToReservedAccount(request)
+ );
+
+ verify(
+ 0,
+ postRequestedFor(
+ urlPathEqualTo("/api/v1/invoice/create")
+ )
+ );
+ }
+
+ @Test
+ void shouldViewInvoiceDetailsSuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v1/invoice/INV123/details"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"amount\":5000,"
+ + "\"invoiceReference\":\"INV123\","
+ + "\"invoiceStatus\":\"PENDING\","
+ + "\"description\":\"Test invoice\","
+ + "\"contractCode\":\"CONTRACT123\","
+ + "\"customerEmail\":\"customer@example.com\","
+ + "\"customerName\":\"Test Customer\","
+ + "\"transactionReference\":\"TXN123\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse result =
+ invoiceService.viewInvoiceDetails("INV123");
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "INV123",
+ result.getResponseBody().getInvoiceReference()
+ );
+
+ assertEquals(
+ "PENDING",
+ result.getResponseBody().getInvoiceStatus()
+ );
+
+ assertEquals(
+ "TXN123",
+ result.getResponseBody().getTransactionReference()
+ );
+
+ verify(
+ 1,
+ getRequestedFor(
+ urlPathEqualTo("/api/v1/invoice/INV123/details")
+ )
+ );
+ }
+
+
+ @Test
+ void shouldRejectEmptyInvoiceReferenceForViewDetails() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> invoiceService.viewInvoiceDetails("")
+ );
+
+ verify(
+ 0,
+ getRequestedFor(
+ urlPathMatching("/api/v1/invoice/.*/details")
+ )
+ );
+ }
+
+ @Test
+ void shouldRejectNullInvoiceReferenceForViewDetails() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> invoiceService.viewInvoiceDetails(null)
+ );
+
+ verify(
+ 0,
+ getRequestedFor(
+ urlPathMatching("/api/v1/invoice/.*/details")
+ )
+ );
+ }
+
+ @Test
+ void shouldGetAllInvoicesSuccessfully() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v1/invoice/all"))
+ .withQueryParam("page", equalTo("0"))
+ .withQueryParam("size", equalTo("10"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"content\":[],"
+ + "\"pageNumber\":0,"
+ + "\"pageSize\":10,"
+ + "\"totalElements\":0,"
+ + "\"totalPages\":0"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> result =
+ invoiceService.getAllInvoices(0, 10);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ verify(
+ 1,
+ getRequestedFor(
+ urlPathEqualTo("/api/v1/invoice/all")
+ )
+ .withQueryParam("page", equalTo("0"))
+ .withQueryParam("size", equalTo("10"))
+ );
+ }
+
+ @Test
+ void shouldCancelInvoiceSuccessfully() {
+
+ stubFor(
+ delete(urlPathEqualTo("/api/v1/invoice/INV123/cancel"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"amount\":5000,"
+ + "\"invoiceReference\":\"INV123\","
+ + "\"invoiceStatus\":\"CANCELLED\","
+ + "\"customerName\":\"Test Customer\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse result =
+ invoiceService.cancelInvoice("INV123");
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "INV123",
+ result.getResponseBody().getInvoiceReference()
+ );
+
+ assertEquals(
+ "CANCELLED",
+ result.getResponseBody().getInvoiceStatus()
+ );
+
+ verify(
+ 1,
+ deleteRequestedFor(
+ urlPathEqualTo("/api/v1/invoice/INV123/cancel")
+ )
+ );
+ }
+
+ @Test
+ void shouldRejectEmptyInvoiceReferenceWhenCancelling() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> invoiceService.cancelInvoice("")
+ );
+
+ verify(
+ 0,
+ deleteRequestedFor(
+ urlPathMatching("/api/v1/invoice/.*/cancel")
+ )
+ );
+ }
+
+ @Test
+ void shouldRejectNullInvoiceReferenceWhenCancelling() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> invoiceService.cancelInvoice(null)
+ );
+
+ verify(
+ 0,
+ deleteRequestedFor(
+ urlPathMatching("/api/v1/invoice/.*/cancel")
+ )
+ );
+ }
+
+ @Test
+ void shouldReturnErrorResponseWhenCreateInvoiceFails() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v1/invoice/create"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":false,"
+ + "\"responseMessage\":\"Invalid invoice\","
+ + "\"responseCode\":\"99\","
+ + "\"responseBody\":null"
+ + "}"
+ )
+ )
+ );
+
+ InvoiceRequest request = createValidInvoiceRequest();
+
+ MonnifyBaseResponse result =
+ invoiceService.createInvoice(request);
+
+ assertNotNull(result);
+ assertFalse(result.isRequestSuccessful());
+ assertEquals("99", result.getResponseCode());
+ assertEquals(
+ "Invalid invoice",
+ result.getResponseMessage()
+ );
+
+ verify(
+ 1,
+ postRequestedFor(
+ urlPathEqualTo("/api/v1/invoice/create")
+ )
+ );
+ }
+
+ private InvoiceRequest createValidInvoiceRequest() {
+
+ InvoiceRequest request = new InvoiceRequest();
+
+ request.setAmount(new BigDecimal("5000.00"));
+ request.setInvoiceReference("INV123");
+ request.setDescription("Test invoice");
+ request.setCurrencyCode("NGN");
+ request.setContractCode("CONTRACT123");
+ request.setCustomerEmail("customer@example.com");
+ request.setCustomerName("Test Customer");
+ request.setExpiryDate("2026-08-20T23:59:59");
+ request.setRedirectUrl("https://example.com/callback");
+
+ return request;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/LimitProfileServiceMockTest.java b/src/test/java/com/monnify/mocktests/LimitProfileServiceMockTest.java
new file mode 100644
index 0000000..55db20a
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/LimitProfileServiceMockTest.java
@@ -0,0 +1,471 @@
+package com.monnify.mocktests;
+
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.exceptions.MonnifyValidationException;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.SearchResponse;
+import com.monnify.models.limitprofile.LimitProfileRequest;
+import com.monnify.models.limitprofile.LimitProfileResponse;
+import com.monnify.services.limitprofile.LimitProfileService;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+class LimitProfileServiceMockTest extends BaseMonnifyMockTest {
+
+ private final LimitProfileService limitProfileService =
+ new LimitProfileService();
+
+ @Test
+ void shouldCreateLimitProfile() {
+
+ String requestBody =
+ "{"
+ + "\"limitProfileName\":\"Test Profile\","
+ + "\"singleTransactionValue\":100000.00,"
+ + "\"dailyTransactionVolume\":10,"
+ + "\"dailyTransactionValue\":500000.00"
+ + "}";
+
+ stubFor(
+ post(urlPathEqualTo("/api/v1/limit-profile"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"limitProfileCode\":\"LP001\","
+ + "\"limitProfileName\":\"Test Profile\","
+ + "\"singleTransactionValue\":100000.00,"
+ + "\"dailyTransactionVolume\":10,"
+ + "\"dailyTransactionValue\":500000.00,"
+ + "\"dateCreated\":\"2026-08-14T10:00:00\","
+ + "\"lastModified\":\"2026-08-14T10:00:00\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ LimitProfileRequest request =
+ LimitProfileRequest.builder()
+ .limitProfileName("Test Profile")
+ .singleTransactionValue(new BigDecimal("100000.00"))
+ .dailyTransactionVolume(10)
+ .dailyTransactionValue(new BigDecimal("500000.00"))
+ .build();
+
+ MonnifyBaseResponse result =
+ limitProfileService.createLimitProfile(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("success", result.getResponseMessage());
+ assertEquals("0", result.getResponseCode());
+
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "LP001",
+ result.getResponseBody().getLimitProfileCode()
+ );
+
+ assertEquals(
+ "Test Profile",
+ result.getResponseBody().getLimitProfileName()
+ );
+
+ assertEquals(
+ new BigDecimal("100000.00"),
+ result.getResponseBody().getSingleTransactionValue()
+ );
+
+ assertEquals(
+ 10,
+ result.getResponseBody().getDailyTransactionVolume()
+ );
+
+ assertEquals(
+ new BigDecimal("500000.00"),
+ result.getResponseBody().getDailyTransactionValue()
+ );
+
+ verify(
+ 1,
+ postRequestedFor(
+ urlPathEqualTo("/api/v1/limit-profile")
+ ).withRequestBody(
+ equalToJson(requestBody)
+ )
+ );
+ }
+
+ @Test
+ void shouldGetLimitProfiles() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v1/limit-profile"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"content\":["
+ + "{"
+ + "\"limitProfileCode\":\"LP001\","
+ + "\"limitProfileName\":\"Test Profile\","
+ + "\"singleTransactionValue\":100000.00,"
+ + "\"dailyTransactionVolume\":10,"
+ + "\"dailyTransactionValue\":500000.00,"
+ + "\"dateCreated\":\"2026-08-14T10:00:00\","
+ + "\"lastModified\":\"2026-08-14T10:00:00\""
+ + "}"
+ + "],"
+ + "\"pageNumber\":0,"
+ + "\"pageSize\":10,"
+ + "\"totalPages\":1,"
+ + "\"totalElements\":1"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> result =
+ limitProfileService.getLimitProfiles();
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("success", result.getResponseMessage());
+ assertEquals("0", result.getResponseCode());
+
+ assertNotNull(result.getResponseBody());
+
+ assertNotNull(result.getResponseBody().getContent());
+ assertEquals(1, result.getResponseBody().getContent().size());
+
+ LimitProfileResponse profile =
+ result.getResponseBody().getContent().get(0);
+
+ assertEquals(
+ "LP001",
+ profile.getLimitProfileCode()
+ );
+
+ assertEquals(
+ "Test Profile",
+ profile.getLimitProfileName()
+ );
+
+ assertEquals(
+ new BigDecimal("100000.00"),
+ profile.getSingleTransactionValue()
+ );
+
+ assertEquals(
+ 10,
+ profile.getDailyTransactionVolume()
+ );
+
+ assertEquals(
+ new BigDecimal("500000.00"),
+ profile.getDailyTransactionValue()
+ );
+
+ verify(
+ 1,
+ getRequestedFor(
+ urlPathEqualTo("/api/v1/limit-profile")
+ )
+ );
+ }
+
+ @Test
+ void shouldUpdateLimitProfile() {
+
+ String limitProfileCode = "LP001";
+
+ String requestBody =
+ "{"
+ + "\"limitProfileName\":\"Updated Profile\","
+ + "\"singleTransactionValue\":200000.00,"
+ + "\"dailyTransactionVolume\":20,"
+ + "\"dailyTransactionValue\":1000000.00"
+ + "}";
+
+ stubFor(
+ put(urlPathEqualTo("/api/v1/limit-profile/LP001"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"limitProfileCode\":\"LP001\","
+ + "\"limitProfileName\":\"Updated Profile\","
+ + "\"singleTransactionValue\":200000.00,"
+ + "\"dailyTransactionVolume\":20,"
+ + "\"dailyTransactionValue\":1000000.00,"
+ + "\"dateCreated\":\"2026-08-14T10:00:00\","
+ + "\"lastModified\":\"2026-08-14T11:00:00\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ LimitProfileRequest request =
+ LimitProfileRequest.builder()
+ .limitProfileName("Updated Profile")
+ .singleTransactionValue(new BigDecimal("200000.00"))
+ .dailyTransactionVolume(20)
+ .dailyTransactionValue(new BigDecimal("1000000.00"))
+ .build();
+
+ MonnifyBaseResponse result =
+ limitProfileService.updateLimitProfile(
+ limitProfileCode,
+ request
+ );
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("success", result.getResponseMessage());
+ assertEquals("0", result.getResponseCode());
+
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "LP001",
+ result.getResponseBody().getLimitProfileCode()
+ );
+
+ assertEquals(
+ "Updated Profile",
+ result.getResponseBody().getLimitProfileName()
+ );
+
+ assertEquals(
+ new BigDecimal("200000.00"),
+ result.getResponseBody().getSingleTransactionValue()
+ );
+
+ assertEquals(
+ 20,
+ result.getResponseBody().getDailyTransactionVolume()
+ );
+
+ assertEquals(
+ new BigDecimal("1000000.00"),
+ result.getResponseBody().getDailyTransactionValue()
+ );
+
+ verify(
+ 1,
+ putRequestedFor(
+ urlPathEqualTo("/api/v1/limit-profile/LP001")
+ ).withRequestBody(
+ equalToJson(requestBody)
+ )
+ );
+ }
+
+ @Test
+ void shouldEncodeLimitProfileCodeInUrlPath() {
+
+ String limitProfileCode = "LP 001";
+
+ stubFor(
+ put(urlPathEqualTo("/api/v1/limit-profile/LP+001"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"limitProfileCode\":\"LP 001\","
+ + "\"limitProfileName\":\"Test Profile\","
+ + "\"singleTransactionValue\":100000.00,"
+ + "\"dailyTransactionVolume\":10,"
+ + "\"dailyTransactionValue\":500000.00,"
+ + "\"dateCreated\":\"2026-08-14T10:00:00\","
+ + "\"lastModified\":\"2026-08-14T10:00:00\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ LimitProfileRequest request =
+ LimitProfileRequest.builder()
+ .limitProfileName("Test Profile")
+ .singleTransactionValue(new BigDecimal("100000.00"))
+ .dailyTransactionVolume(10)
+ .dailyTransactionValue(new BigDecimal("500000.00"))
+ .build();
+
+ MonnifyBaseResponse result =
+ limitProfileService.updateLimitProfile(
+ limitProfileCode,
+ request
+ );
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "LP 001",
+ result.getResponseBody().getLimitProfileCode()
+ );
+
+ verify(
+ 1,
+ putRequestedFor(
+ urlPathEqualTo("/api/v1/limit-profile/LP+001")
+ )
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenLimitProfileCodeIsEmpty() {
+
+ LimitProfileRequest request =
+ LimitProfileRequest.builder()
+ .limitProfileName("Test Profile")
+ .singleTransactionValue(new BigDecimal("100000.00"))
+ .dailyTransactionVolume(10)
+ .dailyTransactionValue(new BigDecimal("500000.00"))
+ .build();
+
+ MonnifyValidationException exception =
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> limitProfileService.updateLimitProfile(
+ "",
+ request
+ )
+ );
+
+ assertEquals(
+ "Limit profile code is empty",
+ exception.getMessage()
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenLimitProfileCodeIsNull() {
+
+ LimitProfileRequest request =
+ LimitProfileRequest.builder()
+ .limitProfileName("Test Profile")
+ .singleTransactionValue(new BigDecimal("100000.00"))
+ .dailyTransactionVolume(10)
+ .dailyTransactionValue(new BigDecimal("500000.00"))
+ .build();
+
+ MonnifyValidationException exception =
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> limitProfileService.updateLimitProfile(
+ null,
+ request
+ )
+ );
+
+ assertEquals(
+ "Limit profile code is empty",
+ exception.getMessage()
+ );
+ }
+
+ @Test
+ void shouldValidateLimitProfileName() {
+
+ LimitProfileRequest request =
+ LimitProfileRequest.builder()
+ .limitProfileName("")
+ .singleTransactionValue(new BigDecimal("100000.00"))
+ .dailyTransactionVolume(10)
+ .dailyTransactionValue(new BigDecimal("500000.00"))
+ .build();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> limitProfileService.createLimitProfile(request)
+ );
+ }
+
+ @Test
+ void shouldValidateSingleTransactionValue() {
+
+ LimitProfileRequest request =
+ LimitProfileRequest.builder()
+ .limitProfileName("Test Profile")
+ .singleTransactionValue(null)
+ .dailyTransactionVolume(10)
+ .dailyTransactionValue(new BigDecimal("500000.00"))
+ .build();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> limitProfileService.createLimitProfile(request)
+ );
+ }
+
+ @Test
+ void shouldValidateDailyTransactionVolume() {
+
+ LimitProfileRequest request =
+ LimitProfileRequest.builder()
+ .limitProfileName("Test Profile")
+ .singleTransactionValue(new BigDecimal("100000.00"))
+ .dailyTransactionVolume(null)
+ .dailyTransactionValue(new BigDecimal("500000.00"))
+ .build();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> limitProfileService.createLimitProfile(request)
+ );
+ }
+
+ @Test
+ void shouldValidateDailyTransactionValue() {
+
+ LimitProfileRequest request =
+ LimitProfileRequest.builder()
+ .limitProfileName("Test Profile")
+ .singleTransactionValue(new BigDecimal("100000.00"))
+ .dailyTransactionVolume(10)
+ .dailyTransactionValue(null)
+ .build();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> limitProfileService.createLimitProfile(request)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/PaycodeServiceMockTest.java b/src/test/java/com/monnify/mocktests/PaycodeServiceMockTest.java
new file mode 100644
index 0000000..3061027
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/PaycodeServiceMockTest.java
@@ -0,0 +1,320 @@
+package com.monnify.mocktests;
+
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.exceptions.MonnifyValidationException;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.SearchResponse;
+import com.monnify.models.paycode.PaycodeRequest;
+import com.monnify.models.paycode.PaycodeResponse;
+import com.monnify.services.paycode.PaycodeService;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class PaycodeServiceMockTest extends BaseMonnifyMockTest {
+
+ private final PaycodeService paycodeService = new PaycodeService();
+
+ @Test
+ void shouldCreatePaycode() {
+ stubFor(post(urlEqualTo("/api/v1/paycode"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"paycode\":\"123456\","
+ + "\"transactionReference\":\"MNFY|123456\","
+ + "\"paycodeReference\":\"PAY-123\","
+ + "\"beneficiaryName\":\"John Doe\","
+ + "\"amount\":1000,"
+ + "\"fee\":10,"
+ + "\"transactionStatus\":\"PENDING\","
+ + "\"expiryDate\":\"2026-08-20T12:00:00\","
+ + "\"createdOn\":\"2026-08-14T10:00:00\","
+ + "\"createdBy\":\"test\","
+ + "\"modifiedBy\":\"test\""
+ + "}"
+ + "}")));
+
+ PaycodeRequest request = PaycodeRequest.builder()
+ .beneficiaryName("John Doe")
+ .amount(BigDecimal.valueOf(1000))
+ .paycodeReference("PAY-123")
+ .expiryDate("2026-08-20")
+ .clientId("client-123")
+ .build();
+
+ MonnifyBaseResponse response =
+ paycodeService.createPaycode(request);
+
+ assertTrue(response.isRequestSuccessful());
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertEquals("123456", response.getResponseBody().getPaycode());
+ assertEquals("PAY-123", response.getResponseBody().getPaycodeReference());
+ assertEquals("John Doe", response.getResponseBody().getBeneficiaryName());
+ assertEquals(BigDecimal.valueOf(1000), response.getResponseBody().getAmount());
+ }
+
+ @Test
+ void shouldRejectInvalidPaycodeRequest() {
+ PaycodeRequest request = PaycodeRequest.builder().build();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> paycodeService.createPaycode(request)
+ );
+ }
+
+ @Test
+ void shouldGetPaycode() {
+ stubFor(get(urlEqualTo("/api/v1/paycode/PAY-123"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"paycode\":\"123456\","
+ + "\"transactionReference\":\"MNFY|123456\","
+ + "\"paycodeReference\":\"PAY-123\","
+ + "\"beneficiaryName\":\"John Doe\","
+ + "\"amount\":1000,"
+ + "\"fee\":10,"
+ + "\"transactionStatus\":\"PENDING\""
+ + "}"
+ + "}")));
+
+ MonnifyBaseResponse response =
+ paycodeService.getPaycode("PAY-123");
+
+ assertTrue(response.isRequestSuccessful());
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertEquals("123456", response.getResponseBody().getPaycode());
+ assertEquals("PAY-123", response.getResponseBody().getPaycodeReference());
+ }
+
+ @Test
+ void shouldEncodePaycodeReferenceInUrlPath() {
+ stubFor(get(urlEqualTo("/api/v1/paycode/PAY%2B123"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"paycode\":\"123456\","
+ + "\"paycodeReference\":\"PAY+123\","
+ + "\"beneficiaryName\":\"John Doe\","
+ + "\"amount\":1000,"
+ + "\"transactionStatus\":\"PENDING\""
+ + "}"
+ + "}")));
+
+ MonnifyBaseResponse response =
+ paycodeService.getPaycode("PAY+123");
+
+ assertTrue(response.isRequestSuccessful());
+ assertNotNull(response.getResponseBody());
+ assertEquals("PAY+123", response.getResponseBody().getPaycodeReference());
+
+ verify(getRequestedFor(urlEqualTo("/api/v1/paycode/PAY%2B123")));
+ }
+
+ @Test
+ void shouldRejectEmptyPaycodeReferenceWhenGettingPaycode() {
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> paycodeService.getPaycode("")
+ );
+ }
+
+ @Test
+ void shouldRejectNullPaycodeReferenceWhenGettingPaycode() {
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> paycodeService.getPaycode(null)
+ );
+ }
+
+ @Test
+ void shouldGetClearPaycode() {
+ stubFor(get(urlEqualTo("/api/v1/paycode/PAY-123/authorize"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"paycode\":\"123456\","
+ + "\"paycodeReference\":\"PAY-123\","
+ + "\"beneficiaryName\":\"John Doe\","
+ + "\"amount\":1000,"
+ + "\"transactionStatus\":\"AUTHORIZED\""
+ + "}"
+ + "}")));
+
+ MonnifyBaseResponse response =
+ paycodeService.getClearPaycode("PAY-123");
+
+ assertTrue(response.isRequestSuccessful());
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertEquals("123456", response.getResponseBody().getPaycode());
+ assertEquals("AUTHORIZED", response.getResponseBody().getTransactionStatus());
+ }
+
+ @Test
+ void shouldRejectEmptyPaycodeReferenceWhenClearingPaycode() {
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> paycodeService.getClearPaycode("")
+ );
+ }
+
+ @Test
+ void shouldFetchPaycodes() {
+ stubFor(get(urlPathEqualTo("/api/v1/paycode"))
+ .withQueryParam("transactionReference", equalTo("MNFY|123456"))
+ .withQueryParam("beneficiaryName", equalTo("John Doe"))
+ .withQueryParam("transactionStatus", equalTo("PENDING"))
+ .withQueryParam("from", equalTo("100"))
+ .withQueryParam("to", equalTo("200"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"content\":[{"
+ + "\"paycode\":\"123456\","
+ + "\"transactionReference\":\"MNFY|123456\","
+ + "\"paycodeReference\":\"PAY-123\","
+ + "\"beneficiaryName\":\"John Doe\","
+ + "\"amount\":1000,"
+ + "\"fee\":10,"
+ + "\"transactionStatus\":\"PENDING\""
+ + "}],"
+ + "\"pageNumber\":0,"
+ + "\"pageSize\":10,"
+ + "\"totalElements\":1,"
+ + "\"totalPages\":1"
+ + "}"
+ + "}")));
+
+ MonnifyBaseResponse> response =
+ paycodeService.fetchPaycodes(
+ "MNFY|123456",
+ "John Doe",
+ "PENDING",
+ 100,
+ 200
+ );
+
+ assertTrue(response.isRequestSuccessful());
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertNotNull(response.getResponseBody().getContent());
+ assertEquals(1, response.getResponseBody().getContent().size());
+ assertEquals(
+ "PAY-123",
+ response.getResponseBody().getContent().get(0).getPaycodeReference()
+ );
+ }
+
+ @Test
+ void shouldFetchPaycodesWithoutOptionalParameters() {
+ stubFor(get(urlPathEqualTo("/api/v1/paycode"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"content\":[],"
+ + "\"pageNumber\":0,"
+ + "\"pageSize\":10,"
+ + "\"totalElements\":0,"
+ + "\"totalPages\":0"
+ + "}"
+ + "}")));
+
+ MonnifyBaseResponse> response =
+ paycodeService.fetchPaycodes(null, null, null, 0, 0);
+
+ assertTrue(response.isRequestSuccessful());
+ assertNotNull(response.getResponseBody());
+ assertNotNull(response.getResponseBody().getContent());
+ assertTrue(response.getResponseBody().getContent().isEmpty());
+ }
+
+ @Test
+ void shouldDeletePaycode() {
+ stubFor(delete(urlEqualTo("/api/v1/paycode/PAY-123"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"paycode\":\"123456\","
+ + "\"paycodeReference\":\"PAY-123\","
+ + "\"beneficiaryName\":\"John Doe\","
+ + "\"amount\":1000,"
+ + "\"transactionStatus\":\"DELETED\""
+ + "}"
+ + "}")));
+
+ MonnifyBaseResponse response =
+ paycodeService.deletePaycode("PAY-123");
+
+ assertTrue(response.isRequestSuccessful());
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertEquals("PAY-123", response.getResponseBody().getPaycodeReference());
+ assertEquals("DELETED", response.getResponseBody().getTransactionStatus());
+ }
+
+ @Test
+ void shouldRejectEmptyPaycodeReferenceWhenDeletingPaycode() {
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> paycodeService.deletePaycode("")
+ );
+ }
+
+ @Test
+ void shouldRejectNullPaycodeReferenceWhenDeletingPaycode() {
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> paycodeService.deletePaycode(null)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/RefundServiceMockTest.java b/src/test/java/com/monnify/mocktests/RefundServiceMockTest.java
new file mode 100644
index 0000000..5908862
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/RefundServiceMockTest.java
@@ -0,0 +1,259 @@
+package com.monnify.mocktests;
+
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.SearchResponse;
+import com.monnify.models.refund.RefundRequest;
+import com.monnify.models.refund.RefundResponse;
+import com.monnify.services.refund.RefundService;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+class RefundServiceMockTest extends BaseMonnifyMockTest {
+
+ private final RefundService refundService = new RefundService();
+
+ @Test
+ void shouldInitiateRefund() {
+
+ stubFor(
+ post(urlPathEqualTo("/api/v1/refunds/initiate-refund"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"Refund initiated successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"refundReference\":\"REF-123456\","
+ + "\"transactionReference\":\"TXN-123456\","
+ + "\"refundAmount\":1000.00,"
+ + "\"refundStatus\":\"PENDING\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ RefundRequest request = RefundRequest.builder()
+ .transactionReference("TXN-123456")
+ .refundReference("REF-123456")
+ .refundAmount(new BigDecimal("1000.00"))
+ .refundReason("Customer requested refund")
+ .customerNote("Refund for failed transaction")
+ .build();
+
+ MonnifyBaseResponse response =
+ refundService.initiateRefund(request);
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertEquals("REF-123456",
+ response.getResponseBody().getRefundReference());
+ assertEquals("TXN-123456",
+ response.getResponseBody().getTransactionReference());
+
+ verify(
+ postRequestedFor(
+ urlPathEqualTo("/api/v1/refunds/initiate-refund")
+ )
+ );
+ }
+
+ @Test
+ void shouldGetRefundStatus() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v1/refunds/REF-123456"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"Refund status retrieved successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"refundReference\":\"REF-123456\","
+ + "\"transactionReference\":\"TXN-123456\","
+ + "\"refundAmount\":1000.00,"
+ + "\"refundStatus\":\"COMPLETED\""
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse response =
+ refundService.getRefundStatus("REF-123456");
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertEquals("REF-123456",
+ response.getResponseBody().getRefundReference());
+ assertEquals("COMPLETED",
+ response.getResponseBody().getRefundStatus());
+
+ verify(
+ getRequestedFor(
+ urlPathEqualTo("/api/v1/refunds/REF-123456")
+ )
+ );
+ }
+
+ @Test
+ void shouldGetRefunds() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v1/refunds"))
+ .withQueryParam("page", equalTo("0"))
+ .withQueryParam("size", equalTo("10"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"Refunds retrieved successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"content\":["
+ + "{"
+ + "\"refundReference\":\"REF-123456\","
+ + "\"transactionReference\":\"TXN-123456\","
+ + "\"refundAmount\":1000.00,"
+ + "\"refundStatus\":\"COMPLETED\""
+ + "}"
+ + "],"
+ + "\"pageSize\":10,"
+ + "\"pageNumber\":0,"
+ + "\"totalPages\":1,"
+ + "\"totalElements\":1"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> response =
+ refundService.getRefunds(0, 10);
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertNotNull(response.getResponseBody().getContent());
+ assertEquals(1, response.getResponseBody().getContent().size());
+
+ RefundResponse refund =
+ response.getResponseBody().getContent().get(0);
+
+ assertEquals("REF-123456", refund.getRefundReference());
+ assertEquals("TXN-123456", refund.getTransactionReference());
+ assertEquals(new BigDecimal("1000.00"), refund.getRefundAmount());
+ assertEquals("COMPLETED", refund.getRefundStatus());
+
+ verify(
+ getRequestedFor(
+ urlPathEqualTo("/api/v1/refunds")
+ )
+ .withQueryParam("page", equalTo("0"))
+ .withQueryParam("size", equalTo("10"))
+ );
+ }
+
+ @Test
+ void shouldGetRefundsWithoutPagination() {
+
+ stubFor(
+ get(urlPathEqualTo("/api/v1/refunds"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"Refunds retrieved successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"content\":[],"
+ + "\"pageSize\":0,"
+ + "\"pageNumber\":0,"
+ + "\"totalPages\":0,"
+ + "\"totalElements\":0"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> response =
+ refundService.getRefunds(null, null);
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertNotNull(response.getResponseBody());
+ assertNotNull(response.getResponseBody().getContent());
+ assertTrue(response.getResponseBody().getContent().isEmpty());
+
+ verify(
+ getRequestedFor(
+ urlPathEqualTo("/api/v1/refunds")
+ )
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenRefundReferenceIsEmpty() {
+
+ assertThrows(
+ Exception.class,
+ () -> refundService.getRefundStatus("")
+ );
+
+ verify(
+ 0,
+ getRequestedFor(
+ urlPathMatching("/api/v1/refunds/.*")
+ )
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenRefundRequestIsInvalid() {
+
+ RefundRequest request = RefundRequest.builder()
+ .transactionReference("")
+ .refundReference("")
+ .refundAmount(null)
+ .refundReason("")
+ .customerNote("")
+ .build();
+
+ assertThrows(
+ Exception.class,
+ () -> refundService.initiateRefund(request)
+ );
+
+ verify(
+ 0,
+ postRequestedFor(
+ urlPathEqualTo("/api/v1/refunds/initiate-refund")
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/ReservedAccountServiceTest.java b/src/test/java/com/monnify/mocktests/ReservedAccountServiceTest.java
new file mode 100644
index 0000000..db41445
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/ReservedAccountServiceTest.java
@@ -0,0 +1,462 @@
+package com.monnify.mocktests;
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.exceptions.MonnifyValidationException;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.SearchResponse;
+import com.monnify.models.account.*;
+import com.monnify.models.transaction.TransactionRecord;
+import com.monnify.services.reservedaccount.ReservedAccountService;
+import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ReservedAccountServiceTest extends BaseMonnifyMockTest {
+
+ private final ReservedAccountService reservedAccountService =
+ new ReservedAccountService();
+
+ @Test
+ void shouldCreateReservedAccount() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"Reserved account created successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"contractCode\":\"MK_TEST\","
+ + "\"accountReference\":\"reserved-account-001\","
+ + "\"accountName\":\"Test Customer\","
+ + "\"currencyCode\":\"NGN\","
+ + "\"customerEmail\":\"test@example.com\","
+ + "\"customerName\":\"Test Customer\","
+ + "\"status\":\"ACTIVE\""
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo("/api/v2/bank-transfer/reserved-accounts"))
+ .willReturn(okJson(response)));
+
+ ReservedAccountRequest request = new ReservedAccountRequest();
+ request.setAccountReference("reserved-account-001");
+ request.setAccountName("Test Customer");
+ request.setCurrencyCode("NGN");
+ request.setContractCode("MK_TEST");
+ request.setCustomerEmail("test@example.com");
+ request.setCustomerName("Test Customer");
+ request.setGetAllAvailableBanks(true);
+
+ MonnifyBaseResponse result =
+ reservedAccountService.createReservedAccount(request);
+
+ assertNotNull(result);
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+ assertEquals(
+ "reserved-account-001",
+ result.getResponseBody().getAccountReference()
+ );
+
+ verify(postRequestedFor(
+ urlEqualTo("/api/v2/bank-transfer/reserved-accounts")
+ ));
+ }
+
+ @Test
+ void shouldRejectReservedAccountWhenPreferredBanksAreRequired() {
+
+ ReservedAccountRequest request = new ReservedAccountRequest();
+ request.setAccountReference("reserved-account-001");
+ request.setAccountName("Test Customer");
+ request.setCurrencyCode("NGN");
+ request.setContractCode("MK_TEST");
+ request.setCustomerEmail("test@example.com");
+ request.setCustomerName("Test Customer");
+ request.setGetAllAvailableBanks(false);
+ request.setPreferredBanks(new ArrayList());
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> reservedAccountService.createReservedAccount(request)
+ );
+ }
+
+ @Test
+ void shouldCreateInvoiceReservedAccount() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"Reserved account created successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"accountReference\":\"invoice-account-001\","
+ + "\"accountName\":\"Invoice Customer\","
+ + "\"currencyCode\":\"NGN\","
+ + "\"status\":\"ACTIVE\""
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo("/api/v1/bank-transfer/reserved-accounts"))
+ .willReturn(okJson(response)));
+
+ ReservedAccountRequest request = new ReservedAccountRequest();
+ request.setAccountReference("invoice-account-001");
+ request.setAccountName("Invoice Customer");
+ request.setCurrencyCode("NGN");
+ request.setContractCode("MK_TEST");
+ request.setCustomerEmail("invoice@example.com");
+ request.setCustomerName("Invoice Customer");
+ request.setGetAllAvailableBanks(true);
+
+ MonnifyBaseResponse result =
+ reservedAccountService.createReservedAccountInvoice(request);
+
+ assertNotNull(result);
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+ assertEquals(
+ "invoice-account-001",
+ result.getResponseBody().getAccountReference()
+ );
+
+ verify(postRequestedFor(
+ urlEqualTo("/api/v1/bank-transfer/reserved-accounts")
+ ));
+ }
+
+ @Test
+ void shouldCreateReservedAccountWithLimit() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"Reserved account created successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"accountReference\":\"limit-account-001\","
+ + "\"accountName\":\"Limit Customer\","
+ + "\"currencyCode\":\"NGN\","
+ + "\"status\":\"ACTIVE\""
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo(
+ "/api/v2/bank-transfer/reserved-accounts/limit"
+ )).willReturn(okJson(response)));
+
+ ReservedAccountRequest request = new ReservedAccountRequest();
+ request.setAccountReference("limit-account-001");
+ request.setAccountName("Limit Customer");
+ request.setCurrencyCode("NGN");
+ request.setContractCode("MK_TEST");
+ request.setCustomerEmail("limit@example.com");
+ request.setCustomerName("Limit Customer");
+ request.setLimitProfileCode("LIMIT_PROFILE_001");
+ request.setGetAllAvailableBanks(true);
+
+ MonnifyBaseResponse result =
+ reservedAccountService.createReservedAccountWithLimit(request);
+
+ assertNotNull(result);
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+ assertEquals(
+ "limit-account-001",
+ result.getResponseBody().getAccountReference()
+ );
+
+ verify(postRequestedFor(
+ urlEqualTo("/api/v2/bank-transfer/reserved-accounts/limit")
+ ));
+ }
+
+ @Test
+ void shouldUpdateReservedAccountLimit() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"Limit profile updated successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"accountReference\":\"reserved-account-001\","
+ + "\"status\":\"ACTIVE\""
+ + "}"
+ + "}";
+
+ stubFor(put(urlEqualTo(
+ "/api/v2/bank-transfer/reserved-accounts/limit"
+ )).willReturn(okJson(response)));
+
+ UpdateReservedAccountLimitRequest request =
+ new UpdateReservedAccountLimitRequest();
+
+ request.setAccountReference("reserved-account-001");
+ request.setLimitProfileCode("LIMIT_PROFILE_001");
+
+ MonnifyBaseResponse result =
+ reservedAccountService.updateReservedAccountLimit(request);
+
+ assertNotNull(result);
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+ assertEquals(
+ "reserved-account-001",
+ result.getResponseBody().getAccountReference()
+ );
+
+ verify(putRequestedFor(
+ urlEqualTo("/api/v2/bank-transfer/reserved-accounts/limit")
+ ));
+ }
+
+ @Test
+ void shouldRejectUpdateReservedAccountLimitWithMissingAccountReference() {
+
+ UpdateReservedAccountLimitRequest request =
+ new UpdateReservedAccountLimitRequest();
+
+ request.setLimitProfileCode("LIMIT_PROFILE_001");
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> reservedAccountService.updateReservedAccountLimit(request)
+ );
+ }
+
+ @Test
+ void shouldGetReservedAccountDetails() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"Reserved account retrieved successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"accountReference\":\"reserved-account-001\","
+ + "\"accountName\":\"Test Customer\","
+ + "\"currencyCode\":\"NGN\","
+ + "\"customerEmail\":\"test@example.com\","
+ + "\"status\":\"ACTIVE\""
+ + "}"
+ + "}";
+
+ stubFor(get(urlEqualTo(
+ "/api/v2/bank-transfer/reserved-accounts/reserved-account-001"
+ )).willReturn(okJson(response)));
+
+ MonnifyBaseResponse result =
+ reservedAccountService.getReservedAccountDetails(
+ "reserved-account-001"
+ );
+
+ assertNotNull(result);
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+ assertEquals(
+ "reserved-account-001",
+ result.getResponseBody().getAccountReference()
+ );
+
+ verify(getRequestedFor(
+ urlEqualTo(
+ "/api/v2/bank-transfer/reserved-accounts/reserved-account-001"
+ )
+ ));
+ }
+
+ @Test
+ void shouldRejectGetReservedAccountDetailsWithoutReference() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> reservedAccountService.getReservedAccountDetails("")
+ );
+ }
+
+ @Test
+ void shouldDeallocateReservedAccount() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"Reserved account deallocated successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"accountReference\":\"reserved-account-001\","
+ + "\"status\":\"DEALLOCATED\""
+ + "}"
+ + "}";
+
+ stubFor(delete(urlEqualTo(
+ "/api/v1/bank-transfer/reserved-accounts/reference/reserved-account-001"
+ )).willReturn(okJson(response)));
+
+ MonnifyBaseResponse result =
+ reservedAccountService.deallocateReservedAccount(
+ "reserved-account-001"
+ );
+
+ assertNotNull(result);
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+ assertEquals(
+ "reserved-account-001",
+ result.getResponseBody().getAccountReference()
+ );
+
+ verify(deleteRequestedFor(
+ urlEqualTo(
+ "/api/v1/bank-transfer/reserved-accounts/reference/reserved-account-001"
+ )
+ ));
+ }
+
+ @Test
+ void shouldRejectDeallocateReservedAccountWithoutReference() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> reservedAccountService.deallocateReservedAccount(null)
+ );
+ }
+
+ @Test
+ void shouldUpdateKycInfoWithBvn() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"KYC information updated successfully\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"accountReference\":\"reserved-account-001\","
+ + "\"accountName\":\"Test Customer\","
+ + "\"customerEmail\":\"test@example.com\","
+ + "\"customerName\":\"Test Customer\","
+ + "\"bvn\":\"12345678901\""
+ + "}"
+ + "}";
+
+ stubFor(put(urlEqualTo(
+ "/api/v1/bank-transfer/reserved-accounts/"
+ + "reserved-account-001/kyc-info"
+ )).willReturn(okJson(response)));
+
+ KycInfoRequest request = new KycInfoRequest();
+ request.setBvn("12345678901");
+
+ MonnifyBaseResponse result =
+ reservedAccountService.updateKycInfo(
+ request,
+ "reserved-account-001"
+ );
+
+ assertNotNull(result);
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+ assertEquals(
+ "reserved-account-001",
+ result.getResponseBody().getAccountReference()
+ );
+ assertEquals(
+ "12345678901",
+ result.getResponseBody().getBvn()
+ );
+
+ verify(putRequestedFor(
+ urlEqualTo(
+ "/api/v1/bank-transfer/reserved-accounts/"
+ + "reserved-account-001/kyc-info"
+ )
+ ));
+ }
+
+ @Test
+ void shouldRejectKycUpdateWithoutBvnOrNin() {
+
+ KycInfoRequest request = new KycInfoRequest();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> reservedAccountService.updateKycInfo(
+ request,
+ "reserved-account-001"
+ )
+ );
+ }
+
+ @Test
+ void shouldRejectKycUpdateWithoutAccountReference() {
+
+ KycInfoRequest request = new KycInfoRequest();
+ request.setBvn("12345678901");
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> reservedAccountService.updateKycInfo(
+ request,
+ ""
+ )
+ );
+ }
+
+ @Test
+ void shouldGetReservedAccountTransactions() {
+
+ stubFor(get(urlPathEqualTo(
+ "/api/v1/bank-transfer/reserved-accounts/transactions"))
+ .withQueryParam("accountReference",
+ equalTo("reserved-account-001"))
+ .withQueryParam("page",
+ equalTo("0"))
+ .withQueryParam("size",
+ equalTo("10"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{\n" +
+ " \"requestSuccessful\": true,\n" +
+ " \"responseMessage\": \"success\",\n" +
+ " \"responseCode\": \"0\",\n" +
+ " \"responseBody\": {\n" +
+ " \"content\": [],\n" +
+ " \"pageable\": {\n" +
+ " \"pageNumber\": 0,\n" +
+ " \"pageSize\": 10\n" +
+ " },\n" +
+ " \"totalElements\": 0,\n" +
+ " \"totalPages\": 0,\n" +
+ " \"last\": true,\n" +
+ " \"first\": true,\n" +
+ " \"numberOfElements\": 0,\n" +
+ " \"size\": 10,\n" +
+ " \"number\": 0\n" +
+ " }\n" +
+ "}")));
+
+ MonnifyBaseResponse> response =
+ reservedAccountService.getReservedAccountTransactions(
+ "reserved-account-001",
+ 0,
+ 10);
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertEquals("success", response.getResponseMessage());
+ assertEquals("0", response.getResponseCode());
+ assertNotNull(response.getResponseBody());
+ assertNotNull(response.getResponseBody().getContent());
+ assertTrue(response.getResponseBody().getContent().isEmpty());
+ }
+
+ @Test
+ void shouldRejectReservedAccountTransactionsWithoutReference() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> reservedAccountService.getReservedAccountTransactions(
+ "",
+ 0,
+ 10
+ )
+ );
+ }
+}
diff --git a/src/test/java/com/monnify/mocktests/SettlementServiceMockTest.java b/src/test/java/com/monnify/mocktests/SettlementServiceMockTest.java
new file mode 100644
index 0000000..5d114b3
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/SettlementServiceMockTest.java
@@ -0,0 +1,410 @@
+package com.monnify.mocktests;
+
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.exceptions.MonnifyValidationException;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.SearchResponse;
+import com.monnify.models.settlement.SettlementResponse;
+import com.monnify.models.transaction.TransactionStatusResponse;
+import com.monnify.services.settlement.SettlementService;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Mock tests for {@link SettlementService}.
+ */
+public class SettlementServiceMockTest extends BaseMonnifyMockTest {
+
+ private final SettlementService settlementService = new SettlementService();
+
+ @Test
+ void shouldGetTransactionsBySettlementReference() {
+
+ stubFor(get(urlPathEqualTo(
+ "/api/v1/transactions/find-by-settlement-reference"
+ ))
+ .withQueryParam(
+ "reference",
+ equalTo("settlement-reference-001")
+ )
+ .withQueryParam(
+ "page",
+ equalTo("0")
+ )
+ .withQueryParam(
+ "size",
+ equalTo("10")
+ )
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json"
+ )
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"content\":["
+ + "{"
+ + "\"transactionReference\":\"MNFY|123456\","
+ + "\"paymentReference\":\"PAY|123456\","
+ + "\"amountPaid\":10000.00,"
+ + "\"totalPayable\":10000.00,"
+ + "\"settlementAmount\":9900.00,"
+ + "\"paymentStatus\":\"PAID\","
+ + "\"paymentDescription\":\"Test settlement\","
+ + "\"currency\":\"NGN\","
+ + "\"paymentMethod\":\"ACCOUNT_TRANSFER\""
+ + "}"
+ + "],"
+ + "\"pageable\":{"
+ + "\"pageNumber\":0,"
+ + "\"pageSize\":10"
+ + "},"
+ + "\"totalElements\":1,"
+ + "\"totalPages\":1"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse> response =
+ settlementService.getTransactionsBySettlementReference(
+ "settlement-reference-001",
+ 0,
+ 10
+ );
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertNotNull(response.getResponseBody());
+
+ assertEquals(
+ 1,
+ response.getResponseBody().getContent().size()
+ );
+
+ TransactionStatusResponse transaction =
+ response.getResponseBody()
+ .getContent()
+ .get(0);
+
+ assertEquals(
+ "MNFY|123456",
+ transaction.getTransactionReference()
+ );
+
+ assertEquals(
+ "PAY|123456",
+ transaction.getPaymentReference()
+ );
+
+ assertEquals(
+ new BigDecimal("10000.00"),
+ transaction.getAmountPaid()
+ );
+
+ assertEquals(
+ new BigDecimal("9900.00"),
+ transaction.getSettlementAmount()
+ );
+
+ assertEquals(
+ "PAID",
+ transaction.getPaymentStatus()
+ );
+
+ assertEquals(
+ "NGN",
+ transaction.getCurrency()
+ );
+
+ verify(
+ getRequestedFor(
+ urlPathEqualTo(
+ "/api/v1/transactions/find-by-settlement-reference"
+ )
+ )
+ .withQueryParam(
+ "reference",
+ equalTo("settlement-reference-001")
+ )
+ .withQueryParam(
+ "page",
+ equalTo("0")
+ )
+ .withQueryParam(
+ "size",
+ equalTo("10")
+ )
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenReferenceIsNull() {
+
+ MonnifyValidationException exception =
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> settlementService
+ .getTransactionsBySettlementReference(
+ null,
+ 0,
+ 10
+ )
+ );
+
+ assertEquals(
+ "Reference is required, size must be greater than 0",
+ exception.getMessage()
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenReferenceIsEmpty() {
+
+ MonnifyValidationException exception =
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> settlementService
+ .getTransactionsBySettlementReference(
+ "",
+ 0,
+ 10
+ )
+ );
+
+ assertEquals(
+ "Reference is required, size must be greater than 0",
+ exception.getMessage()
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenSizeIsZero() {
+
+ MonnifyValidationException exception =
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> settlementService
+ .getTransactionsBySettlementReference(
+ "settlement-reference-001",
+ 0,
+ 0
+ )
+ );
+
+ assertEquals(
+ "Reference is required, size must be greater than 0",
+ exception.getMessage()
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenSizeIsNegative() {
+
+ MonnifyValidationException exception =
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> settlementService
+ .getTransactionsBySettlementReference(
+ "settlement-reference-001",
+ 0,
+ -1
+ )
+ );
+
+ assertEquals(
+ "Reference is required, size must be greater than 0",
+ exception.getMessage()
+ );
+ }
+
+ @Test
+ void shouldGetSettlementInformation() {
+
+ stubFor(get(urlPathEqualTo(
+ "/api/v1/settlement-detail"
+ ))
+ .withQueryParam(
+ "transactionReference",
+ equalTo("MNFY|123456")
+ )
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json"
+ )
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"transaction\":{"
+ + "\"transactionReference\":\"MNFY|123456\","
+ + "\"paymentReference\":\"PAY|123456\","
+ + "\"amountPaid\":10000.00,"
+ + "\"totalPayable\":10000.00,"
+ + "\"settlementAmount\":9900.00,"
+ + "\"paymentStatus\":\"PAID\","
+ + "\"paymentDescription\":\"Test payment\","
+ + "\"transactionHash\":\"transaction-hash-001\","
+ + "\"currency\":\"NGN\","
+ + "\"paymentMethod\":\"ACCOUNT_TRANSFER\""
+ + "},"
+ + "\"beneficiaries\":[]"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse response =
+ settlementService.getSettlementInformation(
+ "MNFY|123456"
+ );
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertNotNull(response.getResponseBody());
+
+ SettlementResponse settlement =
+ response.getResponseBody();
+
+ assertNotNull(settlement.getTransaction());
+
+ TransactionStatusResponse transaction =
+ settlement.getTransaction();
+
+ assertEquals(
+ "MNFY|123456",
+ transaction.getTransactionReference()
+ );
+
+ assertEquals(
+ "PAY|123456",
+ transaction.getPaymentReference()
+ );
+
+ assertEquals(
+ new BigDecimal("10000.00"),
+ transaction.getAmountPaid()
+ );
+
+ assertEquals(
+ new BigDecimal("10000.00"),
+ transaction.getTotalPayable()
+ );
+
+ assertEquals(
+ new BigDecimal("9900.00"),
+ transaction.getSettlementAmount()
+ );
+
+ assertEquals(
+ "PAID",
+ transaction.getPaymentStatus()
+ );
+
+ assertEquals(
+ "NGN",
+ transaction.getCurrency()
+ );
+
+ assertEquals(
+ "ACCOUNT_TRANSFER",
+ transaction.getPaymentMethod()
+ );
+
+ assertNotNull(
+ settlement.getBeneficiaries()
+ );
+
+ assertTrue(
+ settlement.getBeneficiaries().isEmpty()
+ );
+
+ verify(
+ getRequestedFor(
+ urlPathEqualTo(
+ "/api/v1/settlement-detail"
+ )
+ )
+ .withQueryParam(
+ "transactionReference",
+ equalTo("MNFY|123456")
+ )
+ );
+ }
+
+ @Test
+ void shouldGetSettlementInformationWithEncodedReference() {
+
+ String transactionReference =
+ "transaction/reference 001";
+
+ stubFor(get(urlPathEqualTo(
+ "/api/v1/settlement-detail"
+ ))
+ .withQueryParam(
+ "transactionReference",
+ equalTo(transactionReference)
+ )
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json"
+ )
+ .withBody(
+ "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"transaction\":{"
+ + "\"transactionReference\":\"transaction/reference 001\","
+ + "\"paymentReference\":\"PAY-001\","
+ + "\"amountPaid\":5000.00,"
+ + "\"totalPayable\":5000.00,"
+ + "\"settlementAmount\":4900.00,"
+ + "\"paymentStatus\":\"PAID\","
+ + "\"currency\":\"NGN\""
+ + "},"
+ + "\"beneficiaries\":[]"
+ + "}"
+ + "}"
+ )
+ )
+ );
+
+ MonnifyBaseResponse response =
+ settlementService.getSettlementInformation(
+ transactionReference
+ );
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertNotNull(response.getResponseBody());
+
+ assertEquals(
+ transactionReference,
+ response.getResponseBody()
+ .getTransaction()
+ .getTransactionReference()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/SubAccountServiceTest.java b/src/test/java/com/monnify/mocktests/SubAccountServiceTest.java
new file mode 100644
index 0000000..403f5c6
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/SubAccountServiceTest.java
@@ -0,0 +1,271 @@
+package com.monnify.mocktests;
+
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.exceptions.MonnifyValidationException;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.subaccount.SubAccountRequest;
+import com.monnify.models.subaccount.SubAccountResponse;
+import com.monnify.models.subaccount.UpdateSubAccountRequest;
+import com.monnify.services.subaccount.SubAccountService;
+import org.junit.jupiter.api.Test;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class SubAccountServiceTest extends BaseMonnifyMockTest {
+
+ private final SubAccountService subAccountService = new SubAccountService();
+
+ @Test
+ void shouldCreateSubAccounts() {
+
+ stubFor(post(urlEqualTo("/api/v1/sub-accounts"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{\n" +
+ " \"requestSuccessful\": true,\n" +
+ " \"responseMessage\": \"success\",\n" +
+ " \"responseCode\": \"0\",\n" +
+ " \"responseBody\": [\n" +
+ " {\n" +
+ " \"subAccountCode\": \"MFY_SUB_001\",\n" +
+ " \"accountNumber\": \"1234567890\",\n" +
+ " \"accountName\": \"Test Sub Account\",\n" +
+ " \"currencyCode\": \"NGN\",\n" +
+ " \"email\": \"test@example.com\",\n" +
+ " \"bankCode\": \"058\",\n" +
+ " \"bankName\": \"GTBank\",\n" +
+ " \"defaultSplitPercentage\": 50.0,\n" +
+ " \"settlementProfileCode\": \"SETTLEMENT_001\",\n" +
+ " \"settlementReportEmails\": [\n" +
+ " \"test@example.com\"\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ "}"
+ )));
+
+ SubAccountRequest request = SubAccountRequest.builder()
+ .accountNumber("1234567890")
+ .currencyCode("NGN")
+ .email("test@example.com")
+ .bankCode("058")
+ .defaultSplitPercentage(50.0)
+ .build();
+
+ List requests = Arrays.asList(request);
+
+ MonnifyBaseResponse> response =
+ subAccountService.createSubAccounts(requests);
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertNotNull(response.getResponseBody());
+ assertEquals(1, response.getResponseBody().size());
+
+ SubAccountResponse subAccount =
+ response.getResponseBody().get(0);
+
+ assertEquals("MFY_SUB_001", subAccount.getSubAccountCode());
+ assertEquals("1234567890", subAccount.getAccountNumber());
+ assertEquals("Test Sub Account", subAccount.getAccountName());
+ assertEquals("NGN", subAccount.getCurrencyCode());
+ assertEquals("test@example.com", subAccount.getEmail());
+ assertEquals("058", subAccount.getBankCode());
+ assertEquals("GTBank", subAccount.getBankName());
+ assertEquals(50.0, subAccount.getDefaultSplitPercentage());
+
+ verify(postRequestedFor(urlEqualTo("/api/v1/sub-accounts")));
+ }
+
+ @Test
+ void shouldDeleteSubAccount() {
+
+ stubFor(delete(urlEqualTo("/api/v1/sub-accounts/MFY_SUB_001"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{\n" +
+ " \"requestSuccessful\": true,\n" +
+ " \"responseMessage\": \"success\",\n" +
+ " \"responseCode\": \"0\",\n" +
+ " \"responseBody\": null\n" +
+ "}"
+ )));
+
+ MonnifyBaseResponse response =
+ subAccountService.deleteSubAccount("MFY_SUB_001");
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertEquals("0", response.getResponseCode());
+
+ verify(deleteRequestedFor(
+ urlEqualTo("/api/v1/sub-accounts/MFY_SUB_001")
+ ));
+ }
+
+ @Test
+ void shouldThrowExceptionWhenDeletingSubAccountWithEmptyCode() {
+
+ MonnifyValidationException exception =
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> subAccountService.deleteSubAccount("")
+ );
+
+ assertEquals("subAccountCode is empty", exception.getMessage());
+
+ verify(0, deleteRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void shouldThrowExceptionWhenDeletingSubAccountWithNullCode() {
+
+ MonnifyValidationException exception =
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> subAccountService.deleteSubAccount(null)
+ );
+
+ assertEquals("subAccountCode is empty", exception.getMessage());
+
+ verify(0, deleteRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void shouldGetSubAccounts() {
+
+ stubFor(get(urlEqualTo("/api/v1/sub-accounts"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{\n" +
+ " \"requestSuccessful\": true,\n" +
+ " \"responseMessage\": \"success\",\n" +
+ " \"responseCode\": \"0\",\n" +
+ " \"responseBody\": [\n" +
+ " {\n" +
+ " \"subAccountCode\": \"MFY_SUB_001\",\n" +
+ " \"accountNumber\": \"1234567890\",\n" +
+ " \"accountName\": \"Test Sub Account\",\n" +
+ " \"currencyCode\": \"NGN\",\n" +
+ " \"email\": \"test@example.com\",\n" +
+ " \"bankCode\": \"058\",\n" +
+ " \"bankName\": \"GTBank\",\n" +
+ " \"defaultSplitPercentage\": 50.0,\n" +
+ " \"settlementProfileCode\": \"SETTLEMENT_001\",\n" +
+ " \"settlementReportEmails\": [\n" +
+ " \"test@example.com\"\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"subAccountCode\": \"MFY_SUB_002\",\n" +
+ " \"accountNumber\": \"0987654321\",\n" +
+ " \"accountName\": \"Second Sub Account\",\n" +
+ " \"currencyCode\": \"NGN\",\n" +
+ " \"email\": \"second@example.com\",\n" +
+ " \"bankCode\": \"044\",\n" +
+ " \"bankName\": \"Access Bank\",\n" +
+ " \"defaultSplitPercentage\": 30.0,\n" +
+ " \"settlementProfileCode\": \"SETTLEMENT_002\",\n" +
+ " \"settlementReportEmails\": [\n" +
+ " \"second@example.com\"\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ "}"
+ )));
+
+ MonnifyBaseResponse> response =
+ subAccountService.getSubAccounts();
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertNotNull(response.getResponseBody());
+ assertEquals(2, response.getResponseBody().size());
+
+ SubAccountResponse first =
+ response.getResponseBody().get(0);
+
+ assertEquals("MFY_SUB_001", first.getSubAccountCode());
+ assertEquals("1234567890", first.getAccountNumber());
+ assertEquals("Test Sub Account", first.getAccountName());
+ assertEquals("NGN", first.getCurrencyCode());
+ assertEquals("058", first.getBankCode());
+ assertEquals("GTBank", first.getBankName());
+
+ SubAccountResponse second =
+ response.getResponseBody().get(1);
+
+ assertEquals("MFY_SUB_002", second.getSubAccountCode());
+ assertEquals("0987654321", second.getAccountNumber());
+ assertEquals("Second Sub Account", second.getAccountName());
+
+ verify(getRequestedFor(urlEqualTo("/api/v1/sub-accounts")));
+ }
+
+ @Test
+ void shouldUpdateSubAccount() {
+
+ stubFor(put(urlEqualTo("/api/v1/sub-accounts"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{\n" +
+ " \"requestSuccessful\": true,\n" +
+ " \"responseMessage\": \"success\",\n" +
+ " \"responseCode\": \"0\",\n" +
+ " \"responseBody\": {\n" +
+ " \"subAccountCode\": \"MFY_SUB_001\",\n" +
+ " \"accountNumber\": \"1234567890\",\n" +
+ " \"accountName\": \"Updated Sub Account\",\n" +
+ " \"currencyCode\": \"NGN\",\n" +
+ " \"email\": \"updated@example.com\",\n" +
+ " \"bankCode\": \"058\",\n" +
+ " \"bankName\": \"GTBank\",\n" +
+ " \"defaultSplitPercentage\": 70.0,\n" +
+ " \"settlementProfileCode\": \"SETTLEMENT_001\",\n" +
+ " \"settlementReportEmails\": [\n" +
+ " \"updated@example.com\"\n" +
+ " ]\n" +
+ " }\n" +
+ "}"
+ )));
+
+ UpdateSubAccountRequest request =
+ UpdateSubAccountRequest.builder()
+ .subAccountCode("MFY_SUB_001")
+ .currencyCode("NGN")
+ .bankCode("058")
+ .accountNumber("1234567890")
+ .email("updated@example.com")
+ .defaultSplitPercentage(70.0)
+ .build();
+
+ MonnifyBaseResponse response =
+ subAccountService.updateSubAccount(request);
+
+ assertNotNull(response);
+ assertTrue(response.isRequestSuccessful());
+ assertNotNull(response.getResponseBody());
+
+ SubAccountResponse subAccount =
+ response.getResponseBody();
+
+ assertEquals("MFY_SUB_001", subAccount.getSubAccountCode());
+ assertEquals("1234567890", subAccount.getAccountNumber());
+ assertEquals("Updated Sub Account", subAccount.getAccountName());
+ assertEquals("updated@example.com", subAccount.getEmail());
+ assertEquals(70.0, subAccount.getDefaultSplitPercentage());
+
+ verify(putRequestedFor(urlEqualTo("/api/v1/sub-accounts")));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/TransactionServiceMockTest.java b/src/test/java/com/monnify/mocktests/TransactionServiceMockTest.java
new file mode 100644
index 0000000..a2d08b6
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/TransactionServiceMockTest.java
@@ -0,0 +1,502 @@
+package com.monnify.mocktests;
+import com.google.gson.Gson;
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.exceptions.MonnifyValidationException;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.SearchResponse;
+import com.monnify.models.transaction.*;
+import com.monnify.services.transaction.TransactionService;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TransactionServiceMockTest extends BaseMonnifyMockTest {
+
+ private final TransactionService transactionService = new TransactionService();
+ private final Gson gson = new Gson();
+
+ @Test
+ void shouldInitializeTransaction() {
+
+ TransactionRequest request = TransactionRequest.builder()
+ .amount(new BigDecimal("1000.00"))
+ .customerName("John Doe")
+ .customerEmail("john.doe@example.com")
+ .paymentReference("PAY-001")
+ .paymentDescription("Test payment")
+ .currencyCode("NGN")
+ .contractCode("1234567890")
+ .redirectUrl("https://example.com/callback")
+ .build();
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"transactionReference\":\"MNFY-TRANS-001\","
+ + "\"paymentReference\":\"PAY-001\","
+ + "\"amount\":1000.00,"
+ + "\"currencyCode\":\"NGN\","
+ + "\"paymentStatus\":\"PENDING\""
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo("/api/v1/merchant/transactions/init-transaction"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ transactionService.initializeTransaction(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ verify(postRequestedFor(
+ urlEqualTo("/api/v1/merchant/transactions/init-transaction")));
+ }
+
+ @Test
+ void shouldPayWithBankTransfer() {
+
+ BankTransferRequest request = BankTransferRequest.builder()
+ .transactionReference("MNFY-TRANS-001")
+ .bankCode("058")
+ .build();
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"transactionReference\":\"MNFY-TRANS-001\","
+ + "\"paymentReference\":\"PAY-001\","
+ + "\"accountNumber\":\"1234567890\","
+ + "\"accountName\":\"John Doe\","
+ + "\"bankName\":\"GTBank\","
+ + "\"bankCode\":\"058\""
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo("/api/v1/merchant/bank-transfer/init-payment"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ transactionService.payWithBankTransfer(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ verify(postRequestedFor(
+ urlEqualTo("/api/v1/merchant/bank-transfer/init-payment")));
+ }
+
+ @Test
+ void shouldChargeCard() {
+
+ ChargeCardRequest.CardDetails card =
+ new ChargeCardRequest.CardDetails();
+
+ card.setNumber("4111111111111111");
+ card.setExpiryMonth("12");
+ card.setExpiryYear("30");
+ card.setPin("1234");
+ card.setCvv("123");
+
+ ChargeCardRequest request = ChargeCardRequest.builder()
+ .transactionReference("MNFY-TRANS-001")
+ .collectionChannel("CARD")
+ .card(card)
+ .build();
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"authorizedAmount\":1000.00,"
+ + "\"paymentReference\":\"PAY-001\","
+ + "\"transactionReference\":\"MNFY-TRANS-001\","
+ + "\"status\":\"SUCCESS\","
+ + "\"message\":\"Card charged successfully\""
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo("/api/v1/merchant/cards/charge"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ transactionService.chargeCard(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "PAY-001",
+ result.getResponseBody().getPaymentReference()
+ );
+
+ assertEquals(
+ "SUCCESS",
+ result.getResponseBody().getStatus()
+ );
+
+ verify(postRequestedFor(
+ urlEqualTo("/api/v1/merchant/cards/charge")));
+ }
+
+ @Test
+ void shouldChargeCardToken() {
+
+ ChargeCardTokenRequest request = ChargeCardTokenRequest.builder()
+ .cardToken("card-token-001")
+ .amount(new BigDecimal("1000.00"))
+ .customerName("John Doe")
+ .customerEmail("john.doe@example.com")
+ .paymentReference("PAY-002")
+ .paymentDescription("Token card payment")
+ .currencyCode("NGN")
+ .contractCode("1234567890")
+ .apiKey("test-api-key")
+ .build();
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"transactionReference\":\"MNFY-TRANS-002\","
+ + "\"paymentReference\":\"PAY-002\","
+ + "\"paymentStatus\":\"PAID\","
+ + "\"amountPaid\":1000.00,"
+ + "\"currency\":\"NGN\""
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo("/api/v1/merchant/cards/charge-card-token"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ transactionService.chargeCardToken(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "MNFY-TRANS-002",
+ result.getResponseBody().getTransactionReference()
+ );
+
+ verify(postRequestedFor(
+ urlEqualTo("/api/v1/merchant/cards/charge-card-token")));
+ }
+
+ @Test
+ void shouldAuthorizeCardOtp() {
+
+ AuthorizeOtpRequest request = AuthorizeOtpRequest.builder()
+ .transactionReference("MNFY-TRANS-001")
+ .collectionChannel("CARD")
+ .tokenId("otp-token-001")
+ .token("123456")
+ .build();
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"authorizedAmount\":1000.00,"
+ + "\"paymentReference\":\"PAY-001\","
+ + "\"transactionReference\":\"MNFY-TRANS-001\","
+ + "\"status\":\"SUCCESS\","
+ + "\"message\":\"OTP authorization successful\""
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo("/api/v1/merchant/cards/otp/authorize"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ transactionService.authorizeCardOtp(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "SUCCESS",
+ result.getResponseBody().getStatus()
+ );
+
+ verify(postRequestedFor(
+ urlEqualTo("/api/v1/merchant/cards/otp/authorize")));
+ }
+
+ @Test
+ void shouldSearchTransactions() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"content\":["
+ + "{"
+ + "\"transactionReference\":\"MNFY-TRANS-001\","
+ + "\"paymentReference\":\"PAY-001\","
+ + "\"amountPaid\":1000.00,"
+ + "\"paymentStatus\":\"PAID\""
+ + "}"
+ + "],"
+ + "\"pageable\":{"
+ + "\"pageNumber\":0,"
+ + "\"pageSize\":10"
+ + "},"
+ + "\"totalElements\":1,"
+ + "\"totalPages\":1"
+ + "}"
+ + "}";
+
+ stubFor(get(urlPathEqualTo("/api/v1/transactions/search"))
+ .withQueryParam("pageNo", equalTo("0"))
+ .withQueryParam("pageSize", equalTo("10"))
+ .withQueryParam("paymentReference", equalTo("PAY-001"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse> result =
+ transactionService.searchTransactions(
+ 0,
+ 10,
+ "PAY-001",
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ "PAID",
+ null,
+ null
+ );
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ verify(getRequestedFor(
+ urlPathEqualTo("/api/v1/transactions/search"))
+ .withQueryParam("pageNo", equalTo("0"))
+ .withQueryParam("pageSize", equalTo("10"))
+ .withQueryParam("paymentReference", equalTo("PAY-001")));
+ }
+
+ @Test
+ void shouldGetTransactionStatus() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"transactionReference\":\"MNFY-TRANS-001\","
+ + "\"paymentReference\":\"PAY-001\","
+ + "\"amountPaid\":1000.00,"
+ + "\"totalPayable\":1000.00,"
+ + "\"paymentStatus\":\"PAID\","
+ + "\"currency\":\"NGN\""
+ + "}"
+ + "}";
+
+ stubFor(get(urlEqualTo("/api/v2/transactions/MNFY-TRANS-001"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ transactionService.getStatus("MNFY-TRANS-001");
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "MNFY-TRANS-001",
+ result.getResponseBody().getTransactionReference()
+ );
+
+ assertEquals(
+ "PAID",
+ result.getResponseBody().getPaymentStatus()
+ );
+
+ verify(getRequestedFor(
+ urlEqualTo("/api/v2/transactions/MNFY-TRANS-001")));
+ }
+
+ @Test
+ void shouldGetStatusByPaymentReference() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"transactionReference\":\"MNFY-TRANS-001\","
+ + "\"paymentReference\":\"PAY-001\","
+ + "\"paymentStatus\":\"PAID\","
+ + "\"amountPaid\":1000.00,"
+ + "\"currency\":\"NGN\""
+ + "}"
+ + "}";
+
+ stubFor(get(urlPathEqualTo("/api/v2/merchant/transactions/query"))
+ .withQueryParam("paymentReference", equalTo("PAY-001"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ transactionService.getStatusByReference(
+ "PAY-001",
+ null
+ );
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "PAY-001",
+ result.getResponseBody().getPaymentReference()
+ );
+
+ assertEquals(
+ "PAID",
+ result.getResponseBody().getPaymentStatus()
+ );
+
+ verify(getRequestedFor(
+ urlPathEqualTo("/api/v2/merchant/transactions/query"))
+ .withQueryParam(
+ "paymentReference",
+ equalTo("PAY-001")
+ ));
+ }
+
+ @Test
+ void shouldGetStatusByTransactionReference() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"transactionReference\":\"MNFY-TRANS-001\","
+ + "\"paymentReference\":\"PAY-001\","
+ + "\"paymentStatus\":\"PAID\","
+ + "\"amountPaid\":1000.00"
+ + "}"
+ + "}";
+
+ stubFor(get(urlPathEqualTo("/api/v2/merchant/transactions/query"))
+ .withQueryParam(
+ "transactionReference",
+ equalTo("MNFY-TRANS-001")
+ )
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ transactionService.getStatusByReference(
+ null,
+ "MNFY-TRANS-001"
+ );
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "MNFY-TRANS-001",
+ result.getResponseBody().getTransactionReference()
+ );
+
+ verify(getRequestedFor(
+ urlPathEqualTo("/api/v2/merchant/transactions/query"))
+ .withQueryParam(
+ "transactionReference",
+ equalTo("MNFY-TRANS-001")
+ ));
+ }
+
+ @Test
+ void shouldThrowExceptionWhenTransactionReferenceIsEmpty() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> transactionService.getStatus("")
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenBothReferencesAreEmpty() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> transactionService.getStatusByReference(
+ null,
+ null
+ )
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenBothReferencesAreProvided() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> transactionService.getStatusByReference(
+ "PAY-001",
+ "MNFY-TRANS-001"
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monnify/mocktests/VerificationServiceMockTest.java b/src/test/java/com/monnify/mocktests/VerificationServiceMockTest.java
new file mode 100644
index 0000000..dce2237
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/VerificationServiceMockTest.java
@@ -0,0 +1,371 @@
+package com.monnify.mocktests;
+import com.monnify.BaseMonnifyMockTest;
+import com.monnify.exceptions.MonnifyValidationException;
+import com.monnify.models.MonnifyBaseResponse;
+import com.monnify.models.verification.*;
+import com.monnify.services.verification.VerificationService;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+
+public class VerificationServiceMockTest extends BaseMonnifyMockTest {
+
+ private final VerificationService verificationService =
+ new VerificationService();
+
+ @Test
+ void shouldValidateBankAccount() {
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"accountNumber\":\"0123456789\","
+ + "\"accountName\":\"JOHN DOE\","
+ + "\"bankCode\":\"058\""
+ + "}"
+ + "}";
+
+ stubFor(get(urlPathEqualTo(
+ "/api/v2/disbursements/account/validate"))
+ .withQueryParam(
+ "accountNumber",
+ equalTo("0123456789"))
+ .withQueryParam(
+ "bankCode",
+ equalTo("058"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ verificationService.validateBankAccount(
+ "0123456789",
+ "058"
+ );
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "0123456789",
+ result.getResponseBody().getAccountNumber()
+ );
+
+ assertEquals(
+ "JOHN DOE",
+ result.getResponseBody().getAccountName()
+ );
+
+ assertEquals(
+ "058",
+ result.getResponseBody().getBankCode()
+ );
+
+ verify(getRequestedFor(
+ urlPathEqualTo(
+ "/api/v2/disbursements/account/validate"))
+ .withQueryParam(
+ "accountNumber",
+ equalTo("0123456789"))
+ .withQueryParam(
+ "bankCode",
+ equalTo("058"))
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenAccountNumberIsEmpty() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> verificationService.validateBankAccount(
+ "",
+ "058"
+ )
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenBankCodeIsEmpty() {
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> verificationService.validateBankAccount(
+ "0123456789",
+ ""
+ )
+ );
+ }
+
+ @Test
+ void shouldVerifyBVNInformation() {
+
+ String response = "{"
+ + "\"requestSuccessful\": true,"
+ + "\"responseMessage\": \"success\","
+ + "\"responseCode\": \"0\","
+ + "\"responseBody\": {"
+ + "\"bvn\": \"22222222222\","
+ + "\"name\": {"
+ + "\"matchStatus\": \"FULL_MATCH\","
+ + "\"matchPercentage\": 100.0"
+ + "},"
+ + "\"dateOfBirth\": \"1990-01-01\","
+ + "\"mobileNo\": \"08012345678\""
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo("/api/v1/vas/bvn-details-match"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(response)));
+
+ BVNVerificationRequest request = BVNVerificationRequest.builder()
+ .bvn("22222222222")
+ .name("John Doe")
+ .dateOfBirth(LocalDate.of(1990, 1, 1))
+ .mobileNo("08012345678")
+ .build();
+
+ MonnifyBaseResponse result =
+ verificationService.verifyBVNInformation(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertEquals("success", result.getResponseMessage());
+
+ assertNotNull(result.getResponseBody());
+
+ BVNVerificationResponse responseBody = result.getResponseBody();
+
+ assertEquals("22222222222", responseBody.getBvn());
+ assertEquals("1990-01-01", responseBody.getDateOfBirth());
+ assertEquals("08012345678", responseBody.getMobileNo());
+
+ assertNotNull(responseBody.getName());
+ assertEquals("FULL_MATCH", responseBody.getName().getMatchStatus());
+ assertEquals(
+ 100.0,
+ responseBody.getName().getMatchPercentage(),
+ 0.001
+ );
+
+ verify(postRequestedFor(
+ urlEqualTo("/api/v1/vas/bvn-details-match")
+ ));
+ }
+
+ @Test
+ void shouldBVNAccountNameMatch() {
+
+ BVNAccountMatchRequest request =
+ BVNAccountMatchRequest.builder()
+ .bankCode("058")
+ .accountNumber("0123456789")
+ .bvn("22222222222")
+ .build();
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"accountNumber\":\"0123456789\","
+ + "\"accountName\":\"JOHN DOE\","
+ + "\"bvn\":\"22222222222\","
+ + "\"matchStatus\":\"FULL_MATCH\","
+ + "\"matchPercentage\":100.0"
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo(
+ "/api/v1/vas/bvn-account-match"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ verificationService.bvnAccountNameMatch(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ verify(postRequestedFor(
+ urlEqualTo(
+ "/api/v1/vas/bvn-account-match")));
+ }
+
+ @Test
+ void shouldVerifyNIN() {
+
+ NINVerificationRequest request =
+ NINVerificationRequest.builder()
+ .nin("12345678901")
+ .build();
+
+ String response = "{"
+ + "\"requestSuccessful\":true,"
+ + "\"responseMessage\":\"success\","
+ + "\"responseCode\":\"0\","
+ + "\"responseBody\":{"
+ + "\"nin\":\"12345678901\","
+ + "\"lastName\":\"DOE\","
+ + "\"firstName\":\"JOHN\","
+ + "\"middleName\":\"MICHAEL\","
+ + "\"dateOfBirth\":\"1995-01-01\","
+ + "\"gender\":\"MALE\","
+ + "\"mobileNumber\":\"08012345678\""
+ + "}"
+ + "}";
+
+ stubFor(post(urlEqualTo(
+ "/api/v1/vas/nin-details"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader(
+ "Content-Type",
+ "application/json")
+ .withBody(response)));
+
+ MonnifyBaseResponse result =
+ verificationService.verifyNIN(request);
+
+ assertNotNull(result);
+ assertTrue(result.isRequestSuccessful());
+ assertEquals("0", result.getResponseCode());
+ assertNotNull(result.getResponseBody());
+
+ assertEquals(
+ "12345678901",
+ result.getResponseBody().getNin()
+ );
+
+ assertEquals(
+ "DOE",
+ result.getResponseBody().getLastName()
+ );
+
+ assertEquals(
+ "JOHN",
+ result.getResponseBody().getFirstName()
+ );
+
+ assertEquals(
+ "MICHAEL",
+ result.getResponseBody().getMiddleName()
+ );
+
+ assertEquals(
+ "1995-01-01",
+ result.getResponseBody().getDateOfBirth()
+ );
+
+ assertEquals(
+ "MALE",
+ result.getResponseBody().getGender()
+ );
+
+ assertEquals(
+ "08012345678",
+ result.getResponseBody().getMobileNumber()
+ );
+
+ verify(postRequestedFor(
+ urlEqualTo(
+ "/api/v1/vas/nin-details")));
+ }
+
+ @Test
+ void shouldThrowExceptionWhenBVNIsInvalid() {
+
+ BVNAccountMatchRequest request =
+ BVNAccountMatchRequest.builder()
+ .bankCode("058")
+ .accountNumber("0123456789")
+ .bvn("12345")
+ .build();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> verificationService.bvnAccountNameMatch(request)
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenBVNAccountNumberIsEmpty() {
+
+ BVNAccountMatchRequest request =
+ BVNAccountMatchRequest.builder()
+ .bankCode("058")
+ .accountNumber("")
+ .bvn("22222222222")
+ .build();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> verificationService.bvnAccountNameMatch(request)
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenBVNBankCodeIsEmpty() {
+
+ BVNAccountMatchRequest request =
+ BVNAccountMatchRequest.builder()
+ .bankCode("")
+ .accountNumber("0123456789")
+ .bvn("22222222222")
+ .build();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> verificationService.bvnAccountNameMatch(request)
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenNINIsInvalid() {
+
+ NINVerificationRequest request =
+ NINVerificationRequest.builder()
+ .nin("12345")
+ .build();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> verificationService.verifyNIN(request)
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenNINIsEmpty() {
+
+ NINVerificationRequest request =
+ NINVerificationRequest.builder()
+ .nin("")
+ .build();
+
+ assertThrows(
+ MonnifyValidationException.class,
+ () -> verificationService.verifyNIN(request)
+ );
+ }
+}
diff --git a/src/test/java/com/monnify/mocktests/WebhookServiceMockTest.java b/src/test/java/com/monnify/mocktests/WebhookServiceMockTest.java
new file mode 100644
index 0000000..59f2622
--- /dev/null
+++ b/src/test/java/com/monnify/mocktests/WebhookServiceMockTest.java
@@ -0,0 +1,403 @@
+package com.monnify.mocktests;
+import com.monnify.exceptions.MonnifyException;
+import com.monnify.services.webhook.WebhookService;
+import org.junit.jupiter.api.Test;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class WebhookServiceMockTest {
+
+ private final WebhookService webhookService = new WebhookService();
+
+ @Test
+ void shouldCalculateHMAC512TransactionHash() throws Exception {
+ String data = "test-data";
+ String merchantClientSecret = "test-secret";
+
+ String result = webhookService.calculateHMAC512TransactionHash(
+ data,
+ merchantClientSecret
+ );
+
+ assertNotNull(result);
+ assertEquals(128, result.length());
+ }
+
+ @Test
+ void shouldReturnTrueWhenSignatureIsValid() throws Exception {
+ String data = "test-data";
+ String merchantClientSecret = "test-secret";
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ data,
+ merchantClientSecret
+ );
+
+ boolean result = webhookService.isSignatureValid(
+ signature,
+ data,
+ merchantClientSecret
+ );
+
+ assertTrue(result);
+ }
+
+ @Test
+ void shouldReturnFalseWhenSignatureIsInvalid() throws Exception {
+ String data = "test-data";
+ String merchantClientSecret = "test-secret";
+
+ boolean result = webhookService.isSignatureValid(
+ "invalid-signature",
+ data,
+ merchantClientSecret
+ );
+
+ assertFalse(result);
+ }
+
+ @Test
+ void shouldResolveSuccessfulTransactionWebhook() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "SUCCESSFUL_TRANSACTION");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ Object result = webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ );
+
+ assertNotNull(result);
+ assertEquals(
+ "SUCCESSFUL_TRANSACTION",
+ ((com.monnify.models.webhook.BaseWebhookResponse>) result).getEventType()
+ );
+ }
+
+ @Test
+ void shouldResolveSuccessfulDisbursementWebhook() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "SUCCESSFUL_DISBURSEMENT");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ Object result = webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ );
+
+ assertNotNull(result);
+ assertEquals(
+ "SUCCESSFUL_DISBURSEMENT",
+ ((com.monnify.models.webhook.BaseWebhookResponse>) result).getEventType()
+ );
+ }
+
+ @Test
+ void shouldResolveFailedDisbursementWebhook() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "FAILED_DISBURSEMENT");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ Object result = webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ );
+
+ assertNotNull(result);
+ assertEquals(
+ "FAILED_DISBURSEMENT",
+ ((com.monnify.models.webhook.BaseWebhookResponse>) result).getEventType()
+ );
+ }
+
+ @Test
+ void shouldResolveReversedDisbursementWebhook() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "REVERSED_DISBURSEMENT");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ Object result = webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ );
+
+ assertNotNull(result);
+ assertEquals(
+ "REVERSED_DISBURSEMENT",
+ ((com.monnify.models.webhook.BaseWebhookResponse>) result).getEventType()
+ );
+ }
+
+ @Test
+ void shouldResolveSuccessfulRefundWebhook() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "SUCCESSFUL_REFUND");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ Object result = webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ );
+
+ assertNotNull(result);
+ assertEquals(
+ "SUCCESSFUL_REFUND",
+ ((com.monnify.models.webhook.BaseWebhookResponse>) result).getEventType()
+ );
+ }
+
+ @Test
+ void shouldResolveFailedRefundWebhook() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "FAILED_REFUND");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ Object result = webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ );
+
+ assertNotNull(result);
+ assertEquals(
+ "FAILED_REFUND",
+ ((com.monnify.models.webhook.BaseWebhookResponse>) result).getEventType()
+ );
+ }
+
+ @Test
+ void shouldResolveSettlementWebhook() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "SETTLEMENT");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ Object result = webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ );
+
+ assertNotNull(result);
+ assertEquals(
+ "SETTLEMENT",
+ ((com.monnify.models.webhook.BaseWebhookResponse>) result).getEventType()
+ );
+ }
+
+ @Test
+ void shouldResolveRejectedPaymentWebhook() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "REJECTED_PAYMENT");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ Object result = webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ );
+
+ assertNotNull(result);
+ assertEquals(
+ "REJECTED_PAYMENT",
+ ((com.monnify.models.webhook.BaseWebhookResponse>) result).getEventType()
+ );
+ }
+
+ @Test
+ void shouldResolveMandateUpdateWebhook() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "MANDATE_UPDATE");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ Object result = webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ );
+
+ assertNotNull(result);
+ assertEquals(
+ "MANDATE_UPDATE",
+ ((com.monnify.models.webhook.BaseWebhookResponse>) result).getEventType()
+ );
+ }
+
+ @Test
+ void shouldResolveAccountActivityWebhook() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "ACCOUNT_ACTIVITY");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ Object result = webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ );
+
+ assertNotNull(result);
+ assertEquals(
+ "ACCOUNT_ACTIVITY",
+ ((com.monnify.models.webhook.BaseWebhookResponse>) result).getEventType()
+ );
+ }
+
+ @Test
+ void shouldThrowExceptionWhenWebhookSignatureIsInvalid() {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "SETTLEMENT");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ MonnifyException exception = assertThrows(
+ MonnifyException.class,
+ () -> webhookService.resolveMonnifyWebhook(
+ "invalid-signature",
+ webhookPayload,
+ secret
+ )
+ );
+
+ assertEquals("Invalid signature received", exception.getMessage());
+ }
+
+ @Test
+ void shouldThrowExceptionForInvalidEventType() throws Exception {
+ Map webhookPayload = new HashMap<>();
+
+ webhookPayload.put("eventType", "INVALID_EVENT");
+ webhookPayload.put("eventData", new HashMap());
+
+ String secret = "test-secret";
+
+ String payload = new com.google.gson.Gson().toJson(webhookPayload);
+
+ String signature = webhookService.calculateHMAC512TransactionHash(
+ payload,
+ secret
+ );
+
+ MonnifyException exception = assertThrows(
+ MonnifyException.class,
+ () -> webhookService.resolveMonnifyWebhook(
+ signature,
+ webhookPayload,
+ secret
+ )
+ );
+
+ assertEquals(
+ "Invalid event type: INVALID_EVENT",
+ exception.getMessage()
+ );
+ }
+
+}
diff --git a/src/test/java/com/monnify/services/directdebit/DirectDebitServiceTest.java b/src/test/java/com/monnify/services/directdebit/DirectDebitServiceTest.java
index f7f6f22..01a1288 100644
--- a/src/test/java/com/monnify/services/directdebit/DirectDebitServiceTest.java
+++ b/src/test/java/com/monnify/services/directdebit/DirectDebitServiceTest.java
@@ -1,6 +1,5 @@
package com.monnify.services.directdebit;
-import com.google.gson.Gson;
import com.monnify.Monnify;
import com.monnify.models.MonnifyBaseResponse;
import com.monnify.models.directdebit.MandateRequest;
diff --git a/src/test/java/com/monnify/services/disbursement/DisbursementServiceTest.java b/src/test/java/com/monnify/services/disbursement/DisbursementServiceTest.java
index 3801b22..c95326b 100644
--- a/src/test/java/com/monnify/services/disbursement/DisbursementServiceTest.java
+++ b/src/test/java/com/monnify/services/disbursement/DisbursementServiceTest.java
@@ -7,7 +7,6 @@
import org.junit.jupiter.api.*;
import java.math.BigDecimal;
-import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
@@ -35,10 +34,10 @@ void disburseSingle() {
int randomNumber = new Random().nextInt(1000);
MonnifyBaseResponse response = disbursementService.disburseSingle(SingleDisbursementRequest.builder()
.amount(BigDecimal.valueOf(randomNumber))
- .sourceAccountNumber("7853297429")
+ .sourceAccountNumber("7216487825")
.currency("NGN")
- .destinationBankCode("044")
- .destinationAccountNumber("0104430292")
+ .destinationBankCode("058")
+ .destinationAccountNumber("7062396366")
.destinationAccountName("Tester Test")
.reference(reference)
.narration("Test Single trnx" + randomNumber)
@@ -56,29 +55,29 @@ void disburseBatch() {
.narration("test batch on sdk" + randomNumber)
.notificationInterval(25)
.onValidationFailure(OnValidationFailure.CONTINUE)
- .sourceAccountNumber("7853297429")
+ .sourceAccountNumber("8144157072")
.transactionList(List.of(DisbursementTransaction.builder()
- .amount(BigDecimal.valueOf(1000))
+ .amount(BigDecimal.valueOf(10))
.currency("NGN")
.narration("batch dis" + randomNumber)
.reference(UUID.randomUUID().toString())
- .destinationAccountNumber("0104430292")
- .destinationBankCode("044")
+ .destinationAccountNumber("7062396366")
+ .destinationBankCode("305")
.build()
,DisbursementTransaction.builder()
- .amount(BigDecimal.valueOf(500))
+ .amount(BigDecimal.valueOf(50))
.currency("NGN")
.narration("batch dis" + randomNumber)
.reference(UUID.randomUUID().toString())
- .destinationAccountNumber("0104430292")
- .destinationBankCode("044")
+ .destinationAccountNumber("7062396366")
+ .destinationBankCode("305")
.build(),DisbursementTransaction.builder()
- .amount(BigDecimal.valueOf(250))
+ .amount(BigDecimal.valueOf(25))
.currency("NGN")
.narration("batch dis" + randomNumber)
.reference(UUID.randomUUID().toString())
- .destinationAccountNumber("0104430292")
- .destinationBankCode("044")
+ .destinationAccountNumber("7062396366")
+ .destinationBankCode("305")
.build()
)).build());
assertSuccess(response);
diff --git a/src/test/java/com/monnify/services/invoice/InvoiceServiceTest.java b/src/test/java/com/monnify/services/invoice/InvoiceServiceTest.java
index 2cce47a..112371f 100644
--- a/src/test/java/com/monnify/services/invoice/InvoiceServiceTest.java
+++ b/src/test/java/com/monnify/services/invoice/InvoiceServiceTest.java
@@ -1,7 +1,6 @@
package com.monnify.services.invoice;
import com.google.gson.Gson;
-import com.monnify.MainTest;
import com.monnify.Monnify;
import com.monnify.models.MonnifyBaseResponse;
import com.monnify.models.SearchResponse;
diff --git a/src/test/java/com/monnify/services/paycode/PaycodeServiceTest.java b/src/test/java/com/monnify/services/paycode/PaycodeServiceTest.java
index da0cc2c..d7f2f5f 100644
--- a/src/test/java/com/monnify/services/paycode/PaycodeServiceTest.java
+++ b/src/test/java/com/monnify/services/paycode/PaycodeServiceTest.java
@@ -8,7 +8,6 @@
import org.junit.jupiter.api.*;
import java.math.BigDecimal;
-import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
diff --git a/src/test/java/com/monnify/services/settlement/SettlementServiceTest.java b/src/test/java/com/monnify/services/settlement/SettlementServiceTest.java
index 16aba47..7024028 100644
--- a/src/test/java/com/monnify/services/settlement/SettlementServiceTest.java
+++ b/src/test/java/com/monnify/services/settlement/SettlementServiceTest.java
@@ -1,6 +1,5 @@
package com.monnify.services.settlement;
-import com.google.gson.Gson;
import com.monnify.Monnify;
import com.monnify.models.MonnifyBaseResponse;
import com.monnify.models.SearchResponse;
diff --git a/src/test/java/com/monnify/services/webhook/WebhookServiceTest.java b/src/test/java/com/monnify/services/webhook/WebhookServiceTest.java
index d208bcb..25b0e08 100644
--- a/src/test/java/com/monnify/services/webhook/WebhookServiceTest.java
+++ b/src/test/java/com/monnify/services/webhook/WebhookServiceTest.java
@@ -1,7 +1,5 @@
package com.monnify.services.webhook;
-import static org.junit.jupiter.api.Assertions.*;
-
class WebhookServiceTest {
}
\ No newline at end of file