gnunet-svn
[Top][All Lists]
Advanced

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

[taler-taler-android] branch master updated (a8c811f -> 8eb241c)


From: gnunet
Subject: [taler-taler-android] branch master updated (a8c811f -> 8eb241c)
Date: Thu, 23 Jul 2020 20:44:35 +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 a8c811f  [pos] migrate order posting and checking to v1 API and 
merchant-lib
     new 08b10a2  [pos] delete unpaid/unclaimed orders when user cancels or 
timeout happens
     new 8eb241c  [pos] refactor configuration fetching and validation

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                          |   9 +-
 .../main/java/net/taler/merchantlib/MerchantApi.kt |  38 +++---
 .../java/net/taler/merchantlib/MerchantConfig.kt   |   5 +-
 .../main/java/net/taler/merchantlib/Response.kt    |  27 ++++-
 .../java/net/taler/merchantlib/MerchantApiTest.kt  |   5 +-
 merchant-terminal/build.gradle                     |  12 +-
 .../java/net/taler/merchantpos/MainViewModel.kt    |   9 +-
 .../net/taler/merchantpos/config/ConfigManager.kt  | 106 +++++++++--------
 .../taler/merchantpos/config/MerchantRequest.kt    |  14 ++-
 .../config/{MerchantConfig.kt => PosConfig.kt}     |  52 ++++-----
 .../taler/merchantpos/history/HistoryManager.kt    |   2 +-
 .../net/taler/merchantpos/order/OrderFragment.kt   |   2 +-
 .../net/taler/merchantpos/order/OrderManager.kt    |  31 ++---
 .../taler/merchantpos/payment/PaymentManager.kt    |  23 +++-
 .../taler/merchantpos/order/OrderManagerTest.kt    | 128 +++++++--------------
 taler-kotlin-common/build.gradle                   |   2 +-
 17 files changed, 230 insertions(+), 236 deletions(-)
 rename 
merchant-terminal/src/main/java/net/taler/merchantpos/config/{MerchantConfig.kt 
=> PosConfig.kt} (65%)

