aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt48
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt37
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt83
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/Response.kt63
-rw-r--r--merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt95
-rw-r--r--merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt33
-rw-r--r--merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt29
-rw-r--r--merchant-terminal/.gitlab-ci.yml1
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt2
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt5
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt1
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt21
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt2
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt2
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt113
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt6
-rw-r--r--merchant-terminal/src/main/res/values/strings.xml2
17 files changed, 448 insertions, 95 deletions
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<PostOrderResponse> = 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<CheckPaymentResponse> = response {
+ httpClient.get(merchantConfig.urlFor("private/orders/$orderId")) {
+ header(Authorization, "ApiKey ${merchantConfig.apiKey}")
+ } as CheckPaymentResponse
+ }
+
+ private suspend fun <T> response(request: suspend () -> T): Response<T> {
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+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<String, String>? = 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 <http://www.gnu.org/licenses/>
+ */
+
+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<CheckPaymentResponse> {
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantlib
+
+import io.ktor.client.call.receive
+import io.ktor.client.features.ClientRequestException
+import kotlinx.serialization.Serializable
+
+class Response<out T> private constructor(
+ private val value: Any?
+) {
+
+ companion object {
+ fun <T> success(value: T): Response<T> =
+ Response(value)
+
+ fun <T> failure(e: Throwable): Response<T> =
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantlib
+
+import org.junit.Assert
+
+internal suspend fun <T> Response<T>.assertSuccess(assertions: (T) -> Any) {
+ Assert.assertFalse(isFailure)
+ handle(onSuccess = { assertions(it) })
+}
+
+internal suspend fun <T> Response<T>.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<Int, Category>) {
val products = ArrayList<ConfigProduct>()
@@ -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<Payment>()
val payment: LiveData<Payment> = 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 @@
<string name="refund_order_ref">Purchase reference: %1$s\n\n%2$s</string>
<string name="error_network">Network error</string>
+ <string name="error_timeout">No payment found, please try again!</string>
+ <string name="error_cancelled">Payment cancelled</string>
<string name="toast_back_to_exit">Click «back» again to exit</string>