gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-taler-android] branch master updated (4bd0b7a -> a8c811f)


From: gnunet
Subject: [taler-taler-android] branch master updated (4bd0b7a -> a8c811f)
Date: Wed, 22 Jul 2020 21:53:57 +0200

This is an automated email from the git hooks/post-receive script.

torsten-grote pushed a change to branch master
in repository taler-android.

    from 4bd0b7a  [pos] config endpoint does not need authentication
     new b9d7d6e  [common] also support multi-platform 
serialization/deserialization
     new a8c811f  [pos] migrate order posting and checking to v1 API and 
merchant-lib

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 build.gradle                                       |   1 +
 merchant-lib/build.gradle                          |  14 +--
 .../main/java/net/taler/merchantlib/MerchantApi.kt |  48 ++++++++-
 .../{ConfigResponse.kt => MerchantConfig.kt}       |  29 +++---
 .../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     |  22 ++--
 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 +
 taler-kotlin-common/build.gradle                   |  10 +-
 .../src/main/java/net/taler/common/Amount.kt       |  17 ++++
 .../main/java/net/taler/common/ContractTerms.kt    |  23 +++--
 22 files changed, 459 insertions(+), 134 deletions(-)
 copy merchant-lib/src/main/java/net/taler/merchantlib/{ConfigResponse.kt => 
MerchantConfig.kt} (62%)
 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
 copy merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt 
=> merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt (65%)

diff --git a/build.gradle b/build.gradle
index 3067d22..dc530bf 100644
--- a/build.gradle
+++ b/build.gradle
@@ -11,6 +11,7 @@ buildscript {
     dependencies {
         classpath 'com.android.tools.build:gradle:4.0.1'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
         classpath 
"androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
     }
 }
diff --git a/merchant-lib/build.gradle b/merchant-lib/build.gradle
index 08da35d..93e2d4d 100644
--- a/merchant-lib/build.gradle
+++ b/merchant-lib/build.gradle
@@ -15,13 +15,12 @@
  */
 
 plugins {
-    id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
+    id 'com.android.library'
+    id 'kotlin-android'
+    id 'kotlin-android-extensions'
+    id 'kotlinx-serialization'
 }
 
