From a8c811f6cdf4bf1b787ebaaa9fd220588fd1ffcf Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 22 Jul 2020 16:53:06 -0300 Subject: [pos] migrate order posting and checking to v1 API and merchant-lib --- .../main/java/net/taler/merchantlib/MerchantApi.kt | 48 ++++++++- .../java/net/taler/merchantlib/MerchantConfig.kt | 37 +++++++ .../java/net/taler/merchantlib/PostOrderRequest.kt | 83 +++++++++++++++ .../main/java/net/taler/merchantlib/Response.kt | 63 ++++++++++++ .../java/net/taler/merchantlib/MerchantApiTest.kt | 95 +++++++++++++++++ .../java/net/taler/merchantlib/MockHttpClient.kt | 33 +++++- .../java/net/taler/merchantlib/TestResponse.kt | 29 ++++++ merchant-terminal/.gitlab-ci.yml | 1 + .../java/net/taler/merchantpos/MainViewModel.kt | 2 +- .../net/taler/merchantpos/config/MerchantConfig.kt | 5 +- .../taler/merchantpos/config/MerchantRequest.kt | 1 - .../main/java/net/taler/merchantpos/order/Order.kt | 21 ++++ .../net/taler/merchantpos/order/OrderManager.kt | 2 +- .../java/net/taler/merchantpos/payment/Payment.kt | 2 +- .../taler/merchantpos/payment/PaymentManager.kt | 113 ++++++--------------- .../merchantpos/payment/ProcessPaymentFragment.kt | 6 +- merchant-terminal/src/main/res/values/strings.xml | 2 + 17 files changed, 448 insertions(+), 95 deletions(-) create mode 100644 merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt create mode 100644 merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt create mode 100644 merchant-lib/src/main/java/net/taler/merchantlib/Response.kt create mode 100644 merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt index 3406f78..335e42d 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt @@ -21,6 +21,16 @@ import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.json.serializer.KotlinxSerializer import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.http.ContentType.Application.Json +import io.ktor.http.HttpHeaders.Authorization +import io.ktor.http.contentType +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonConfiguration +import net.taler.common.ContractTerms +import net.taler.merchantlib.Response.Companion.failure +import net.taler.merchantlib.Response.Companion.success class MerchantApi(private val httpClient: HttpClient) { @@ -30,10 +40,46 @@ class MerchantApi(private val httpClient: HttpClient) { return httpClient.get("$baseUrl/config") } + suspend fun postOrder( + merchantConfig: MerchantConfig, + contractTerms: ContractTerms + ): Response = response { + httpClient.post(merchantConfig.urlFor("private/orders")) { + header(Authorization, "ApiKey ${merchantConfig.apiKey}") + contentType(Json) + body = PostOrderRequest(contractTerms) + } as PostOrderResponse + } + + suspend fun checkOrder( + merchantConfig: MerchantConfig, + orderId: String + ): Response = response { + httpClient.get(merchantConfig.urlFor("private/orders/$orderId")) { + header(Authorization, "ApiKey ${merchantConfig.apiKey}") + } as CheckPaymentResponse + } + + private suspend fun response(request: suspend () -> T): Response { + return try { + success(request()) + } catch (e: Throwable) { + failure(e) + } + } } private fun getDefaultHttpClient(): HttpClient = HttpClient(OkHttp) { install(JsonFeature) { - serializer = KotlinxSerializer() + serializer = getSerializer() } } + +fun getSerializer() = KotlinxSerializer( + Json( + JsonConfiguration( + encodeDefaults = false, + ignoreUnknownKeys = true + ) + ) +) diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt new file mode 100644 index 0000000..71185b9 --- /dev/null +++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt @@ -0,0 +1,37 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantlib + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MerchantConfig( + @SerialName("base_url") + val baseUrl: String, + val instance: String, + @SerialName("api_key") + val apiKey: String +) { + fun urlFor(endpoint: String, params: Map? = null): String { + val sb = StringBuilder(baseUrl) + if (sb.last() != '/') sb.append('/') + sb.append("instances/$instance/") + sb.append(endpoint) + return sb.toString() + } +} diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt b/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt new file mode 100644 index 0000000..a6e74d6 --- /dev/null +++ b/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt @@ -0,0 +1,83 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantlib + +import kotlinx.serialization.Decoder +import kotlinx.serialization.Encoder +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.json.JsonInput +import kotlinx.serialization.json.JsonObject +import net.taler.common.ContractTerms + +@Serializable +data class PostOrderRequest( + @SerialName("order") + val contractTerms: ContractTerms + +) + +@Serializable +data class PostOrderResponse( + @SerialName("order_id") + val orderId: String +) + +@Serializable +sealed class CheckPaymentResponse { + abstract val paid: Boolean + + @Serializer(forClass = CheckPaymentResponse::class) + companion object : KSerializer { + override fun deserialize(decoder: Decoder): CheckPaymentResponse { + val input = decoder as JsonInput + val tree = input.decodeJson() as JsonObject + val paid = tree.getPrimitive("paid").boolean +// return if (paid) decoder.json.fromJson(Paid.serializer(), tree) +// else decoder.json.fromJson(Unpaid.serializer(), tree) + // manual parsing due to https://github.com/Kotlin/kotlinx.serialization/issues/576 + return if (paid) Paid( + refunded = tree.getPrimitive("refunded").boolean + ) else Unpaid( + talerPayUri = tree.getPrimitive("taler_pay_uri").content + ) + } + + override fun serialize(encoder: Encoder, value: CheckPaymentResponse) = when (value) { + is Unpaid -> Unpaid.serializer().serialize(encoder, value) + is Paid -> Paid.serializer().serialize(encoder, value) + } + } + + @Serializable + data class Unpaid( + override val paid: Boolean = false, + @SerialName("taler_pay_uri") + val talerPayUri: String, + @SerialName("already_paid_order_id") + val alreadyPaidOrderId: String? = null + ) : CheckPaymentResponse() + + @Serializable + data class Paid( + override val paid: Boolean = true, + val refunded: Boolean + ) : CheckPaymentResponse() + +} diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt new file mode 100644 index 0000000..23fa101 --- /dev/null +++ b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt @@ -0,0 +1,63 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantlib + +import io.ktor.client.call.receive +import io.ktor.client.features.ClientRequestException +import kotlinx.serialization.Serializable + +class Response private constructor( + private val value: Any? +) { + + companion object { + fun success(value: T): Response = + Response(value) + + fun failure(e: Throwable): Response = + Response(Failure(e)) + } + + val isFailure: Boolean get() = value is Failure + + suspend fun handle(onFailure: ((String) -> Any)? = null, onSuccess: ((T) -> Any)? = null) { + if (value is Failure) onFailure?.let { it(getFailureString(value)) } + else onSuccess?.let { + @Suppress("UNCHECKED_CAST") + it(value as T) + } + } + + private suspend fun getFailureString(failure: Failure): String = when (failure.exception) { + is ClientRequestException -> getExceptionString(failure.exception) + else -> failure.exception.toString() + } + + private suspend fun getExceptionString(e: ClientRequestException): String { + val error: Error = e.response.receive() + return "Error ${error.code}: ${error.hint}" + } + + private class Failure(val exception: Throwable) + + @Serializable + private class Error( + val code: Int?, + val hint: String? + ) + +} diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt index 6b2199b..de1ca33 100644 --- a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt +++ b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt @@ -16,15 +16,25 @@ package net.taler.merchantlib +import io.ktor.http.HttpStatusCode import kotlinx.coroutines.runBlocking +import net.taler.common.Amount +import net.taler.common.ContractProduct +import net.taler.common.ContractTerms import net.taler.merchantlib.MockHttpClient.giveJsonResponse import net.taler.merchantlib.MockHttpClient.httpClient import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test class MerchantApiTest { private val api = MerchantApi(httpClient) + private val merchantConfig = MerchantConfig( + baseUrl = "http://example.net/", + instance = "testInstance", + apiKey = "apiKeyFooBar" + ) @Test fun testGetConfig() = runBlocking { @@ -40,4 +50,89 @@ class MerchantApiTest { assertEquals(ConfigResponse("0:0:0", "INTKUDOS"), response) } + @Test + fun testPostOrder() = runBlocking { + val product = ContractProduct( + productId = "foo", + description = "bar", + price = Amount("TEST", 1, 0), + quantity = 2 + ) + val contractTerms = ContractTerms( + summary = "test", + amount = Amount("TEST", 2, 1), + fulfillmentUrl = "http://example.org", + products = listOf(product) + ) + val contractTermsJson = """ + { + "order": { + "summary": "${contractTerms.summary}", + "amount": "${contractTerms.amount.toJSONString()}", + "fulfillment_url": "${contractTerms.fulfillmentUrl}", + "products": [ + { + "product_id": "${product.productId}", + "description": "${product.description}", + "price": "${product.price.toJSONString()}", + "quantity": ${product.quantity} + } + ] + } + } + """.trimIndent() + httpClient.giveJsonResponse( + "http://example.net/instances/testInstance/private/orders", + contractTermsJson + ) { + """{"order_id": "test"}""" + } + api.postOrder(merchantConfig, contractTerms).assertSuccess { + assertEquals(PostOrderResponse("test"), it) + } + + httpClient.giveJsonResponse( + "http://example.net/instances/testInstance/private/orders", + statusCode = HttpStatusCode.NotFound + ) { + """{ + "code": 2000, + "hint": "merchant instance unknown" + }""" + } + api.postOrder(merchantConfig, contractTerms).assertFailure { + assertTrue(it.contains("2000")) + assertTrue(it.contains("merchant instance unknown")) + } + } + + @Test + fun testCheckOrder() = runBlocking { + val orderId = "orderIdFoo" + val unpaidResponse = CheckPaymentResponse.Unpaid(false, "http://taler.net/foo") + httpClient.giveJsonResponse("http://example.net/instances/testInstance/private/orders/$orderId") { + """{ + "paid": ${unpaidResponse.paid}, + "taler_pay_uri": "${unpaidResponse.talerPayUri}" + }""".trimIndent() + } + api.checkOrder(merchantConfig, orderId).assertSuccess { + assertEquals(unpaidResponse, it) + } + + httpClient.giveJsonResponse( + "http://example.net/instances/testInstance/private/orders/$orderId", + statusCode = HttpStatusCode.NotFound + ) { + """{ + "code": 2909, + "hint": "Did not find contract terms for order in DB" + }""" + } + api.checkOrder(merchantConfig, orderId).assertFailure { + assertTrue(it.contains("2909")) + assertTrue(it.contains("Did not find contract terms for order in DB")) + } + } + } diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt b/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt index 076b77e..993be15 100644 --- a/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt +++ b/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt @@ -21,31 +21,50 @@ import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.MockEngineConfig import io.ktor.client.engine.mock.respond import io.ktor.client.features.json.JsonFeature -import io.ktor.client.features.json.serializer.KotlinxSerializer +import io.ktor.client.features.logging.LogLevel +import io.ktor.client.features.logging.Logger +import io.ktor.client.features.logging.Logging +import io.ktor.client.features.logging.SIMPLE import io.ktor.http.ContentType.Application.Json +import io.ktor.http.HttpStatusCode import io.ktor.http.Url +import io.ktor.http.content.TextContent import io.ktor.http.fullPath import io.ktor.http.headersOf import io.ktor.http.hostWithPort +import org.junit.Assert.assertEquals object MockHttpClient { val httpClient = HttpClient(MockEngine) { install(JsonFeature) { - serializer = KotlinxSerializer() + serializer = getSerializer() + } + install(Logging) { + logger = Logger.SIMPLE + level = LogLevel.ALL } engine { addHandler { error("No response handler set") } } } - fun HttpClient.giveJsonResponse(url: String, jsonProducer: () -> String) { + fun HttpClient.giveJsonResponse( + url: String, + expectedBody: String? = null, + statusCode: HttpStatusCode = HttpStatusCode.OK, + jsonProducer: () -> String + ) { val httpConfig = engineConfig as MockEngineConfig httpConfig.requestHandlers.removeAt(0) httpConfig.requestHandlers.add { request -> if (request.url.fullUrl == url) { val headers = headersOf("Content-Type" to listOf(Json.toString())) - respond(jsonProducer(), headers = headers) + if (expectedBody != null) { + val content = request.body as TextContent + assertJsonEquals(expectedBody, content.text) + } + respond(jsonProducer(), headers = headers, status = statusCode) } else { error("Unexpected URL: ${request.url.fullUrl}") } @@ -55,4 +74,10 @@ object MockHttpClient { private val Url.hostWithPortIfRequired: String get() = if (port == protocol.defaultPort) host else hostWithPort private val Url.fullUrl: String get() = "${protocol.name}://$hostWithPortIfRequired$fullPath" + private fun assertJsonEquals(json1: String, json2: String) { + val parsed1 = kotlinx.serialization.json.Json.parseJson(json1) + val parsed2 = kotlinx.serialization.json.Json.parseJson(json2) + assertEquals(parsed1, parsed2) + } + } diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt b/merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt new file mode 100644 index 0000000..0d3d906 --- /dev/null +++ b/merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt @@ -0,0 +1,29 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantlib + +import org.junit.Assert + +internal suspend fun Response.assertSuccess(assertions: (T) -> Any) { + Assert.assertFalse(isFailure) + handle(onSuccess = { assertions(it) }) +} + +internal suspend fun Response.assertFailure(assertions: (String) -> Any) { + Assert.assertTrue(isFailure) + handle(onFailure = { assertions(it) }) +} diff --git a/merchant-terminal/.gitlab-ci.yml b/merchant-terminal/.gitlab-ci.yml index 034818c..74ac21f 100644 --- a/merchant-terminal/.gitlab-ci.yml +++ b/merchant-terminal/.gitlab-ci.yml @@ -3,6 +3,7 @@ merchant_test: only: changes: - merchant-terminal/**/* + - merchant-lib/**/* - taler-kotlin-common/**/* - build.gradle script: ./gradlew :merchant-terminal:check :merchant-terminal:assembleRelease diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt index 2dd2c24..ce05980 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt @@ -42,7 +42,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { val configManager = ConfigManager(app, viewModelScope, api, mapper, queue).apply { addConfigurationReceiver(orderManager) } - val paymentManager = PaymentManager(configManager, queue, mapper) + val paymentManager = PaymentManager(app, configManager, viewModelScope, api) val historyManager = HistoryManager(configManager, queue, mapper) val refundManager = RefundManager(configManager, queue) diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt index 0e707d3..0c7e3b7 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt @@ -23,7 +23,7 @@ import net.taler.common.Amount import net.taler.common.ContractProduct import net.taler.common.Product import net.taler.common.TalerUtils -import java.util.* +import java.util.UUID data class Config( val configUrl: String, @@ -50,6 +50,9 @@ data class MerchantConfig( } return uriBuilder.toString() } + fun convert() = net.taler.merchantlib.MerchantConfig( + baseUrl, instance, apiKey + ) } data class Category( diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt index 6c9c741..9cfae94 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt @@ -16,7 +16,6 @@ package net.taler.merchantpos.config - import android.util.ArrayMap import com.android.volley.Response import com.android.volley.toolbox.JsonObjectRequest diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt index ff6e6b7..bb75362 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt @@ -17,8 +17,13 @@ package net.taler.merchantpos.order import net.taler.common.Amount +import net.taler.common.ContractTerms +import net.taler.common.now import net.taler.merchantpos.config.Category import net.taler.merchantpos.config.ConfigProduct +import java.net.URLEncoder + +private const val FULFILLMENT_PREFIX = "taler://fulfillment-success/" data class Order(val id: Int, val currency: String, val availableCategories: Map) { val products = ArrayList() @@ -103,4 +108,20 @@ data class Order(val id: Int, val currency: String, val availableCategories: Map }.toMap() } + private val fulfillmentUri: String + get() { + val fulfillmentId = "${now()}-${hashCode()}" + return "$FULFILLMENT_PREFIX${URLEncoder.encode(summary, "UTF-8")}#$fulfillmentId" + } + + fun toContractTerms(): ContractTerms { + return ContractTerms( + summary = summary, + summaryI18n = summaryI18n, + amount = total, + fulfillmentUrl = fulfillmentUri, + products = products.map { it.toContractProduct() } + ) + } + } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt index ff2be48..46ea238 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt @@ -113,7 +113,7 @@ class OrderManager( @UiThread internal fun getOrder(orderId: Int): LiveOrder { - return orders[orderId] ?: throw IllegalArgumentException() + return orders[orderId] ?: throw IllegalArgumentException("Order not found: $orderId") } @UiThread diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt index b7e4a4b..9200ced 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt @@ -25,5 +25,5 @@ data class Payment( val orderId: String? = null, val talerPayUri: String? = null, val paid: Boolean = false, - val error: Boolean = false + val error: String? = null ) diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt index 9138740..e238284 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt @@ -16,42 +16,33 @@ package net.taler.merchantpos.payment +import android.content.Context import android.os.CountDownTimer -import android.util.Log import androidx.annotation.UiThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.android.volley.Request.Method.GET -import com.android.volley.Request.Method.POST -import com.android.volley.RequestQueue -import com.android.volley.Response.Listener -import com.fasterxml.jackson.databind.ObjectMapper -import net.taler.common.Timestamp -import net.taler.common.now -import net.taler.merchantpos.LogErrorListener +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import net.taler.merchantlib.CheckPaymentResponse +import net.taler.merchantlib.MerchantApi +import net.taler.merchantlib.PostOrderResponse +import net.taler.merchantpos.R import net.taler.merchantpos.config.ConfigManager -import net.taler.merchantpos.config.MerchantRequest import net.taler.merchantpos.order.Order -import org.json.JSONArray -import org.json.JSONObject -import java.net.URLEncoder import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.SECONDS private val TIMEOUT = MINUTES.toMillis(2) private val CHECK_INTERVAL = SECONDS.toMillis(1) -private const val FULFILLMENT_PREFIX = "taler://fulfillment-success/" class PaymentManager( + private val context: Context, private val configManager: ConfigManager, - private val queue: RequestQueue, - private val mapper: ObjectMapper + private val scope: CoroutineScope, + private val api: MerchantApi ) { - companion object { - val TAG = PaymentManager::class.java.simpleName - } - private val mPayment = MutableLiveData() val payment: LiveData = mPayment @@ -63,93 +54,51 @@ class PaymentManager( } override fun onFinish() { - payment.value?.copy(error = true)?.let { mPayment.value = it } + val str = context.getString(R.string.error_timeout) + payment.value?.copy(error = str)?.let { mPayment.value = it } } } @UiThread fun createPayment(order: Order) { val merchantConfig = configManager.merchantConfig!! - - val currency = merchantConfig.currency!! - val summary = order.summary - val summaryI18n = order.summaryI18n - val now = now() - val deadline = Timestamp(now + MINUTES.toMillis(120)) - - mPayment.value = Payment(order, summary, currency) - - val fulfillmentId = "${now}-${order.hashCode()}" - val fulfillmentUrl = - "${FULFILLMENT_PREFIX}${URLEncoder.encode(summary, "UTF-8")}#$fulfillmentId" - val body = JSONObject().apply { - put("order", JSONObject().apply { - put("amount", order.total.toJSONString()) - put("summary", summary) - if (summaryI18n != null) put("summary_i18n", order.summaryI18n) - // fulfillment_url needs to be unique per order - put("fulfillment_url", fulfillmentUrl) - put("instance", "default") - put("wire_transfer_deadline", JSONObject(mapper.writeValueAsString(deadline))) - put("refund_deadline", JSONObject(mapper.writeValueAsString(deadline))) - put("products", order.getProductsJson()) - }) + mPayment.value = Payment(order, order.summary, merchantConfig.currency!!) + scope.launch(Dispatchers.IO) { + val response = api.postOrder(merchantConfig.convert(), order.toContractTerms()) + response.handle(::onNetworkError, ::onOrderCreated) } - - Log.d(TAG, body.toString(4)) - - val req = MerchantRequest(POST, merchantConfig, "order", null, body, - Listener { onOrderCreated(it) }, - LogErrorListener { onNetworkError() } - ) - queue.add(req) - } - - private fun Order.getProductsJson(): JSONArray { - val contractProducts = products.map { it.toContractProduct() } - val productsStr = mapper.writeValueAsString(contractProducts) - return JSONArray(productsStr) } - private fun onOrderCreated(orderResponse: JSONObject) { - val orderId = orderResponse.getString("order_id") - mPayment.value = mPayment.value!!.copy(orderId = orderId) + private fun onOrderCreated(orderResponse: PostOrderResponse) = scope.launch(Dispatchers.Main) { + mPayment.value = mPayment.value!!.copy(orderId = orderResponse.orderId) checkTimer.start() } private fun checkPayment(orderId: String) { val merchantConfig = configManager.merchantConfig!! - val params = mapOf( - "order_id" to orderId, - "instance" to merchantConfig.instance - ) - - val req = MerchantRequest(GET, merchantConfig, "check-payment", params, null, - Listener { onPaymentChecked(it) }, - LogErrorListener { onNetworkError() }) - queue.add(req) + scope.launch(Dispatchers.IO) { + val response = api.checkOrder(merchantConfig.convert(), orderId) + response.handle(::onNetworkError, ::onPaymentChecked) + } } - /** - * Called when the /check-payment response gave a result. - */ - private fun onPaymentChecked(checkPaymentResponse: JSONObject) { + private fun onPaymentChecked(response: CheckPaymentResponse) = scope.launch(Dispatchers.Main) { val currentValue = requireNotNull(mPayment.value) - if (checkPaymentResponse.getBoolean("paid")) { + if (response.paid) { mPayment.value = currentValue.copy(paid = true) checkTimer.cancel() } else if (currentValue.talerPayUri == null) { - val talerPayUri = checkPaymentResponse.getString("taler_pay_uri") - mPayment.value = currentValue.copy(talerPayUri = talerPayUri) + response as CheckPaymentResponse.Unpaid + mPayment.value = currentValue.copy(talerPayUri = response.talerPayUri) } } - private fun onNetworkError() { - cancelPayment() + private fun onNetworkError(error: String) = scope.launch(Dispatchers.Main) { + cancelPayment(error) } - fun cancelPayment() { - mPayment.value = mPayment.value!!.copy(error = true) + fun cancelPayment(error: String) { + mPayment.value = mPayment.value!!.copy(error = error) checkTimer.cancel() } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt index 9060fd3..5278a03 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt @@ -61,8 +61,8 @@ class ProcessPaymentFragment : Fragment() { } private fun onPaymentStateChanged(payment: Payment) { - if (payment.error) { - topSnackbar(requireView(), R.string.error_network, LENGTH_LONG) + if (payment.error != null) { + topSnackbar(requireView(), payment.error, LENGTH_LONG) findNavController().navigateUp() return } @@ -86,7 +86,7 @@ class ProcessPaymentFragment : Fragment() { } private fun onPaymentCancel() { - paymentManager.cancelPayment() + paymentManager.cancelPayment(getString(R.string.error_cancelled)) findNavController().navigateUp() topSnackbar(requireView(), R.string.payment_canceled, LENGTH_LONG) } diff --git a/merchant-terminal/src/main/res/values/strings.xml b/merchant-terminal/src/main/res/values/strings.xml index b3dcd8d..4c0ba5a 100644 --- a/merchant-terminal/src/main/res/values/strings.xml +++ b/merchant-terminal/src/main/res/values/strings.xml @@ -64,6 +64,8 @@ Purchase reference: %1$s\n\n%2$s Network error + No payment found, please try again! + Payment cancelled Click «back» again to exit -- cgit v1.2.3