Skip to content

Commit

Permalink
MOB-183 - Fix transaction initialisation using access code. (#150)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
michael-paystack authored Feb 8, 2023
1 parent 61343e5 commit 241f0e8
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 13 deletions.
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,4 +37,4 @@ POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=paystack
POM_DEVELOPER_NAME=Paystack
POM_DEVELOPER_EMAIL=[email protected]
org.gradle.unsafe.configuration-cache=true
org.gradle.unsafe.configuration-cache=false
20 changes: 15 additions & 5 deletions paystack/src/main/java/co/paystack/android/TransactionManager.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package co.paystack.android;

import static co.paystack.android.Transaction.EMPTY_TRANSACTION;

import android.app.Activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.CountDownTimer;
import android.provider.Settings;
import android.util.Log;

import androidx.annotation.VisibleForTesting;

import org.jetbrains.annotations.NotNull;

import co.paystack.android.api.ApiCallback;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<TransactionInitResponse>() {
@VisibleForTesting
void initiateTransaction(String publicKey, Charge charge, String deviceId) {
ApiCallback<TransactionInitResponse> callback = new ApiCallback<TransactionInitResponse>() {
@Override
public void onSuccess(TransactionInitResponse data) {
Card card = charge.getCard();
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TransactionInitResponse>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -71,6 +77,14 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService
)
}

override fun getTransactionWithAccessCode(accessCode: String, callback: ApiCallback<TransactionInitResponse>) {
makeApiRequest(
onSuccess = { data -> callback.onSuccess(data) },
onError = { throwable -> callback.onError(throwable) },
apiCall = { apiService.getTransaction(accessCode) }
)
}

private fun <T> makeApiRequest(apiCall: () -> Call<T>, onSuccess: (T) -> Unit, onError: (Throwable) -> Unit) {
val retrofitCallback = object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<String, String>,
) {
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 {
Expand All @@ -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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ internal interface PaystackApiService {
@GET("/checkout/request_inline")
fun initializeTransaction(@QueryMap params: Map<String, @JvmSuppressWildcards Any>): Call<TransactionInitResponse>

@GET("/transaction/verify_access_code/{accessCode}")
fun getTransaction(@Path("accessCode") accessCode: String): Call<TransactionInitResponse>

@FormUrlEncoded
@POST("/checkout/card/charge")
@NoWrap
Expand Down
11 changes: 8 additions & 3 deletions paystack/src/main/java/co/paystack/android/ui/AuthActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,20 @@ public void handleResponse() {
}
synchronized (si) {
si.setResponseJson(responseJson);
si.notify();
si.notifyAll();
}
finish();
}

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);
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<String>(),
isA<Charge>(),
isA<String>(),
isA<ApiCallback<TransactionInitResponse>>()
)
}

@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<String>(),
isA<ApiCallback<TransactionInitResponse>>()
)
}


@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<TransactionInitResponse>
callback.onSuccess(TransactionInitResponse("success", "trans_id"))
}

val transactionManager = TransactionManager(paystackRepository)
transactionManager.initiateTransaction(publicKey, charge, deviceId)

verify(paystackRepository).processCardCharge(
isA<ChargeParams>(),
isA<ChargeApiCallback>()
)
}


companion object {
private const val TAG = "TransactionManagerTest"
val TEST_CARD = Card("5105105105105100", 2, 2024, "123")
}
}
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -24,13 +31,51 @@ 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<ApiCallback<TransactionInitResponse>>()
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 = "[email protected]"
const val testDeviceId = "test_device_id"
const val testAmount = 10000
const val testCurrency = "NGN"
const val testTransactionId = "123458685949"
const val testReference = "ref_123458685949"

val TEST_CHARGE_PARAMS = ChargeParams(
clientData = "encryptedClientData",
Expand Down

0 comments on commit 241f0e8

Please sign in to comment.