Skip to content

Commit

Permalink
Card: Remove try/catch from Approve Order Flow (#302)
Browse files Browse the repository at this point in the history
* Rename ConfirmPaymentSourceResponse to ConfirmPaymentSourceResult.

* Remove throws from CardClient.approveOrder flow.

* Make if statement truthy.

* Clean up detekt errors.

* Remove coroutines exception handler from CardClient.approveOrder flow.
  • Loading branch information
sshropshire authored Dec 2, 2024
1 parent afda478 commit b5c9f73
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 122 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.net.Uri
import android.util.Log
import androidx.activity.ComponentActivity
import com.paypal.android.cardpayments.api.CheckoutOrdersAPI
import com.paypal.android.cardpayments.api.ConfirmPaymentSourceResult
import com.paypal.android.corepayments.CoreConfig
import com.paypal.android.corepayments.CoreCoroutineExceptionHandler
import com.paypal.android.corepayments.PayPalSDKError
Expand Down Expand Up @@ -39,12 +40,6 @@ class CardClient internal constructor(

private var approveOrderId: String? = null

// TODO: remove once try-catch is removed
private val approveOrderExceptionHandler = CoreCoroutineExceptionHandler { error ->
analytics.notifyApproveOrderUnknownError(null)
approveOrderListener?.onApproveOrderFailure(error)
}

// TODO: remove once try-catch is removed
private val vaultExceptionHandler = CoreCoroutineExceptionHandler { error ->
analytics.notifyVaultUnknownError(null)
Expand Down Expand Up @@ -76,26 +71,28 @@ class CardClient internal constructor(
approveOrderId = cardRequest.orderId
analytics.notifyApproveOrderStarted(cardRequest.orderId)

CoroutineScope(dispatcher).launch(approveOrderExceptionHandler) {
// TODO: migrate away from throwing exceptions to result objects
try {
val response = checkoutOrdersAPI.confirmPaymentSource(cardRequest)
if (response.payerActionHref == null) {
analytics.notifyApproveOrderSucceeded(response.orderId)
val result =
response.run { CardResult(orderId = orderId, status = status?.name) }
approveOrderListener?.onApproveOrderSuccess(result)
} else {
analytics.notifyApproveOrderAuthChallengeReceived(cardRequest.orderId)
approveOrderListener?.onApproveOrderThreeDSecureWillLaunch()
CoroutineScope(dispatcher).launch {
when (val response = checkoutOrdersAPI.confirmPaymentSource(cardRequest)) {
is ConfirmPaymentSourceResult.Success -> {
if (response.payerActionHref == null) {
analytics.notifyApproveOrderSucceeded(response.orderId)
val result =
response.run { CardResult(orderId = orderId, status = status?.name) }
approveOrderListener?.onApproveOrderSuccess(result)
} else {
analytics.notifyApproveOrderAuthChallengeReceived(cardRequest.orderId)
approveOrderListener?.onApproveOrderThreeDSecureWillLaunch()

val url = Uri.parse(response.payerActionHref)
val authChallenge = CardAuthChallenge.ApproveOrder(url, cardRequest)
approveOrderListener?.onApproveOrderAuthorizationRequired(authChallenge)
}
}

val url = Uri.parse(response.payerActionHref)
val authChallenge = CardAuthChallenge.ApproveOrder(url, cardRequest)
approveOrderListener?.onApproveOrderAuthorizationRequired(authChallenge)
is ConfirmPaymentSourceResult.Failure -> {
analytics.notifyApproveOrderFailed(cardRequest.orderId)
approveOrderListener?.onApproveOrderFailure(response.error)
}
} catch (error: PayPalSDKError) {
analytics.notifyApproveOrderFailed(cardRequest.orderId)
throw error
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.paypal.android.cardpayments

import com.paypal.android.cardpayments.api.ConfirmPaymentSourceResponse
import com.paypal.android.cardpayments.api.ConfirmPaymentSourceResult
import com.paypal.android.cardpayments.model.PaymentSource
import com.paypal.android.cardpayments.model.PurchaseUnit
import com.paypal.android.corepayments.APIClientError
Expand All @@ -13,8 +13,7 @@ import org.json.JSONException

internal class CardResponseParser {

@Throws(PayPalSDKError::class)
fun parseConfirmPaymentSourceResponse(httpResponse: HttpResponse): ConfirmPaymentSourceResponse =
fun parseConfirmPaymentSourceResponse(httpResponse: HttpResponse): ConfirmPaymentSourceResult =
try {
val bodyResponse = httpResponse.body!!

Expand All @@ -24,7 +23,7 @@ internal class CardResponseParser {

// this section is for 3DS
val payerActionHref = json.getLinkHref("payer-action")
ConfirmPaymentSourceResponse(
ConfirmPaymentSourceResult.Success(
id,
OrderStatus.valueOf(status),
payerActionHref,
Expand All @@ -33,7 +32,8 @@ internal class CardResponseParser {
)
} catch (ignored: JSONException) {
val correlationId = httpResponse.headers["Paypal-Debug-Id"]
throw APIClientError.dataParsingError(correlationId)
val error = APIClientError.dataParsingError(correlationId)
ConfirmPaymentSourceResult.Failure(error)
}

fun parseError(httpResponse: HttpResponse): PayPalSDKError? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ internal class CheckoutOrdersAPI(
) {
constructor(coreConfig: CoreConfig) : this(RestClient(coreConfig))

suspend fun confirmPaymentSource(cardRequest: CardRequest): ConfirmPaymentSourceResponse {
suspend fun confirmPaymentSource(cardRequest: CardRequest): ConfirmPaymentSourceResult {
val apiRequest = requestFactory.createConfirmPaymentSourceRequest(cardRequest)
val httpResponse = restClient.send(apiRequest)

val error = responseParser.parseError(httpResponse)
if (error != null) {
throw error
return if (error == null) {
responseParser.parseConfirmPaymentSourceResponse(httpResponse)
} else {
return responseParser.parseConfirmPaymentSourceResponse(httpResponse)
ConfirmPaymentSourceResult.Failure(error)
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.paypal.android.cardpayments.api

import com.paypal.android.cardpayments.model.PaymentSource
import com.paypal.android.cardpayments.model.PurchaseUnit
import com.paypal.android.corepayments.OrderStatus
import com.paypal.android.corepayments.PayPalSDKError

internal sealed class ConfirmPaymentSourceResult {

data class Success(
val orderId: String,
val status: OrderStatus? = null,
val payerActionHref: String? = null,
val paymentSource: PaymentSource? = null,
val purchaseUnits: List<PurchaseUnit>? = null
) : ConfirmPaymentSourceResult()

data class Failure(val error: PayPalSDKError) : ConfirmPaymentSourceResult()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ApplicationProvider
import com.paypal.android.cardpayments.api.CheckoutOrdersAPI
import com.paypal.android.cardpayments.api.ConfirmPaymentSourceResponse
import com.paypal.android.cardpayments.api.ConfirmPaymentSourceResult
import com.paypal.android.corepayments.OrderStatus
import com.paypal.android.corepayments.PayPalSDKError
import io.mockk.Called
Expand Down Expand Up @@ -58,8 +58,8 @@ class CardClientUnitTest {
private val paymentMethodTokensAPI = mockk<DataVaultPaymentMethodTokensAPI>(relaxed = true)

private val cardAnalytics = mockk<CardAnalytics>(relaxed = true)
private val confirmPaymentSourceResponse =
ConfirmPaymentSourceResponse(orderId, OrderStatus.APPROVED)
private val confirmPaymentSourceResult =
ConfirmPaymentSourceResult.Success(orderId, OrderStatus.APPROVED)

private val activity = mockk<FragmentActivity>(relaxed = true)

Expand Down Expand Up @@ -90,7 +90,7 @@ class CardClientUnitTest {
fun `approve order notifies listener of confirm payment source success`() = runTest {
val sut = createCardClient(testScheduler)

coEvery { checkoutOrdersAPI.confirmPaymentSource(cardRequest) } returns confirmPaymentSourceResponse
coEvery { checkoutOrdersAPI.confirmPaymentSource(cardRequest) } returns confirmPaymentSourceResult

sut.approveOrder(cardRequest)
advanceUntilIdle()
Expand All @@ -109,7 +109,9 @@ class CardClientUnitTest {
val sut = createCardClient(testScheduler)

val error = PayPalSDKError(0, "mock_error_message")
coEvery { checkoutOrdersAPI.confirmPaymentSource(cardRequest) } throws error
coEvery {
checkoutOrdersAPI.confirmPaymentSource(cardRequest)
} returns ConfirmPaymentSourceResult.Failure(error)

sut.approveOrder(cardRequest)
advanceUntilIdle()
Expand All @@ -124,7 +126,7 @@ class CardClientUnitTest {
@Test
fun `approveOrder() notifies listener when authorization is required`() = runTest {
val threeDSecureAuthChallengeResponse =
ConfirmPaymentSourceResponse(orderId, OrderStatus.APPROVED, "/payer/action/href")
ConfirmPaymentSourceResult.Success(orderId, OrderStatus.APPROVED, "/payer/action/href")

coEvery { checkoutOrdersAPI.confirmPaymentSource(cardRequest) } returns threeDSecureAuthChallengeResponse

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.paypal.android.cardpayments

import com.paypal.android.cardpayments.api.CheckoutOrdersAPI
import com.paypal.android.cardpayments.api.ConfirmPaymentSourceResult
import com.paypal.android.corepayments.APIRequest
import com.paypal.android.corepayments.HttpMethod
import com.paypal.android.corepayments.HttpResponse
import com.paypal.android.corepayments.OrderStatus
import com.paypal.android.corepayments.PayPalSDKError
import com.paypal.android.corepayments.PaymentsJSON
import com.paypal.android.corepayments.RestClient
import io.mockk.coEvery
Expand Down Expand Up @@ -103,7 +103,7 @@ class CheckoutOrdersAPIUnitTest {
val httpResponse = HttpResponse(200, headers, successBody)
coEvery { restClient.send(apiRequest) } returns httpResponse

val result = sut.confirmPaymentSource(cardRequest)
val result = sut.confirmPaymentSource(cardRequest) as ConfirmPaymentSourceResult.Success

assertEquals("test-order-id", result.orderId)
assertEquals(OrderStatus.APPROVED, result.status)
Expand All @@ -114,17 +114,11 @@ class CheckoutOrdersAPIUnitTest {
val httpResponse = HttpResponse(404, headers, errorBody)
coEvery { restClient.send(apiRequest) } returns httpResponse

lateinit var capturedError: PayPalSDKError
try {
sut.confirmPaymentSource(cardRequest)
} catch (e: PayPalSDKError) {
capturedError = e
}

val result = sut.confirmPaymentSource(cardRequest) as ConfirmPaymentSourceResult.Failure
assertEquals(
"The specified resource does not exist. -> [Issue: INVALID_RESOURCE_ID.\n" +
"Error description: Specified resource ID does not exist.]",
capturedError.errorDescription
result.error.errorDescription
)
}

Expand All @@ -135,16 +129,10 @@ class CheckoutOrdersAPIUnitTest {
val httpResponse = HttpResponse(-1, headers, errorBody)
coEvery { restClient.send(apiRequest) } returns httpResponse

lateinit var capturedError: PayPalSDKError
try {
sut.confirmPaymentSource(cardRequest)
} catch (e: PayPalSDKError) {
capturedError = e
}

val result = sut.confirmPaymentSource(cardRequest) as ConfirmPaymentSourceResult.Failure
assertEquals(
"An unknown error occurred. Contact developer.paypal.com/support.",
capturedError.errorDescription
result.error.errorDescription
)
}

Expand All @@ -155,16 +143,10 @@ class CheckoutOrdersAPIUnitTest {
val httpResponse = HttpResponse(-10, headers, emptyErrorBody)
coEvery { restClient.send(apiRequest) } returns httpResponse

lateinit var capturedError: PayPalSDKError
try {
sut.confirmPaymentSource(cardRequest)
} catch (e: PayPalSDKError) {
capturedError = e
}

val result = sut.confirmPaymentSource(cardRequest) as ConfirmPaymentSourceResult.Failure
assertEquals(
"An error occurred due to missing HTTP response data. Contact developer.paypal.com/support.",
capturedError.errorDescription
result.error.errorDescription
)
}

Expand All @@ -177,16 +159,10 @@ class CheckoutOrdersAPIUnitTest {
coEvery { restClient.send(apiRequest) } returns httpResponse
every { paymentsJSON.getString(any()) } throws parsingException

lateinit var capturedError: PayPalSDKError
try {
sut.confirmPaymentSource(cardRequest)
} catch (e: PayPalSDKError) {
capturedError = e
}

val result = sut.confirmPaymentSource(cardRequest) as ConfirmPaymentSourceResult.Failure
assertEquals(
"An error occurred parsing HTTP response data. Contact developer.paypal.com/support.",
capturedError.errorDescription
result.error.errorDescription
)
}

Expand All @@ -196,16 +172,10 @@ class CheckoutOrdersAPIUnitTest {
val httpResponse = HttpResponse(-2, headers, errorBody)
coEvery { restClient.send(apiRequest) } returns httpResponse

lateinit var capturedError: PayPalSDKError
try {
sut.confirmPaymentSource(cardRequest)
} catch (e: PayPalSDKError) {
capturedError = e
}

val result = sut.confirmPaymentSource(cardRequest) as ConfirmPaymentSourceResult.Failure
assertEquals(
"An error occurred due to an invalid HTTP response. Contact developer.paypal.com/support.",
capturedError.errorDescription
result.error.errorDescription
)
}

Expand All @@ -215,48 +185,29 @@ class CheckoutOrdersAPIUnitTest {
val httpResponse = HttpResponse(-3, headers, errorBody)
coEvery { restClient.send(apiRequest) } returns httpResponse

lateinit var capturedError: PayPalSDKError
try {
sut.confirmPaymentSource(cardRequest)
} catch (e: PayPalSDKError) {
capturedError = e
}

val result = sut.confirmPaymentSource(cardRequest) as ConfirmPaymentSourceResult.Failure
assertEquals(
"A server error occurred. Contact developer.paypal.com/support.",
capturedError.errorDescription
result.error.errorDescription
)
}

@Test
fun `when confirmPaymentSource fails to parse response, correlation ID is set in Error`() =
runTest {
coEvery { restClient.send(apiRequest) } returns HttpResponse(
200,
headers,
unexpectedBody
)
coEvery {
restClient.send(apiRequest)
} returns HttpResponse(200, headers, unexpectedBody)

lateinit var capturedError: PayPalSDKError
try {
sut.confirmPaymentSource(cardRequest)
} catch (e: PayPalSDKError) {
capturedError = e
}
assertEquals(correlationId, capturedError.correlationId)
val result = sut.confirmPaymentSource(cardRequest) as ConfirmPaymentSourceResult.Failure
assertEquals(correlationId, result.error.correlationId)
}

@Test
fun `when confirmPaymentSource is errors, correlation ID is set in Error`() = runTest {
coEvery { restClient.send(apiRequest) } returns HttpResponse(400, headers, errorBody)

lateinit var capturedError: PayPalSDKError
try {
sut.confirmPaymentSource(cardRequest)
} catch (e: PayPalSDKError) {
capturedError = e
}

assertEquals(correlationId, capturedError.correlationId)
val result = sut.confirmPaymentSource(cardRequest) as ConfirmPaymentSourceResult.Failure
assertEquals(correlationId, result.error.correlationId)
}
}

0 comments on commit b5c9f73

Please sign in to comment.