diff --git a/build.gradle b/build.gradle
index dc530bf..76f687e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,6 @@
 buildscript {
     ext.kotlin_version = '1.3.72'
+    ext.ktor_version = "1.3.2"
     ext.nav_version = "2.3.0"
     ext.lifecycle_version = "2.2.0"
     // check https://android-rebuilds.beuc.net/ for availability of free build 
tools
diff --git a/merchant-lib/build.gradle b/merchant-lib/build.gradle
index 93e2d4d..128f4c1 100644
--- a/merchant-lib/build.gradle
+++ b/merchant-lib/build.gradle
@@ -45,14 +45,13 @@ android {
 }
 
 dependencies {
-    implementation project(":taler-kotlin-common")
+    api project(":taler-kotlin-common")
 
     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
 
-    def ktor_version = "1.3.2"
-    implementation "io.ktor:ktor-client:$ktor_version"
-    implementation "io.ktor:ktor-client-okhttp:$ktor_version"
-    implementation "io.ktor:ktor-client-serialization-jvm:$ktor_version"
+    api "io.ktor:ktor-client:$ktor_version"
+    api "io.ktor:ktor-client-okhttp:$ktor_version"
+    api "io.ktor:ktor-client-serialization-jvm:$ktor_version"
 
     testImplementation 'junit:junit:4.13'
     testImplementation "io.ktor:ktor-client-mock-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 335e42d..e995724 100644
--- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
@@ -16,28 +16,29 @@
 
 package net.taler.merchantlib
 
+import android.util.Log
 import io.ktor.client.HttpClient
 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.delete
 import io.ktor.client.request.get
 import io.ktor.client.request.header
 import io.ktor.client.request.post
+import io.ktor.client.statement.HttpResponse
+import io.ktor.client.statement.readBytes
 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
+import net.taler.merchantlib.Response.Companion.response
 
 class MerchantApi(private val httpClient: HttpClient) {
 
-    constructor() : this(getDefaultHttpClient())
-
-    suspend fun getConfig(baseUrl: String): ConfigResponse {
-        return httpClient.get("$baseUrl/config")
+    suspend fun getConfig(baseUrl: String): Response<ConfigResponse> = 
response {
+        httpClient.get("$baseUrl/config") as ConfigResponse
     }
 
     suspend fun postOrder(
@@ -60,16 +61,27 @@ class MerchantApi(private val httpClient: HttpClient) {
         } as CheckPaymentResponse
     }
 
-    private suspend fun <T> response(request: suspend () -> T): Response<T> {
-        return try {
-            success(request())
-        } catch (e: Throwable) {
-            failure(e)
-        }
+    suspend fun deleteOrder(
+        merchantConfig: MerchantConfig,
+        orderId: String
+    ): Response<HttpResponse> = response {
+        val resp = 
httpClient.delete(merchantConfig.urlFor("private/orders/$orderId")) {
+            header(Authorization, "ApiKey ${merchantConfig.apiKey}")
+        } as HttpResponse
+        // TODO remove when the API call was fixed
+        Log.e("TEST", "status: ${resp.status.value}")
+        Log.e("TEST", String(resp.readBytes()))
+        resp
     }
+
 }
 
-private fun getDefaultHttpClient(): HttpClient = HttpClient(OkHttp) {
+fun getDefaultHttpClient(): HttpClient = HttpClient(OkHttp) {
+    engine {
+        config {
+            retryOnConnectionFailure(true)
+        }
+    }
     install(JsonFeature) {
         serializer = getSerializer()
     }
diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt 
b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt
index 71185b9..a01624e 100644
--- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt
@@ -23,11 +23,12 @@ import kotlinx.serialization.Serializable
 data class MerchantConfig(
     @SerialName("base_url")
     val baseUrl: String,
-    val instance: String,
+    // TODO remove instance when it is part of baseURL
+    val instance: String? = null,
     @SerialName("api_key")
     val apiKey: String
 ) {
-    fun urlFor(endpoint: String, params: Map<String, String>? = null): String {
+    fun urlFor(endpoint: String): String {
         val sb = StringBuilder(baseUrl)
         if (sb.last() != '/') sb.append('/')
         sb.append("instances/$instance/")
diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt 
b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
index 23fa101..1b49d78 100644
--- a/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
@@ -25,6 +25,14 @@ class Response<out T> private constructor(
 ) {
 
     companion object {
+        suspend fun <T> response(request: suspend () -> T): Response<T> {
+            return try {
+                success(request())
+            } catch (e: Throwable) {
+                failure(e)
+            }
+        }
+
         fun <T> success(value: T): Response<T> =
             Response(value)
 
@@ -42,14 +50,29 @@ class Response<out T> private constructor(
         }
     }
 
+    suspend fun handleSuspend(
+        onFailure: ((String) -> Any)? = null,
+        onSuccess: (suspend (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}"
+        return try {
+            val error: Error = e.response.receive()
+            "Error ${error.code}: ${error.hint}"
+        } catch (ex: Exception) {
+            "Status code: ${e.response.status.value}"
+        }
     }
 
     private class Failure(val exception: Throwable)
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 de1ca33..ea5a12a 100644
--- a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
+++ b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
@@ -46,8 +46,9 @@ class MerchantApiTest {
             }
             """.trimIndent()
         }
-        val response = api.getConfig("https://backend.int.taler.net";)
-        assertEquals(ConfigResponse("0:0:0", "INTKUDOS"), response)
+        api.getConfig("https://backend.int.taler.net";).assertSuccess {
+            assertEquals(ConfigResponse("0:0:0", "INTKUDOS"), it)
+        }
     }
 
     @Test
diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle
index 2ba1a66..1cec0c5 100644
--- a/merchant-terminal/build.gradle
+++ b/merchant-terminal/build.gradle
@@ -1,7 +1,10 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
-apply plugin: "androidx.navigation.safeargs.kotlin"
+plugins {
+    id 'com.android.application'
+    id 'kotlin-android'
+    id 'kotlin-android-extensions'
+    id 'kotlinx-serialization'
+    id 'androidx.navigation.safeargs.kotlin'
+}
 
 android {
     compileSdkVersion 29
@@ -54,7 +57,6 @@ android {
 }
 
 dependencies {
-    implementation project(":taler-kotlin-common")
     implementation project(":merchant-lib")
 
     implementation 'com.google.android.material:material:1.1.0'
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 ce05980..b62c550 100644
--- a/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt
@@ -24,6 +24,7 @@ import 
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PRO
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.module.kotlin.KotlinModule
 import net.taler.merchantlib.MerchantApi
+import net.taler.merchantlib.getDefaultHttpClient
 import net.taler.merchantpos.config.ConfigManager
 import net.taler.merchantpos.history.HistoryManager
 import net.taler.merchantpos.history.RefundManager
@@ -32,14 +33,15 @@ import net.taler.merchantpos.payment.PaymentManager
 
 class MainViewModel(app: Application) : AndroidViewModel(app) {
 
-    private val api = MerchantApi()
+    private val httpClient = getDefaultHttpClient()
+    private val api = MerchantApi(httpClient)
     private val mapper = ObjectMapper()
         .registerModule(KotlinModule())
         .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
     private val queue = Volley.newRequestQueue(app)
 
-    val orderManager = OrderManager(app, mapper)
-    val configManager = ConfigManager(app, viewModelScope, api, mapper, 
queue).apply {
+    val orderManager = OrderManager(app)
+    val configManager = ConfigManager(app, viewModelScope, httpClient, 
api).apply {
         addConfigurationReceiver(orderManager)
     }
     val paymentManager = PaymentManager(app, configManager, viewModelScope, 
api)
@@ -47,6 +49,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) 
{
     val refundManager = RefundManager(configManager, queue)
 
     override fun onCleared() {
+        httpClient.close()
         queue.cancelAll { !it.isCanceled }
     }
 
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
index 3f45e32..c0b01a2 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
@@ -22,15 +22,14 @@ import android.util.Base64.NO_WRAP
 import android.util.Base64.encodeToString
 import android.util.Log
 import androidx.annotation.UiThread
+import androidx.annotation.WorkerThread
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
-import com.android.volley.Request.Method.GET
-import com.android.volley.RequestQueue
-import com.android.volley.Response.Listener
-import com.android.volley.VolleyError
-import com.android.volley.toolbox.JsonObjectRequest
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.readValue
+import io.ktor.client.HttpClient
+import io.ktor.client.features.ClientRequestException
+import io.ktor.client.request.get
+import io.ktor.client.request.header
+import io.ktor.http.HttpHeaders.Authorization
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -38,9 +37,8 @@ import net.taler.common.Version
 import net.taler.common.getIncompatibleStringOrNull
 import net.taler.merchantlib.ConfigResponse
 import net.taler.merchantlib.MerchantApi
-import net.taler.merchantpos.LogErrorListener
+import net.taler.merchantlib.MerchantConfig
 import net.taler.merchantpos.R
-import org.json.JSONObject
 
 private const val SETTINGS_NAME = "taler-merchant-terminal"
 
@@ -60,15 +58,14 @@ interface ConfigurationReceiver {
     /**
      * Returns null if the configuration was valid, or a error string for user 
display otherwise.
      */
-    suspend fun onConfigurationReceived(json: JSONObject, currency: String): 
String?
+    suspend fun onConfigurationReceived(posConfig: PosConfig, currency: 
String): String?
 }
 
 class ConfigManager(
     private val context: Context,
     private val scope: CoroutineScope,
-    private val api: MerchantApi,
-    private val mapper: ObjectMapper,
-    private val queue: RequestQueue
+    private val httpClient: HttpClient,
+    private val api: MerchantApi
 ) {
 
     private val prefs = context.getSharedPreferences(SETTINGS_NAME, 
MODE_PRIVATE)
@@ -79,8 +76,12 @@ class ConfigManager(
         username = prefs.getString(SETTINGS_USERNAME, CONFIG_USERNAME_DEMO)!!,
         password = prefs.getString(SETTINGS_PASSWORD, CONFIG_PASSWORD_DEMO)!!
     )
+    @Volatile
     var merchantConfig: MerchantConfig? = null
         private set
+    @Volatile
+    var currency: String? = null
+        private set
 
     private val mConfigUpdateResult = MutableLiveData<ConfigUpdateResult>()
     val configUpdateResult: LiveData<ConfigUpdateResult> = mConfigUpdateResult
@@ -96,74 +97,76 @@ class ConfigManager(
             if (savePassword) config else config.copy(password = "")
         } else null
 
-        val stringRequest = object : JsonObjectRequest(GET, config.configUrl, 
null,
-            Listener { onConfigReceived(it, configToSave) },
-            LogErrorListener { onNetworkError(it) }
-        ) {
-            // send basic auth header
-            override fun getHeaders(): MutableMap<String, String> {
-                val credentials = "${config.username}:${config.password}"
-                val auth = ("Basic ${encodeToString(credentials.toByteArray(), 
NO_WRAP)}")
-                return mutableMapOf("Authorization" to auth)
-            }
-        }
-        queue.add(stringRequest)
-    }
-
-    @UiThread
-    private fun onConfigReceived(json: JSONObject, config: Config?) {
-        val merchantConfig: MerchantConfig = try {
-            mapper.readValue(json.getString("config"))
-        } catch (e: Exception) {
-            Log.e(TAG, "Error parsing merchant config", e)
-            val msg = context.getString(R.string.config_error_malformed)
-            mConfigUpdateResult.value = ConfigUpdateResult.Error(msg)
-            return
-        }
-
         scope.launch(Dispatchers.IO) {
-            val configResponse = api.getConfig(merchantConfig.baseUrl)
-            onMerchantConfigReceived(config, json, merchantConfig, 
configResponse)
+            try {
+                // get PoS configuration
+                val posConfig: PosConfig = httpClient.get(config.configUrl) {
+                    val credentials = "${config.username}:${config.password}"
+                    val auth = ("Basic 
${encodeToString(credentials.toByteArray(), NO_WRAP)}")
+                    header(Authorization, auth)
+                }
+                val merchantConfig = posConfig.merchantConfig
+                // get config from merchant backend API
+                
api.getConfig(merchantConfig.baseUrl).handleSuspend(::onNetworkError) {
+                    onMerchantConfigReceived(configToSave, posConfig, 
merchantConfig, it)
+                }
+            } catch (e: Exception) {
+                Log.e(TAG, "Error retrieving merchant config", e)
+                val msg = if (e is ClientRequestException) {
+                    context.getString(
+                        if (e.response.status.value == 401) 
R.string.config_auth_error
+                        else R.string.config_error_network
+                    )
+                } else {
+                    context.getString(R.string.config_error_malformed)
+                }
+                onNetworkError(msg)
+            }
         }
     }
 
-    private fun onMerchantConfigReceived(
+    @WorkerThread
+    private suspend fun onMerchantConfigReceived(
         newConfig: Config?,
-        configJson: JSONObject,
+        posConfig: PosConfig,
         merchantConfig: MerchantConfig,
         configResponse: ConfigResponse
-    ) = scope.launch(Dispatchers.Default) {
-        val versionIncompatible = VERSION.getIncompatibleStringOrNull(context, 
configResponse.version)
+    ) {
+        val versionIncompatible =
+            VERSION.getIncompatibleStringOrNull(context, 
configResponse.version)
         if (versionIncompatible != null) {
             
mConfigUpdateResult.postValue(ConfigUpdateResult.Error(versionIncompatible))
-            return@launch
+            return
         }
         for (receiver in configurationReceivers) {
             val result = try {
-                receiver.onConfigurationReceived(configJson, 
configResponse.currency)
+                receiver.onConfigurationReceived(posConfig, 
configResponse.currency)
             } catch (e: Exception) {
                 Log.e(TAG, "Error handling configuration by 
${receiver::class.java.simpleName}", e)
                 context.getString(R.string.config_error_unknown)
             }
             if (result != null) {  // error
                 mConfigUpdateResult.postValue(ConfigUpdateResult.Error(result))
-                return@launch
+                return
             }
         }
         newConfig?.let {
             config = it
             saveConfig(it)
         }
-        this@ConfigManager.merchantConfig = merchantConfig.copy(currency = 
configResponse.currency)
+        this.merchantConfig = merchantConfig
+        this.currency = configResponse.currency
         
mConfigUpdateResult.postValue(ConfigUpdateResult.Success(configResponse.currency))
     }
 
+    @UiThread
     fun forgetPassword() {
         config = config.copy(password = "")
         saveConfig(config)
         merchantConfig = null
     }
 
+    @UiThread
     private fun saveConfig(config: Config) {
         prefs.edit()
             .putString(SETTINGS_CONFIG_URL, config.configUrl)
@@ -172,12 +175,7 @@ class ConfigManager(
             .apply()
     }
 
-    @UiThread
-    private fun onNetworkError(it: VolleyError?) {
-        val msg = context.getString(
-            if (it?.networkResponse?.statusCode == 401) 
R.string.config_auth_error
-            else R.string.config_error_network
-        )
+    private fun onNetworkError(msg: String) = scope.launch(Dispatchers.Main) {
         mConfigUpdateResult.value = ConfigUpdateResult.Error(msg)
     }
 
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 9cfae94..5d41196 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,9 +16,11 @@
 
 package net.taler.merchantpos.config
 
+import android.net.Uri
 import android.util.ArrayMap
 import com.android.volley.Response
 import com.android.volley.toolbox.JsonObjectRequest
+import net.taler.merchantlib.MerchantConfig
 import net.taler.merchantpos.LogErrorListener
 import org.json.JSONObject
 
@@ -33,7 +35,7 @@ class MerchantRequest(
 ) :
     JsonObjectRequest(
         method,
-        merchantConfig.urlFor(endpoint, params),
+        merchantConfig.legacyUrl(endpoint, params),
         jsonRequest,
         listener,
         errorListener
@@ -44,4 +46,14 @@ class MerchantRequest(
         headerMap["Authorization"] = "ApiKey " + merchantConfig.apiKey
         return headerMap
     }
+
+}
+
+private fun MerchantConfig.legacyUrl(endpoint: String, params: Map<String, 
String>?): String {
+    val uriBuilder = Uri.parse(baseUrl).buildUpon()
+    uriBuilder.appendPath(endpoint)
+    params?.forEach {
+        uriBuilder.appendQueryParameter(it.key, it.value)
+    }
+    return uriBuilder.toString()
 }
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
 b/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt
similarity index 65%
rename from 
merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
rename to 
merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt
index 0c7e3b7..2d8c040 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt
@@ -16,9 +16,8 @@
 
 package net.taler.merchantpos.config
 
-import android.net.Uri
-import com.fasterxml.jackson.annotation.JsonIgnore
-import com.fasterxml.jackson.annotation.JsonProperty
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
 import net.taler.common.Amount
 import net.taler.common.ContractProduct
 import net.taler.common.Product
@@ -34,51 +33,40 @@ data class Config(
     fun hasPassword() = !password.isBlank()
 }
 
-data class MerchantConfig(
-    @JsonProperty("base_url")
-    val baseUrl: String,
-    val instance: String,
-    @JsonProperty("api_key")
-    val apiKey: String,
-    val currency: String?
-) {
-    fun urlFor(endpoint: String, params: Map<String, String>?): String {
-        val uriBuilder = Uri.parse(baseUrl).buildUpon()
-        uriBuilder.appendPath(endpoint)
-        params?.forEach {
-            uriBuilder.appendQueryParameter(it.key, it.value)
-        }
-        return uriBuilder.toString()
-    }
-    fun convert() = net.taler.merchantlib.MerchantConfig(
-        baseUrl, instance, apiKey
-    )
-}
+@Serializable
+data class PosConfig(
+    @SerialName("config")
+    val merchantConfig: net.taler.merchantlib.MerchantConfig,
+    val categories: List<Category>,
+    val products: List<ConfigProduct>
+)
 
+@Serializable
 data class Category(
     val id: Int,
     val name: String,
-    @JsonProperty("name_i18n")
-    val nameI18n: Map<String, String>?
+    @SerialName("name_i18n")
+    val nameI18n: Map<String, String>? = null
 ) {
     var selected: Boolean = false
     val localizedName: String get() = TalerUtils.getLocalizedString(nameI18n, 
name)
 }
 
+@Serializable
 data class ConfigProduct(
-    @JsonIgnore
     val id: String = UUID.randomUUID().toString(),
-    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 categories: List<Int>,
-    @JsonIgnore
     val quantity: Int = 0
 ) : Product() {
-    @get:JsonIgnore
     val totalPrice by lazy { price * quantity }
 
     fun toContractProduct() = ContractProduct(
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
index 6b95e16..24c7a0c 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
@@ -65,7 +65,7 @@ class HistoryManager(
     internal fun fetchHistory() {
         mIsLoading.value = true
         val merchantConfig = configManager.merchantConfig!!
-        val params = mapOf("instance" to merchantConfig.instance)
+        val params = mapOf("instance" to merchantConfig.instance!!)
         val req = MerchantRequest(GET, merchantConfig, "history", params, null,
             Listener { onHistoryResponse(it) },
             LogErrorListener { onHistoryError() })
diff --git 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
index ad6cd87..7291a23 100644
--- 
a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
+++ 
b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt
@@ -65,7 +65,7 @@ class OrderFragment : Fragment() {
         super.onStart()
         if (!viewModel.configManager.config.isValid()) {
             navigate(actionOrderToMerchantSettings())
-        } else if (viewModel.configManager.merchantConfig?.currency == null) {
+        } else if (viewModel.configManager.currency == null) {
             navigate(actionGlobalConfigFetcher())
         }
     }
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 46ea238..56cdc8a 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
@@ -22,19 +22,14 @@ import androidx.annotation.UiThread
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Transformations.map
-import com.fasterxml.jackson.core.type.TypeReference
-import com.fasterxml.jackson.databind.ObjectMapper
 import net.taler.merchantpos.R
 import net.taler.merchantpos.config.Category
 import net.taler.merchantpos.config.ConfigProduct
 import net.taler.merchantpos.config.ConfigurationReceiver
+import net.taler.merchantpos.config.PosConfig
 import net.taler.merchantpos.order.RestartState.ENABLED
-import org.json.JSONObject
 
-class OrderManager(
-    private val context: Context,
-    private val mapper: ObjectMapper
-) : ConfigurationReceiver {
+class OrderManager(private val context: Context) : ConfigurationReceiver {
 
     companion object {
         val TAG = OrderManager::class.java.simpleName
@@ -55,26 +50,18 @@ class OrderManager(
     private val mCategories = MutableLiveData<List<Category>>()
     internal val categories: LiveData<List<Category>> = mCategories
 
-    override suspend fun onConfigurationReceived(json: JSONObject, currency: 
String): String? {
+    override suspend fun onConfigurationReceived(posConfig: PosConfig, 
currency: String): String? {
         // parse categories
-        val categoriesStr = json.getJSONArray("categories").toString()
-        val categoriesType = object : TypeReference<List<Category>>() {}
-        val categories: List<Category> = mapper.readValue(categoriesStr, 
categoriesType)
-        if (categories.isEmpty()) {
+        if (posConfig.categories.isEmpty()) {
             Log.e(TAG, "No valid category found.")
             return context.getString(R.string.config_error_category)
         }
         // pre-select the first category
-        categories[0].selected = true
-
-        // parse products (live data gets updated in setCurrentCategory())
-        val productsStr = json.getJSONArray("products").toString()
-        val productsType = object : TypeReference<List<ConfigProduct>>() {}
-        val products: List<ConfigProduct> = mapper.readValue(productsStr, 
productsType)
+        posConfig.categories[0].selected = true
 
         // group products by categories
         productsByCategory.clear()
-        products.forEach { product ->
+        posConfig.products.forEach { product ->
             val productCurrency = product.price.currency
             if (productCurrency != currency) {
                 Log.e(TAG, "Product $product has currency $productCurrency, 
$currency expected")
@@ -83,7 +70,7 @@ class OrderManager(
                 )
             }
             product.categories.forEach { categoryId ->
-                val category = categories.find { it.id == categoryId }
+                val category = posConfig.categories.find { it.id == categoryId 
}
                 if (category == null) {
                     Log.e(TAG, "Product $product has unknown category 
$categoryId")
                     return context.getString(
@@ -99,8 +86,8 @@ class OrderManager(
         }
         return if (productsByCategory.size > 0) {
             this.currency = currency
-            mCategories.postValue(categories)
-            mProducts.postValue(productsByCategory[categories[0]])
+            mCategories.postValue(posConfig.categories)
+            mProducts.postValue(productsByCategory[posConfig.categories[0]])
             // Initialize first empty order, note this won't work when 
updating config mid-flight
             if (orders.isEmpty()) {
                 val id = orderCounter++
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 e238284..fc4f642 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
@@ -18,6 +18,7 @@ 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
@@ -27,6 +28,7 @@ import kotlinx.coroutines.launch
 import net.taler.merchantlib.CheckPaymentResponse
 import net.taler.merchantlib.MerchantApi
 import net.taler.merchantlib.PostOrderResponse
+import net.taler.merchantpos.MainActivity.Companion.TAG
 import net.taler.merchantpos.R
 import net.taler.merchantpos.config.ConfigManager
 import net.taler.merchantpos.order.Order
@@ -54,17 +56,16 @@ class PaymentManager(
         }
 
         override fun onFinish() {
-            val str = context.getString(R.string.error_timeout)
-            payment.value?.copy(error = str)?.let { mPayment.value = it }
+            cancelPayment(context.getString(R.string.error_timeout))
         }
     }
 
     @UiThread
     fun createPayment(order: Order) {
         val merchantConfig = configManager.merchantConfig!!
-        mPayment.value = Payment(order, order.summary, 
merchantConfig.currency!!)
+        mPayment.value = Payment(order, order.summary, 
configManager.currency!!)
         scope.launch(Dispatchers.IO) {
-            val response = api.postOrder(merchantConfig.convert(), 
order.toContractTerms())
+            val response = api.postOrder(merchantConfig, 
order.toContractTerms())
             response.handle(::onNetworkError, ::onOrderCreated)
         }
     }
@@ -77,7 +78,7 @@ class PaymentManager(
     private fun checkPayment(orderId: String) {
         val merchantConfig = configManager.merchantConfig!!
         scope.launch(Dispatchers.IO) {
-            val response = api.checkOrder(merchantConfig.convert(), orderId)
+            val response = api.checkOrder(merchantConfig, orderId)
             response.handle(::onNetworkError, ::onPaymentChecked)
         }
     }
@@ -97,7 +98,19 @@ class PaymentManager(
         cancelPayment(error)
     }
 
+    @UiThread
     fun cancelPayment(error: String) {
+        // delete unpaid order
+        val merchantConfig = configManager.merchantConfig!!
+        mPayment.value?.let { payment ->
+            if (!payment.paid) payment.orderId?.let { orderId ->
+                Log.e(TAG, "Deleting cancelled and unpaid order $orderId")
+                scope.launch(Dispatchers.IO) {
+                    api.deleteOrder(merchantConfig, orderId)
+                }
+            }
+        }
+
         mPayment.value = mPayment.value!!.copy(error = error)
         checkTimer.cancel()
     }
diff --git 
a/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt
 
b/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt
index d06428d..bb8dcb7 100644
--- 
a/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt
+++ 
b/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt
@@ -19,12 +19,13 @@ package net.taler.merchantpos.order
 import android.app.Application
 import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import 
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.KotlinModule
 import kotlinx.coroutines.runBlocking
+import net.taler.common.Amount
+import net.taler.merchantlib.MerchantConfig
 import net.taler.merchantpos.R
-import org.json.JSONObject
+import net.taler.merchantpos.config.Category
+import net.taler.merchantpos.config.ConfigProduct
+import net.taler.merchantpos.config.PosConfig
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNull
 import org.junit.Test
@@ -35,118 +36,71 @@ import org.robolectric.annotation.Config
 @RunWith(AndroidJUnit4::class)
 class OrderManagerTest {
 
-    private val mapper = ObjectMapper()
-        .registerModule(KotlinModule())
-        .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
-
     private val app: Application = getApplicationContext()
-    private val orderManager = OrderManager(app, mapper)
+    private val orderManager = OrderManager(app)
+    private val posConfig = PosConfig(
+        merchantConfig = MerchantConfig(
+            baseUrl = "http://example.org";,
+            apiKey = "sandbox"
+        ),
+        categories = listOf(
+            Category(1, "one"),
+            Category(2, "two")
+        ),
+        products = listOf(
+            ConfigProduct(
+                description = "foo",
+                price = Amount("KUDOS", 1, 0),
+                categories = listOf(1)
+            ),
+            ConfigProduct(
+                description = "bar",
+                price = Amount("KUDOS", 1, 50000),
+                categories = listOf(2)
+            )
+        )
+    )
 
     @Test
     fun `config test missing categories`() = runBlocking {
-        val json = JSONObject(
-            """
-            { "categories": [] }
-        """.trimIndent()
-        )
-        val result = orderManager.onConfigurationReceived(json, "KUDOS")
+        val config = posConfig.copy(categories = emptyList())
+        val result = orderManager.onConfigurationReceived(config, "KUDOS")
         assertEquals(app.getString(R.string.config_error_category), result)
     }
 
     @Test
     fun `config test currency mismatch`() = runBlocking {
-        val json = JSONObject(
-            """{
-            "categories": [
-                {
-                    "id": 1,
-                    "name": "Snacks"
-                }
-            ],
-            "products": [
-                {
-                    "product_id": "631361561",
-                    "description": "Chips",
-                    "price": "WRONGCUR:1.00",
-                    "categories": [ 1 ],
-                    "delivery_location": "cafeteria"
-                }
-            ]
-        }""".trimIndent()
-        )
-        val result = orderManager.onConfigurationReceived(json, "KUDOS")
+        val products = listOf(posConfig.products[0].copy(price = 
Amount("WRONGCUR", 1, 0)))
+        val config = posConfig.copy(products = products)
+        val result = orderManager.onConfigurationReceived(config, "KUDOS")
         val expectedStr = app.getString(
-            R.string.config_error_currency, "Chips", "WRONGCUR", "KUDOS"
+            R.string.config_error_currency, "foo", "WRONGCUR", "KUDOS"
         )
         assertEquals(expectedStr, result)
     }
 
     @Test
     fun `config test unknown category ID`() = runBlocking {
-        val json = JSONObject(
-            """{
-            "categories": [
-                {
-                    "id": 1,
-                    "name": "Snacks"
-                }
-            ],
-            "products": [
-                {
-                    "product_id": "631361561",
-                    "description": "Chips",
-                    "price": "KUDOS:1.00",
-                    "categories": [ 2 ]
-                }
-            ]
-        }""".trimIndent()
-        )
-        val result = orderManager.onConfigurationReceived(json, "KUDOS")
+        val products = listOf(posConfig.products[0].copy(categories = 
listOf(42)))
+        val config = posConfig.copy(products = products)
+        val result = orderManager.onConfigurationReceived(config, "KUDOS")
         val expectedStr = app.getString(
-            R.string.config_error_product_category_id, "Chips", 2
+            R.string.config_error_product_category_id, "foo", 42
         )
         assertEquals(expectedStr, result)
     }
 
     @Test
     fun `config test no products`() = runBlocking {
-        val json = JSONObject(
-            """{
-            "categories": [
-                {
-                    "id": 1,
-                    "name": "Snacks"
-                }
-            ],
-            "products": []
-        }""".trimIndent()
-        )
-        val result = orderManager.onConfigurationReceived(json, "KUDOS")
+        val config = posConfig.copy(products = emptyList())
+        val result = orderManager.onConfigurationReceived(config, "KUDOS")
         val expectedStr = app.getString(R.string.config_error_product_zero)
         assertEquals(expectedStr, result)
     }
 
     @Test
     fun `config test valid config gets accepted`() = runBlocking {
-        val json = JSONObject(
-            """{
-            "categories": [
-                {
-                    "id": 1,
-                    "name": "Snacks"
-                }
-            ],
-            "products": [
-                {
-                    "product_id": "631361561",
-                    "description": "Chips",
-                    "price": "KUDOS:1.00",
-                    "categories": [ 1 ]
-                }
-            ]
-        }""".trimIndent()
-        )
-        val result = orderManager.onConfigurationReceived(json, "KUDOS")
+        val result = orderManager.onConfigurationReceived(posConfig, "KUDOS")
         assertNull(result)
     }
 
diff --git a/taler-kotlin-common/build.gradle b/taler-kotlin-common/build.gradle
index df4b65f..dd083b7 100644
--- a/taler-kotlin-common/build.gradle
+++ b/taler-kotlin-common/build.gradle
@@ -62,7 +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"
+    api "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'

-- 
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]