aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.gradle1
-rw-r--r--merchant-lib/build.gradle9
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt9
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt5
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/Response.kt19
-rw-r--r--merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt5
-rw-r--r--merchant-terminal/build.gradle12
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt9
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt106
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt14
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt (renamed from merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt)52
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt2
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt2
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt31
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt8
-rw-r--r--merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt128
-rw-r--r--taler-kotlin-common/build.gradle2
17 files changed, 190 insertions, 224 deletions
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 06388f4..e995724 100644
--- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
@@ -37,8 +37,8 @@ import net.taler.merchantlib.Response.Companion.response
class MerchantApi(private val httpClient: HttpClient) {
- 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(
@@ -77,6 +77,11 @@ class MerchantApi(private val httpClient: HttpClient) {
}
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 eb1ef27..1b49d78 100644
--- a/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
@@ -50,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
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 ea16cb4..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
@@ -63,9 +63,9 @@ class PaymentManager(
@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)
}
}
@@ -78,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)
}
}
@@ -106,7 +106,7 @@ class PaymentManager(
if (!payment.paid) payment.orderId?.let { orderId ->
Log.e(TAG, "Deleting cancelled and unpaid order $orderId")
scope.launch(Dispatchers.IO) {
- api.deleteOrder(merchantConfig.convert(), orderId)
+ api.deleteOrder(merchantConfig, orderId)
}
}
}
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'