libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit b041de6e185e334e37572b33e31966028ae50bb6
parent 8a2da270d8ded0c99626c96bc60c06a33b902954
Author: ms <ms@taler.net>
Date:   Thu, 21 Oct 2021 16:39:21 +0200

extend Payto parser

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 1+
Mutil/src/main/kotlin/Payto.kt | 62+++++++++++++++++++++++++++++++++++++++++++-------------------
Mutil/src/test/kotlin/PaytoTest.kt | 27+++++++++++++++++++++------
3 files changed, 65 insertions(+), 25 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt @@ -181,6 +181,7 @@ private suspend fun talerTransfer(call: ApplicationCall) { val transferRequest = call.receive<TalerTransferRequest>() val amountObj = parseAmount(transferRequest.amount) // FIXME: Right now we only parse the credit_account, should we also validate that it matches our account info? + // FIXME, another parse happens below; is this really useful here? parsePayto(transferRequest.credit_account) val facadeId = expectNonNull(call.parameters["fcid"]) val opaqueRowId = transaction { diff --git a/util/src/main/kotlin/Payto.kt b/util/src/main/kotlin/Payto.kt @@ -1,21 +1,40 @@ package tech.libeufin.util import java.net.URI +import java.net.URLDecoder /** - * Helper data structures. + * Payto information. */ data class Payto( - // Can represent a the sender or a receiver. + // represent query param "sender-name" or "receiver-name". val name: String?, val iban: String, - val bic: String? + val bic: String?, + // Typically, a wire transfer's subject. + val message: String?, + val amount: String? ) class InvalidPaytoError(msg: String) : Exception(msg) +// Return the value of query string parameter 'name', or null if not found. +// 'params' is the a list of key-value elements of all the query parameters found in the URI. +private fun getQueryParamOrNull(name: String, params: List<Pair<String, String>>?): String? { + if (params == null) return null + return params.firstNotNullOfOrNull { pair -> + URLDecoder.decode(pair.second, Charsets.UTF_8).takeIf { pair.first == name } + } +} + fun parsePayto(paytoLine: String): Payto { + /** + * This check is due because URIs having a "payto:" prefix without + * slashes are correctly parsed by the Java 'URI' class. 'mailto' + * for example lacks the double-slash part. + */ if (!paytoLine.startsWith("payto://")) throw InvalidPaytoError("Invalid payto URI: $paytoLine") + val javaParsedUri = try { URI(paytoLine) } catch (e: java.lang.Exception) { @@ -25,22 +44,9 @@ fun parsePayto(paytoLine: String): Payto { throw InvalidPaytoError("'${paytoLine}' is not payto") } val wireMethod = javaParsedUri.host - if (wireMethod != "sepa") { - throw InvalidPaytoError("Only SEPA is supported, not '$wireMethod'") + if (wireMethod != "iban") { + throw InvalidPaytoError("Only 'iban' is supported, not '$wireMethod'") } - val accountOwner = if (javaParsedUri.query != null) { - val queryStringAsList = javaParsedUri.query.split("&") - // admit only ONE parameter: receiver-name. - if (queryStringAsList.size != 1) { - throw InvalidPaytoError("'${paytoLine}' has unsupported query string") - } - val splitParameter = queryStringAsList.first().split("=") - if (splitParameter.first() != "receiver-name" && splitParameter.first() != "sender-name") { - throw InvalidPaytoError("'${paytoLine}' has unsupported query string") - } - splitParameter.last() - } else null - val splitPath = javaParsedUri.path.split("/").filter { it.isNotEmpty() } if (splitPath.size > 2) { throw InvalidPaytoError("too many path segments in iban payto URI") @@ -48,5 +54,23 @@ fun parsePayto(paytoLine: String): Payto { val (iban, bic) = if (splitPath.size == 1) { Pair(splitPath[0], null) } else Pair(splitPath[1], splitPath[0]) - return Payto(iban = iban, bic = bic, name = accountOwner) + + val params: List<Pair<String, String>>? = if (javaParsedUri.query != null) { + val queryString: List<String> = javaParsedUri.query.split("&") + queryString.map { + val split = it.split("="); Pair(split[0], split[1]) + } + } else null + + val receiverName = getQueryParamOrNull("receiver-name", params) + val senderName = getQueryParamOrNull("sender-name", params) + if (receiverName != null && senderName != null) throw InvalidPaytoError("URI had both sender and receiver") + + return Payto( + iban = iban, + bic = bic, + amount = getQueryParamOrNull("amount", params), + message = getQueryParamOrNull("message", params), + name = listOf(receiverName, senderName).firstNotNullOfOrNull { it } + ) } \ No newline at end of file diff --git a/util/src/test/kotlin/PaytoTest.kt b/util/src/test/kotlin/PaytoTest.kt @@ -21,19 +21,21 @@ class PaytoTest { } try { parsePayto( - "payto://iban/BIC123/IBAN123?receiver-name=The%20Name&address=house" + "payto:iban/BIC123/IBAN123?receiver-name=The%20Name&address=house" ) } catch (e: InvalidPaytoError) { println(e) - println("more than one parameter isn't allowed") + println("'://' missing, invalid Payto") } try { - parsePayto( - "payto:iban/BIC123/IBAN123?receiver-name=The%20Name&address=house" - ) + parsePayto("payto://iban/BIC123/IBAN123?sender-name=Foo&receiver-name=Foo") + } catch (e: InvalidPaytoError) { + println(e) + } + try { + parsePayto("payto://wrong/BIC123/IBAN123?sender-name=Foo&receiver-name=Foo") } catch (e: InvalidPaytoError) { println(e) - println("'://' missing, invalid Payto") } } @@ -43,5 +45,18 @@ class PaytoTest { assert(withBic.iban == "IBAN123") assert(withBic.bic == "BIC123") assert(withBic.name == "The Name") + val complete = parsePayto("payto://iban/BIC123/IBAN123?sender-name=The%20Name&amount=EUR:1&message=donation") + assert(withBic.iban == "IBAN123") + assert(withBic.bic == "BIC123") + assert(withBic.name == "The Name") + assert(complete.message == "donation") + assert(complete.amount == "EUR:1") + val withoutOptionals = parsePayto( + "payto://iban/IBAN123" + ) + assert(withoutOptionals.bic == null) + assert(withoutOptionals.message == null) + assert(withoutOptionals.name == null) + assert(withoutOptionals.amount == null) } } \ No newline at end of file