From 241f0e81f32bd3269d3beb389e09d12d4a45f18d Mon Sep 17 00:00:00 2001 From: Michael Obi <58430556+michael-paystack@users.noreply.github.com> Date: Wed, 8 Feb 2023 10:50:09 +0100 Subject: [PATCH] MOB-183 - Fix transaction initialisation using access code. (#150) * Fix transaction initialisation using access code. * Call notifyAll on singleton instance object in AuthActivity * Add tests for initiateTransaction * Add optional charge parameters to initialize request (#151) --- build.gradle | 4 +- gradle.properties | 4 +- .../paystack/android/TransactionManager.java | 20 +++-- .../android/api/PaystackRepository.kt | 5 ++ .../android/api/PaystackRepositoryImpl.kt | 14 ++++ .../api/request/TransactionInitRequestBody.kt | 20 ++++- .../android/api/service/PaystackApiService.kt | 3 + .../co/paystack/android/ui/AuthActivity.java | 11 ++- .../android/TransactionManagerTest.kt | 79 +++++++++++++++++++ .../android/api/PaystackRepositoryImplTest.kt | 45 +++++++++++ 10 files changed, 192 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index b4f854c..f5471a1 100644 --- a/build.gradle +++ b/build.gradle @@ -64,10 +64,10 @@ ext { compileSdkVersion = 29 minSdkVersion = 16 targetSdkVersion = 29 - versionCode = 19 + versionCode = 21 buildToolsVersion = "29.0.2" - versionName = "3.2.0-alpha02" + versionName = "3.3.0-alpha02" } Object getEnvOrDefault(String propertyName, Object defaultValue) { diff --git a/gradle.properties b/gradle.properties index 69fbc7b..a614749 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,7 +25,7 @@ android.useAndroidX=true org.gradle.daemon=true org.gradle.jvmargs=-Xmx2560m GROUP=co.paystack.android -VERSION_NAME=3.2.0-alpha02 +VERSION_NAME=3.3.0-alpha01 POM_DESCRIPTION=Android SDK for Paystack POM_URL=https://github.com/PaystackHQ/paystack-android POM_SCM_URL=https://github.com/PaystackHQ/paystack-android @@ -37,4 +37,4 @@ POM_LICENCE_DIST=repo POM_DEVELOPER_ID=paystack POM_DEVELOPER_NAME=Paystack POM_DEVELOPER_EMAIL=developers@paystack.co -org.gradle.unsafe.configuration-cache=true \ No newline at end of file +org.gradle.unsafe.configuration-cache=false \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/TransactionManager.java b/paystack/src/main/java/co/paystack/android/TransactionManager.java index 52bbc65..6c9e308 100644 --- a/paystack/src/main/java/co/paystack/android/TransactionManager.java +++ b/paystack/src/main/java/co/paystack/android/TransactionManager.java @@ -1,5 +1,7 @@ package co.paystack.android; +import static co.paystack.android.Transaction.EMPTY_TRANSACTION; + import android.app.Activity; import android.content.Intent; import android.os.AsyncTask; @@ -7,6 +9,8 @@ import android.provider.Settings; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import org.jetbrains.annotations.NotNull; import co.paystack.android.api.ApiCallback; @@ -34,8 +38,6 @@ import co.paystack.android.utils.Crypto; import co.paystack.android.utils.StringUtils; -import static co.paystack.android.Transaction.EMPTY_TRANSACTION; - class TransactionManager { private static final String LOG_TAG = TransactionManager.class.getSimpleName(); @@ -117,8 +119,9 @@ private void validateCardThenInitTransaction(String publicKey, Charge charge) { } } - private void initiateTransaction(String publicKey, Charge charge, String deviceId) { - paystackRepository.initializeTransaction(publicKey, charge, deviceId, new ApiCallback() { + @VisibleForTesting + void initiateTransaction(String publicKey, Charge charge, String deviceId) { + ApiCallback callback = new ApiCallback() { @Override public void onSuccess(TransactionInitResponse data) { Card card = charge.getCard(); @@ -132,12 +135,19 @@ public void onSuccess(TransactionInitResponse data) { ); paystackRepository.processCardCharge(params, cardProcessCallback); } + @Override public void onError(@NotNull Throwable exception) { Log.e(LOG_TAG, exception.getMessage(), exception); notifyProcessingError(exception); } - }); + }; + + if (charge.getAccessCode() == null || charge.getAccessCode().isEmpty()) { + paystackRepository.initializeTransaction(publicKey, charge, deviceId, callback); + } else { + paystackRepository.getTransactionWithAccessCode(charge.getAccessCode(), callback); + } } private void processChargeResponse(ChargeParams chargeParams, ChargeResponse chargeResponse) { diff --git a/paystack/src/main/java/co/paystack/android/api/PaystackRepository.kt b/paystack/src/main/java/co/paystack/android/api/PaystackRepository.kt index ec1881e..c95916c 100644 --- a/paystack/src/main/java/co/paystack/android/api/PaystackRepository.kt +++ b/paystack/src/main/java/co/paystack/android/api/PaystackRepository.kt @@ -16,4 +16,9 @@ interface PaystackRepository { fun validateAddress(chargeParams: ChargeParams, address: Address, callback: ChargeApiCallback) fun requeryTransaction(chargeParams: ChargeParams, callback: ChargeApiCallback) + + fun getTransactionWithAccessCode( + accessCode: String, + callback: ApiCallback + ) } \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt b/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt index 519db7b..60e6603 100644 --- a/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt +++ b/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt @@ -21,6 +21,12 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService currency = charge.currency, metadata = charge.metadata, device = deviceId, + reference = charge.reference, + subAccount = charge.subaccount, + transactionCharge = charge.transactionCharge, + plan = charge.plan, + bearer = charge.bearer, + additionalParameters = charge.additionalParameters, ).toRequestMap() makeApiRequest( @@ -71,6 +77,14 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService ) } + override fun getTransactionWithAccessCode(accessCode: String, callback: ApiCallback) { + makeApiRequest( + onSuccess = { data -> callback.onSuccess(data) }, + onError = { throwable -> callback.onError(throwable) }, + apiCall = { apiService.getTransaction(accessCode) } + ) + } + private fun makeApiRequest(apiCall: () -> Call, onSuccess: (T) -> Unit, onError: (Throwable) -> Unit) { val retrofitCallback = object : Callback { override fun onResponse(call: Call, response: Response) { diff --git a/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt b/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt index c11e517..9b618a0 100644 --- a/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt +++ b/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt @@ -1,6 +1,7 @@ package co.paystack.android.api.request import co.paystack.android.api.utils.pruneNullValues +import co.paystack.android.model.Charge.Bearer data class TransactionInitRequestBody( val publicKey: String, @@ -9,14 +10,25 @@ data class TransactionInitRequestBody( val currency: String?, val metadata: String?, val device: String, + val reference: String?, + val subAccount: String?, + val transactionCharge: Int?, + val plan: String?, + val bearer: Bearer?, + val additionalParameters: Map, ) { - fun toRequestMap() = mapOf( + fun toRequestMap() = additionalParameters + mapOf( FIELD_KEY to publicKey, FIELD_EMAIL to email, FIELD_AMOUNT to amount, FIELD_CURRENCY to currency, FIELD_METADATA to metadata, FIELD_DEVICE to device, + FIELD_REFERENCE to reference, + FIELD_SUBACCOUNT to subAccount, + FIELD_TRANSACTION_CHARGE to transactionCharge, + FIELD_BEARER to bearer?.name, + FIELD_PLAN to plan, ).pruneNullValues() companion object { @@ -26,5 +38,11 @@ data class TransactionInitRequestBody( const val FIELD_CURRENCY = "currency" const val FIELD_METADATA = "metadata" const val FIELD_DEVICE = "device" + + const val FIELD_REFERENCE = "reference"; + const val FIELD_SUBACCOUNT = "subaccount"; + const val FIELD_TRANSACTION_CHARGE = "transaction_charge"; + const val FIELD_BEARER = "bearer"; + const val FIELD_PLAN = "plan"; } } diff --git a/paystack/src/main/java/co/paystack/android/api/service/PaystackApiService.kt b/paystack/src/main/java/co/paystack/android/api/service/PaystackApiService.kt index c7a7942..376885d 100644 --- a/paystack/src/main/java/co/paystack/android/api/service/PaystackApiService.kt +++ b/paystack/src/main/java/co/paystack/android/api/service/PaystackApiService.kt @@ -14,6 +14,9 @@ internal interface PaystackApiService { @GET("/checkout/request_inline") fun initializeTransaction(@QueryMap params: Map): Call + @GET("/transaction/verify_access_code/{accessCode}") + fun getTransaction(@Path("accessCode") accessCode: String): Call + @FormUrlEncoded @POST("/checkout/card/charge") @NoWrap diff --git a/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java b/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java index 9d40c69..82ea939 100644 --- a/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java +++ b/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java @@ -65,7 +65,7 @@ public void handleResponse() { } synchronized (si) { si.setResponseJson(responseJson); - si.notify(); + si.notifyAll(); } finish(); } @@ -73,7 +73,12 @@ public void handleResponse() { protected void setupWebview() { setContentView(R.layout.co_paystack_android____activity_auth); - findViewById(R.id.iv_close).setOnClickListener(v -> finish()); + findViewById(R.id.iv_close).setOnClickListener(v -> { + synchronized (si) { + si.notify(); + } + finish(); + }); webView = findViewById(R.id.webView); webView.setKeepScreenOn(true); @@ -133,7 +138,7 @@ public void onLoadResource(WebView view, String url) { } public void onDestroy() { - pusher.unsubscribe(channelName); + pusher.disconnect(); super.onDestroy(); if (webView != null) { webView.stopLoading(); diff --git a/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt b/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt index b7294b3..fd7cfa1 100644 --- a/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt +++ b/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt @@ -1,15 +1,23 @@ package co.paystack.android +import android.util.Log import androidx.test.core.app.ActivityScenario +import co.paystack.android.api.ApiCallback +import co.paystack.android.api.ChargeApiCallback import co.paystack.android.api.PaystackRepository +import co.paystack.android.api.model.TransactionInitResponse +import co.paystack.android.api.request.ChargeParams import co.paystack.android.model.Card import co.paystack.android.model.Charge +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.isA import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.anyString import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import org.robolectric.RobolectricTestRunner @@ -44,7 +52,78 @@ class TransactionManagerTest { } } + @Test + fun initiateTransactionIsCalled_ChargeAccessCodeIsNull_callInitializeTransactionOnPaystackRepository() { + val publicKey = "pk_test_key" + val deviceId = "android_702948084" + val charge = Charge().apply { + card = TEST_CARD + accessCode = null + } + whenever(paystackRepository.initializeTransaction(anyString(), any(), anyString(), any())) + .thenAnswer { Log.i(TAG, "initializeTransaction called") } + + val transactionManager = TransactionManager(paystackRepository) + transactionManager.initiateTransaction(publicKey, charge, deviceId) + + verify(paystackRepository).initializeTransaction( + isA(), + isA(), + isA(), + isA>() + ) + } + + @Test + fun initiateTransactionIsCalled_ChargeAccessCodeIsNotNull_call_getTransactionWithAccessCode_on_PaystackRepository() { + val publicKey = "pk_test_key" + val deviceId = "android_702948084" + val transAccessCode = "transaction_access_code" + val charge = Charge().apply { + card = TEST_CARD + accessCode = transAccessCode + } + + whenever(paystackRepository.getTransactionWithAccessCode(anyString(), any())) + .thenAnswer { Log.i(TAG, "getTransactionWithAccessCode called") } + + val transactionManager = TransactionManager(paystackRepository) + transactionManager.initiateTransaction(publicKey, charge, deviceId) + + verify(paystackRepository).getTransactionWithAccessCode( + isA(), + isA>() + ) + } + + + @Test + fun initiateTransactionIsCalled_transactionInitializationSucceeds_call_processCardCharge_OnPaystackRepository() { + val publicKey = "pk_test_key" + val deviceId = "android_702948084" + val charge = Charge().apply { + card = TEST_CARD + accessCode = null + } + + whenever(paystackRepository.initializeTransaction(anyString(), any(), anyString(), any())) + .thenAnswer { + val callback = it.arguments[3] as ApiCallback + callback.onSuccess(TransactionInitResponse("success", "trans_id")) + } + + val transactionManager = TransactionManager(paystackRepository) + transactionManager.initiateTransaction(publicKey, charge, deviceId) + + verify(paystackRepository).processCardCharge( + isA(), + isA() + ) + } + + companion object { + private const val TAG = "TransactionManagerTest" val TEST_CARD = Card("5105105105105100", 2, 2024, "123") } } \ No newline at end of file diff --git a/paystack/src/test/java/co/paystack/android/api/PaystackRepositoryImplTest.kt b/paystack/src/test/java/co/paystack/android/api/PaystackRepositoryImplTest.kt index ee09cad..b18422b 100644 --- a/paystack/src/test/java/co/paystack/android/api/PaystackRepositoryImplTest.kt +++ b/paystack/src/test/java/co/paystack/android/api/PaystackRepositoryImplTest.kt @@ -1,14 +1,21 @@ package co.paystack.android.api import co.paystack.android.api.model.ChargeResponse +import co.paystack.android.api.model.TransactionInitResponse import co.paystack.android.api.request.ChargeParams +import co.paystack.android.api.request.TransactionInitRequestBody import co.paystack.android.api.service.PaystackApiService +import co.paystack.android.model.Charge import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyMap +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class PaystackRepositoryImplTest { private val apiService: PaystackApiService = mock() @@ -24,6 +31,43 @@ class PaystackRepositoryImplTest { verify(apiService, times(1)).chargeCard(TEST_CHARGE_PARAMS.toRequestMap()) } + @Test + fun `initializeTransaction calls PaystackApiService with correct params`() { + whenever(apiService.initializeTransaction(anyMap())) + .thenReturn(FakeCall.success(TransactionInitResponse("success", "trans_id"))) + val repository = PaystackRepositoryImpl(apiService) + val apiCallback = mock>() + val testMetadata = "internal_tag" to "tag_internal_example" + val charge = Charge().apply { + email = testEmail + amount = testAmount + currency = testCurrency + reference = testReference + subaccount = "subacc_13850248" + transactionCharge = 1000 + plan = "PLN_123456789" + bearer = Charge.Bearer.subaccount + putMetadata(testMetadata.first, testMetadata.second) + } + repository.initializeTransaction(testPublicKey, charge, testDeviceId, apiCallback) + + val expectedApiCallMap = mapOf( + TransactionInitRequestBody.FIELD_KEY to testPublicKey, + TransactionInitRequestBody.FIELD_EMAIL to charge.email, + TransactionInitRequestBody.FIELD_AMOUNT to charge.amount, + TransactionInitRequestBody.FIELD_CURRENCY to charge.currency, + TransactionInitRequestBody.FIELD_METADATA to charge.metadata, + TransactionInitRequestBody.FIELD_DEVICE to testDeviceId, + TransactionInitRequestBody.FIELD_REFERENCE to charge.reference, + TransactionInitRequestBody.FIELD_SUBACCOUNT to charge.subaccount, + TransactionInitRequestBody.FIELD_TRANSACTION_CHARGE to charge.transactionCharge, + TransactionInitRequestBody.FIELD_BEARER to charge.bearer.name, + TransactionInitRequestBody.FIELD_PLAN to charge.plan, + ) + + verify(apiService).initializeTransaction(expectedApiCallMap) + } + companion object { const val testPublicKey = "pk_live_123445677555" const val testEmail = "michael@paystack.com" @@ -31,6 +75,7 @@ class PaystackRepositoryImplTest { const val testAmount = 10000 const val testCurrency = "NGN" const val testTransactionId = "123458685949" + const val testReference = "ref_123458685949" val TEST_CHARGE_PARAMS = ChargeParams( clientData = "encryptedClientData",