[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.
- [taler-taler-android] branch master updated (4bd0b7a -> a8c811f),
gnunet <=