aboutsummaryrefslogtreecommitdiff
path: root/merchant-lib/src
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2020-07-22 16:53:06 -0300
committerTorsten Grote <t@grobox.de>2020-07-22 16:53:06 -0300
commita8c811f6cdf4bf1b787ebaaa9fd220588fd1ffcf (patch)
tree604cd516a904f9c7474e37a87f92018f55ca4d1b /merchant-lib/src
parentb9d7d6edb7cc12e5238168c4446a28c0ae98c729 (diff)
downloadtaler-android-a8c811f6cdf4bf1b787ebaaa9fd220588fd1ffcf.tar.gz
taler-android-a8c811f6cdf4bf1b787ebaaa9fd220588fd1ffcf.tar.bz2
taler-android-a8c811f6cdf4bf1b787ebaaa9fd220588fd1ffcf.zip
[pos] migrate order posting and checking to v1 API and merchant-lib
Diffstat (limited to 'merchant-lib/src')
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt48
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt37
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt83
-rw-r--r--merchant-lib/src/main/java/net/taler/merchantlib/Response.kt63
-rw-r--r--merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt95
-rw-r--r--merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt33
-rw-r--r--merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt29
7 files changed, 383 insertions, 5 deletions
diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
index 3406f78..335e42d 100644
--- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
@@ -21,6 +21,16 @@ import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.request.get
+import io.ktor.client.request.header
+import io.ktor.client.request.post
+import io.ktor.http.ContentType.Application.Json
+import io.ktor.http.HttpHeaders.Authorization
+import io.ktor.http.contentType
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonConfiguration
+import net.taler.common.ContractTerms
+import net.taler.merchantlib.Response.Companion.failure
+import net.taler.merchantlib.Response.Companion.success
class MerchantApi(private val httpClient: HttpClient) {
@@ -30,10 +40,46 @@ class MerchantApi(private val httpClient: HttpClient) {
return httpClient.get("$baseUrl/config")
}
+ suspend fun postOrder(
+ merchantConfig: MerchantConfig,
+ contractTerms: ContractTerms
+ ): Response<PostOrderResponse> = response {
+ httpClient.post(merchantConfig.urlFor("private/orders")) {
+ header(Authorization, "ApiKey ${merchantConfig.apiKey}")
+ contentType(Json)
+ body = PostOrderRequest(contractTerms)
+ } as PostOrderResponse
+ }
+
+ suspend fun checkOrder(
+ merchantConfig: MerchantConfig,
+ orderId: String
+ ): Response<CheckPaymentResponse> = response {
+ httpClient.get(merchantConfig.urlFor("private/orders/$orderId")) {
+ header(Authorization, "ApiKey ${merchantConfig.apiKey}")
+ } as CheckPaymentResponse
+ }
+
+ private suspend fun <T> response(request: suspend () -> T): Response<T> {
+ return try {
+ success(request())
+ } catch (e: Throwable) {
+ failure(e)
+ }
+ }
}
private fun getDefaultHttpClient(): HttpClient = HttpClient(OkHttp) {
install(JsonFeature) {
- serializer = KotlinxSerializer()
+ serializer = getSerializer()
}
}
+
+fun getSerializer() = KotlinxSerializer(
+ Json(
+ JsonConfiguration(
+ encodeDefaults = false,
+ ignoreUnknownKeys = true
+ )
+ )
+)
diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt
new file mode 100644
index 0000000..71185b9
--- /dev/null
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt
@@ -0,0 +1,37 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantlib
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MerchantConfig(
+ @SerialName("base_url")
+ val baseUrl: String,
+ val instance: String,
+ @SerialName("api_key")
+ val apiKey: String
+) {
+ fun urlFor(endpoint: String, params: Map<String, String>? = null): String {
+ val sb = StringBuilder(baseUrl)
+ if (sb.last() != '/') sb.append('/')
+ sb.append("instances/$instance/")
+ sb.append(endpoint)
+ return sb.toString()
+ }
+}
diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt b/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt
new file mode 100644
index 0000000..a6e74d6
--- /dev/null
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt
@@ -0,0 +1,83 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantlib
+
+import kotlinx.serialization.Decoder
+import kotlinx.serialization.Encoder
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Serializer
+import kotlinx.serialization.json.JsonInput
+import kotlinx.serialization.json.JsonObject
+import net.taler.common.ContractTerms
+
+@Serializable
+data class PostOrderRequest(
+ @SerialName("order")
+ val contractTerms: ContractTerms
+
+)
+
+@Serializable
+data class PostOrderResponse(
+ @SerialName("order_id")
+ val orderId: String
+)
+
+@Serializable
+sealed class CheckPaymentResponse {
+ abstract val paid: Boolean
+
+ @Serializer(forClass = CheckPaymentResponse::class)
+ companion object : KSerializer<CheckPaymentResponse> {
+ override fun deserialize(decoder: Decoder): CheckPaymentResponse {
+ val input = decoder as JsonInput
+ val tree = input.decodeJson() as JsonObject
+ val paid = tree.getPrimitive("paid").boolean
+// return if (paid) decoder.json.fromJson(Paid.serializer(), tree)
+// else decoder.json.fromJson(Unpaid.serializer(), tree)
+ // manual parsing due to https://github.com/Kotlin/kotlinx.serialization/issues/576
+ return if (paid) Paid(
+ refunded = tree.getPrimitive("refunded").boolean
+ ) else Unpaid(
+ talerPayUri = tree.getPrimitive("taler_pay_uri").content
+ )
+ }
+
+ override fun serialize(encoder: Encoder, value: CheckPaymentResponse) = when (value) {
+ is Unpaid -> Unpaid.serializer().serialize(encoder, value)
+ is Paid -> Paid.serializer().serialize(encoder, value)
+ }
+ }
+
+ @Serializable
+ data class Unpaid(
+ override val paid: Boolean = false,
+ @SerialName("taler_pay_uri")
+ val talerPayUri: String,
+ @SerialName("already_paid_order_id")
+ val alreadyPaidOrderId: String? = null
+ ) : CheckPaymentResponse()
+
+ @Serializable
+ data class Paid(
+ override val paid: Boolean = true,
+ val refunded: Boolean
+ ) : CheckPaymentResponse()
+
+}
diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
new file mode 100644
index 0000000..23fa101
--- /dev/null
+++ b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt
@@ -0,0 +1,63 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantlib
+
+import io.ktor.client.call.receive
+import io.ktor.client.features.ClientRequestException
+import kotlinx.serialization.Serializable
+
+class Response<out T> private constructor(
+ private val value: Any?
+) {
+
+ companion object {
+ fun <T> success(value: T): Response<T> =
+ Response(value)
+
+ fun <T> failure(e: Throwable): Response<T> =
+ Response(Failure(e))
+ }
+
+ val isFailure: Boolean get() = value is Failure
+
+ suspend fun handle(onFailure: ((String) -> Any)? = null, onSuccess: ((T) -> Any)? = null) {
+ if (value is Failure) onFailure?.let { it(getFailureString(value)) }
+ else onSuccess?.let {
+ @Suppress("UNCHECKED_CAST")
+ it(value as T)
+ }
+ }
+
+ private suspend fun getFailureString(failure: Failure): String = when (failure.exception) {
+ is ClientRequestException -> getExceptionString(failure.exception)
+ else -> failure.exception.toString()
+ }
+
+ private suspend fun getExceptionString(e: ClientRequestException): String {
+ val error: Error = e.response.receive()
+ return "Error ${error.code}: ${error.hint}"
+ }
+
+ private class Failure(val exception: Throwable)
+
+ @Serializable
+ private class Error(
+ val code: Int?,
+ val hint: String?
+ )
+
+}
diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
index 6b2199b..de1ca33 100644
--- a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
+++ b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt
@@ -16,15 +16,25 @@
package net.taler.merchantlib
+import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.runBlocking
+import net.taler.common.Amount
+import net.taler.common.ContractProduct
+import net.taler.common.ContractTerms
import net.taler.merchantlib.MockHttpClient.giveJsonResponse
import net.taler.merchantlib.MockHttpClient.httpClient
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
import org.junit.Test
class MerchantApiTest {
private val api = MerchantApi(httpClient)
+ private val merchantConfig = MerchantConfig(
+ baseUrl = "http://example.net/",
+ instance = "testInstance",
+ apiKey = "apiKeyFooBar"
+ )
@Test
fun testGetConfig() = runBlocking {
@@ -40,4 +50,89 @@ class MerchantApiTest {
assertEquals(ConfigResponse("0:0:0", "INTKUDOS"), response)
}
+ @Test
+ fun testPostOrder() = runBlocking {
+ val product = ContractProduct(
+ productId = "foo",
+ description = "bar",
+ price = Amount("TEST", 1, 0),
+ quantity = 2
+ )
+ val contractTerms = ContractTerms(
+ summary = "test",
+ amount = Amount("TEST", 2, 1),
+ fulfillmentUrl = "http://example.org",
+ products = listOf(product)
+ )
+ val contractTermsJson = """
+ {
+ "order": {
+ "summary": "${contractTerms.summary}",
+ "amount": "${contractTerms.amount.toJSONString()}",
+ "fulfillment_url": "${contractTerms.fulfillmentUrl}",
+ "products": [
+ {
+ "product_id": "${product.productId}",
+ "description": "${product.description}",
+ "price": "${product.price.toJSONString()}",
+ "quantity": ${product.quantity}
+ }
+ ]
+ }
+ }
+ """.trimIndent()
+ httpClient.giveJsonResponse(
+ "http://example.net/instances/testInstance/private/orders",
+ contractTermsJson
+ ) {
+ """{"order_id": "test"}"""
+ }
+ api.postOrder(merchantConfig, contractTerms).assertSuccess {
+ assertEquals(PostOrderResponse("test"), it)
+ }
+
+ httpClient.giveJsonResponse(
+ "http://example.net/instances/testInstance/private/orders",
+ statusCode = HttpStatusCode.NotFound
+ ) {
+ """{
+ "code": 2000,
+ "hint": "merchant instance unknown"
+ }"""
+ }
+ api.postOrder(merchantConfig, contractTerms).assertFailure {
+ assertTrue(it.contains("2000"))
+ assertTrue(it.contains("merchant instance unknown"))
+ }
+ }
+
+ @Test
+ fun testCheckOrder() = runBlocking {
+ val orderId = "orderIdFoo"
+ val unpaidResponse = CheckPaymentResponse.Unpaid(false, "http://taler.net/foo")
+ httpClient.giveJsonResponse("http://example.net/instances/testInstance/private/orders/$orderId") {
+ """{
+ "paid": ${unpaidResponse.paid},
+ "taler_pay_uri": "${unpaidResponse.talerPayUri}"
+ }""".trimIndent()
+ }
+ api.checkOrder(merchantConfig, orderId).assertSuccess {
+ assertEquals(unpaidResponse, it)
+ }
+
+ httpClient.giveJsonResponse(
+ "http://example.net/instances/testInstance/private/orders/$orderId",
+ statusCode = HttpStatusCode.NotFound
+ ) {
+ """{
+ "code": 2909,
+ "hint": "Did not find contract terms for order in DB"
+ }"""
+ }
+ api.checkOrder(merchantConfig, orderId).assertFailure {
+ assertTrue(it.contains("2909"))
+ assertTrue(it.contains("Did not find contract terms for order in DB"))
+ }
+ }
+
}
diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt b/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt
index 076b77e..993be15 100644
--- a/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt
+++ b/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt
@@ -21,31 +21,50 @@ import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.MockEngineConfig
import io.ktor.client.engine.mock.respond
import io.ktor.client.features.json.JsonFeature
-import io.ktor.client.features.json.serializer.KotlinxSerializer
+import io.ktor.client.features.logging.LogLevel
+import io.ktor.client.features.logging.Logger
+import io.ktor.client.features.logging.Logging
+import io.ktor.client.features.logging.SIMPLE
import io.ktor.http.ContentType.Application.Json
+import io.ktor.http.HttpStatusCode
import io.ktor.http.Url
+import io.ktor.http.content.TextContent
import io.ktor.http.fullPath
import io.ktor.http.headersOf
import io.ktor.http.hostWithPort
+import org.junit.Assert.assertEquals
object MockHttpClient {
val httpClient = HttpClient(MockEngine) {
install(JsonFeature) {
- serializer = KotlinxSerializer()
+ serializer = getSerializer()
+ }
+ install(Logging) {
+ logger = Logger.SIMPLE
+ level = LogLevel.ALL
}
engine {
addHandler { error("No response handler set") }
}
}
- fun HttpClient.giveJsonResponse(url: String, jsonProducer: () -> String) {
+ fun HttpClient.giveJsonResponse(
+ url: String,
+ expectedBody: String? = null,
+ statusCode: HttpStatusCode = HttpStatusCode.OK,
+ jsonProducer: () -> String
+ ) {
val httpConfig = engineConfig as MockEngineConfig
httpConfig.requestHandlers.removeAt(0)
httpConfig.requestHandlers.add { request ->
if (request.url.fullUrl == url) {
val headers = headersOf("Content-Type" to listOf(Json.toString()))
- respond(jsonProducer(), headers = headers)
+ if (expectedBody != null) {
+ val content = request.body as TextContent
+ assertJsonEquals(expectedBody, content.text)
+ }
+ respond(jsonProducer(), headers = headers, status = statusCode)
} else {
error("Unexpected URL: ${request.url.fullUrl}")
}
@@ -55,4 +74,10 @@ object MockHttpClient {
private val Url.hostWithPortIfRequired: String get() = if (port == protocol.defaultPort) host else hostWithPort
private val Url.fullUrl: String get() = "${protocol.name}://$hostWithPortIfRequired$fullPath"
+ private fun assertJsonEquals(json1: String, json2: String) {
+ val parsed1 = kotlinx.serialization.json.Json.parseJson(json1)
+ val parsed2 = kotlinx.serialization.json.Json.parseJson(json2)
+ assertEquals(parsed1, parsed2)
+ }
+
}
diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt b/merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt
new file mode 100644
index 0000000..0d3d906
--- /dev/null
+++ b/merchant-lib/src/test/java/net/taler/merchantlib/TestResponse.kt
@@ -0,0 +1,29 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantlib
+
+import org.junit.Assert
+
+internal suspend fun <T> Response<T>.assertSuccess(assertions: (T) -> Any) {
+ Assert.assertFalse(isFailure)
+ handle(onSuccess = { assertions(it) })
+}
+
+internal suspend fun <T> Response<T>.assertFailure(assertions: (String) -> Any) {
+ Assert.assertTrue(isFailure)
+ handle(onFailure = { assertions(it) })
+}