-apply plugin: 'com.android.library'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
-
 android {
     compileSdkVersion 29
     //noinspection GradleDependency
@@ -46,6 +45,8 @@ android {
 }
 
 dependencies {
+    implementation project(":taler-kotlin-common")
+
     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
 
     def ktor_version = "1.3.2"
@@ -54,5 +55,6 @@ dependencies {
     implementation "io.ktor:ktor-client-serialization-jvm:$ktor_version"
 
     testImplementation 'junit:junit:4.13'
-    testApi "io.ktor:ktor-client-mock-jvm:$ktor_version"
+    testImplementation "io.ktor:ktor-client-mock-jvm:$ktor_version"
+    testImplementation "io.ktor:ktor-client-logging-jvm:$ktor_version"
 }
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/ConfigResponse.kt 
b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt
similarity index 62%
copy from merchant-lib/src/main/java/net/taler/merchantlib/ConfigResponse.kt
copy to merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt
index 49164e6..71185b9 100644
--- a/merchant-lib/src/main/java/net/taler/merchantlib/ConfigResponse.kt
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt
@@ -16,19 +16,22 @@
 
 package net.taler.merchantlib
 
+import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 
 @Serializable
-data class ConfigResponse(
-    /**
-     * libtool-style representation of the Merchant protocol version, see
-     * 
https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
-     * The format is "current:revision:age".
-     */
-    val version: String,
-
-    /**
-    Currency supported by this backend.
-     */
-    val currency: String
-)
+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-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt 
b/merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt
similarity index 65%
copy from 
merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt
copy to merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt
index b7e4a4b..0d3d906 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt
+++ b/merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt
@@ -14,16 +14,16 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-package net.taler.merchantpos.payment
+package net.taler.merchantlib
 
-import net.taler.merchantpos.order.Order
+import org.junit.Assert
 
-data class Payment(
-    val order: Order,
-    val summary: String,
-    val currency: String,
-    val orderId: String? = null,
-    val talerPayUri: String? = null,
-    val paid: Boolean = false,
-    val error: Boolean = false
-)
+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>
 
diff --git a/taler-kotlin-common/build.gradle b/taler-kotlin-common/build.gradle
index 573a329..df4b65f 100644
--- a/taler-kotlin-common/build.gradle
+++ b/taler-kotlin-common/build.gradle
@@ -14,9 +14,12 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-apply plugin: 'com.android.library'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
+plugins {
+    id 'com.android.library'
+    id 'kotlin-android'
+    id 'kotlin-android-extensions'
+    id 'kotlinx-serialization'
+}
 
 android {
     compileSdkVersion 29
@@ -59,6 +62,7 @@ dependencies {
     implementation 'com.google.zxing:core:3.4.0'  // needs minSdkVersion 24+
 
     // JSON parsing and serialization
+    implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0"
     implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2"
 
     lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha'
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
index 76cd294..992f93b 100644
--- a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
@@ -26,6 +26,11 @@ import 
com.fasterxml.jackson.databind.annotation.JsonDeserialize
 import com.fasterxml.jackson.databind.annotation.JsonSerialize
 import com.fasterxml.jackson.databind.deser.std.StdDeserializer
 import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import kotlinx.serialization.Decoder
+import kotlinx.serialization.Encoder
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Serializer
 import org.json.JSONObject
 import java.lang.Math.floorDiv
 import kotlin.math.pow
@@ -34,6 +39,7 @@ import kotlin.math.roundToInt
 class AmountParserException(msg: String? = null, cause: Throwable? = null) : 
Exception(msg, cause)
 class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : 
Exception(msg, cause)
 
+@Serializable(with = KotlinXAmountSerializer::class)
 @JsonSerialize(using = AmountSerializer::class)
 @JsonDeserialize(using = AmountDeserializer::class)
 data class Amount(
@@ -211,6 +217,17 @@ data class Amount(
 
 }
 
+@Serializer(forClass = Amount::class)
+object KotlinXAmountSerializer: KSerializer<Amount> {
+    override fun serialize(encoder: Encoder, value: Amount) {
+        encoder.encodeString(value.toJSONString())
+    }
+
+    override fun deserialize(decoder: Decoder): Amount {
+        return Amount.fromJSONString(decoder.decodeString())
+    }
+}
+
 class AmountSerializer : StdSerializer<Amount>(Amount::class.java) {
     override fun serialize(value: Amount, gen: JsonGenerator, provider: 
SerializerProvider) {
         gen.writeString(value.toJSONString())
diff --git 
a/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt 
b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
index 8b8e02d..c07127a 100644
--- a/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
+++ b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt
@@ -23,13 +23,20 @@ import com.fasterxml.jackson.annotation.JsonInclude
 import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY
 import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL
 import com.fasterxml.jackson.annotation.JsonProperty
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
 import net.taler.common.TalerUtils.getLocalizedString
 
+@Serializable
 @JsonIgnoreProperties(ignoreUnknown = true)
 data class ContractTerms(
     val summary: String,
-    val products: List<ContractProduct>,
-    val amount: Amount
+    @SerialName("summary_i18n")
+    val summaryI18n: Map<String, String>? = null,
+    val amount: Amount,
+    @SerialName("fulfillment_url")
+    val fulfillmentUrl: String,
+    val products: List<ContractProduct>
 )
 
 @JsonInclude(NON_NULL)
@@ -52,13 +59,17 @@ abstract class Product {
         get() = getLocalizedString(descriptionI18n, description)
 }
 
+@Serializable
 data class ContractProduct(
-    override val productId: String?,
+    @SerialName("product_id")
+    override val productId: String? = null,
     override val description: String,
-    override val descriptionI18n: Map<String, String>?,
+    @SerialName("description_i18n")
+    override val descriptionI18n: Map<String, String>? = null,
     override val price: Amount,
-    override val location: String?,
-    override val image: String?,
+    @SerialName("delivery_location")
+    override val location: String? = null,
+    override val image: String? = null,
     val quantity: Int
 ) : Product() {
     @get:JsonIgnore

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]