libeufin

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

commit 08850ccd91ca448c7fa9d63e2e0088578c6b070d
parent 640cc009d787ee69aaef8e32b35dcbd74d932c73
Author: Antoine A <>
Date:   Sat, 14 Oct 2023 00:24:40 +0000

Fix add_incoming and start checking exchange URL and payto URI

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt | 25++++++++++---------------
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 40++++++++++++++++++++--------------------
Mbank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt | 6++----
Mbank/src/main/kotlin/tech/libeufin/bank/Metadata.kt | 16+++++++---------
Mbank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 24++++++++++++------------
Mbank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 2+-
Mbank/src/test/kotlin/DatabaseTest.kt | 20++++++++++----------
Mbank/src/test/kotlin/LibeuFinApiTest.kt | 10+++++-----
Mbank/src/test/kotlin/TalerApiTest.kt | 65+++++++++++++++++++++++++++++++++++------------------------------
Mdatabase-versioning/procedures.sql | 2+-
Mutil/src/main/kotlin/IbanPayto.kt | 2+-
12 files changed, 195 insertions(+), 110 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt @@ -176,11 +176,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { if (this == null) return@run null db.bankAccountGetFromOwnerId(this.expectRowId()) } - val internalPayto: String = if (req.internal_payto_uri != null) { - stripIbanPayto(req.internal_payto_uri) ?: throw badRequest("internal_payto_uri is invalid") - } else { - stripIbanPayto(genIbanPaytoUri()) ?: throw internalServerError("Bank generated an invalid internal payto URI") - } + val internalPayto = req.internal_payto_uri ?: IbanPayTo(genIbanPaytoUri()) if (maybeCustomerExists != null && maybeHasBankAccount != null) { logger.debug("Registering username was found: ${maybeCustomerExists.login}") // Checking _all_ the details are the same. val isIdentic = @@ -191,7 +187,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { CryptoUtil.checkpw(req.password, maybeCustomerExists.passwordHash) && maybeHasBankAccount.isPublic == req.is_public && maybeHasBankAccount.isTalerExchange == req.is_taler_exchange && - maybeHasBankAccount.internalPaytoUri == internalPayto + maybeHasBankAccount.internalPaytoUri.stripped == internalPayto.stripped if (isIdentic) { call.respond(HttpStatusCode.Created) return@post @@ -516,24 +512,23 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { val c: Customer = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized() val resourceName = call.expectUriComponent("USERNAME") // admin has no rights here. if ((c.login != resourceName) && (call.getAuthToken() == null)) throw forbidden() - val txData = call.receive<BankAccountTransactionCreate>() - val payto = parsePayto(txData.payto_uri) ?: throw badRequest("Invalid creditor Payto") - val subject = payto.message ?: throw badRequest("Wire transfer lacks subject") + val tx = call.receive<BankAccountTransactionCreate>() + val subject = tx.payto_uri.message ?: throw badRequest("Wire transfer lacks subject") val debtorBankAccount = db.bankAccountGetFromOwnerId(c.expectRowId()) ?: throw internalServerError("Debtor bank account not found") - if (txData.amount.currency != ctx.currency) throw badRequest( - "Wrong currency: ${txData.amount.currency}", + if (tx.amount.currency != ctx.currency) throw badRequest( + "Wrong currency: ${tx.amount.currency}", talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH ) if (!isBalanceEnough( balance = debtorBankAccount.expectBalance(), - due = txData.amount, + due = tx.amount, hasBalanceDebt = debtorBankAccount.hasDebt, maxDebt = debtorBankAccount.maxDebt )) throw conflict(hint = "Insufficient balance.", talerEc = TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT) - logger.info("creditor payto: ${txData.payto_uri}") - val creditorBankAccount = db.bankAccountGetFromInternalPayto("payto://iban/${payto.iban.lowercase()}") + logger.info("creditor payto: ${tx.payto_uri}") + val creditorBankAccount = db.bankAccountGetFromInternalPayto(tx.payto_uri) ?: throw notFound( "Creditor account not found", TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT @@ -542,7 +537,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { debtorAccountId = debtorBankAccount.expectRowId(), creditorAccountId = creditorBankAccount.expectRowId(), subject = subject, - amount = txData.amount, + amount = tx.amount, transactionDate = Instant.now() ) val res = db.bankTransactionCreate(dbInstructions) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -575,7 +575,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos (?, ?, ?, ?, (?, ?)::taler_amount) RETURNING bank_account_id; """) - stmt.setString(1, bankAccount.internalPaytoUri) + stmt.setString(1, bankAccount.internalPaytoUri.stripped) stmt.setLong(2, bankAccount.owningCustomerId) stmt.setBoolean(3, bankAccount.isPublic) stmt.setBoolean(4, bankAccount.isTalerExchange) @@ -637,7 +637,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos stmt.oneOrNull { BankAccount( - internalPaytoUri = it.getString("internal_payto_uri"), + internalPaytoUri = IbanPayTo(it.getString("internal_payto_uri")), balance = TalerAmount( it.getLong("balance_val"), it.getInt("balance_frac"), @@ -685,7 +685,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos } } - suspend fun bankAccountGetFromInternalPayto(internalPayto: String): BankAccount? = conn { conn -> + suspend fun bankAccountGetFromInternalPayto(internalPayto: IbanPayTo): BankAccount? = conn { conn -> val stmt = conn.prepareStatement(""" SELECT bank_account_id @@ -701,7 +701,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos FROM bank_accounts WHERE internal_payto_uri=? """) - stmt.setString(1, internalPayto) + stmt.setString(1, internalPayto.stripped) stmt.oneOrNull { BankAccount( @@ -797,7 +797,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos VALUES (?, ?, ?) """) stmt.setBytes(1, metadata.wtid.raw) - stmt.setString(2, metadata.exchangeBaseUrl) + stmt.setString(2, metadata.exchangeBaseUrl.url) stmt.setLong(3, rowId) stmt.executeUpdate() conn.execSQLUpdate("NOTIFY outgoing_tx, '${"${tx.debtorAccountId} $rowId"}'") @@ -1059,9 +1059,9 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos it.getInt("amount_frac"), getCurrency() ), - credit_account = it.getString("creditor_payto_uri"), + credit_account = IbanPayTo(it.getString("creditor_payto_uri")), wtid = ShortHashCode(it.getBytes("wtid")), - exchange_base_url = it.getString("exchange_base_url") + exchange_base_url = ExchangeUrl(it.getString("exchange_base_url")) ) } } @@ -1173,7 +1173,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos getCurrency() ), selectionDone = it.getBoolean("selection_done"), - selectedExchangePayto = it.getString("selected_exchange_payto"), + selectedExchangePayto = it.getString("selected_exchange_payto")?.run(::IbanPayTo), walletBankAccount = it.getLong("wallet_bank_account"), confirmationDone = it.getBoolean("confirmation_done"), aborted = it.getBoolean("aborted"), @@ -1208,7 +1208,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos */ suspend fun talerWithdrawalSetDetails( opUuid: UUID, - exchangePayto: String, + exchangePayto: IbanPayTo, reservePub: String ): Boolean = conn { conn -> val stmt = conn.prepareStatement(""" @@ -1217,7 +1217,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos WHERE withdrawal_uuid=? """ ) - stmt.setString(1, exchangePayto) + stmt.setString(1, exchangePayto.stripped) stmt.setString(2, reservePub) stmt.setObject(3, opUuid) stmt.executeUpdateViolation() @@ -1490,7 +1490,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos pmtInfId: String = "not used", endToEndId: String = "not used", ): TalerTransferCreationResult = conn { conn -> - val subject = OutgoingTxMetadata(req.wtid, req.exchange_base_url).toString() + val subject = OutgoingTxMetadata(req.wtid, req.exchange_base_url).encode() val stmt = conn.prepareStatement(""" SELECT out_debtor_not_found @@ -1515,8 +1515,8 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos stmt.setString(3, subject) stmt.setLong(4, req.amount.value) stmt.setInt(5, req.amount.frac) - stmt.setString(6, req.exchange_base_url) - stmt.setString(7, stripIbanPayto(req.credit_account) ?: throw badRequest("credit_account payto URI is invalid")) + stmt.setString(6, req.exchange_base_url.url) + stmt.setString(7, req.credit_account.stripped) stmt.setString(8, username) stmt.setLong(9, timestamp.toDbMicros() ?: throw faultyTimestampByBank()) stmt.setString(10, acctSvcrRef) @@ -1578,7 +1578,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos pmtInfId: String = "not used", endToEndId: String = "not used", ): TalerAddIncomingCreationResult = conn { conn -> - val subject = IncomingTxMetadata(req.reserve_pub).toString() + val subject = IncomingTxMetadata(req.reserve_pub).encode() val stmt = conn.prepareStatement(""" SELECT out_creditor_not_found @@ -1589,11 +1589,11 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos ,out_reserve_pub_reuse ,out_debitor_balance_insufficient ,out_tx_row_id - FROM - taler_add_incoming ( - ?, ?, - (?,?)::taler_amount, - ?, ?, ?, ?, ?, ? + FROM + taler_add_incoming ( + ?, ?, + (?,?)::taler_amount, + ?, ?, ?, ?, ?, ? ); """) @@ -1601,7 +1601,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos stmt.setString(2, subject) stmt.setLong(3, req.amount.value) stmt.setInt(4, req.amount.frac) - stmt.setString(5, stripIbanPayto(req.debit_account) ?: throw badRequest("debit_account payto URI is invalid")) + stmt.setString(5, req.debit_account.stripped) stmt.setString(6, username) stmt.setLong(7, timestamp.toDbMicros() ?: throw faultyTimestampByBank()) stmt.setString(8, acctSvcrRef) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt @@ -26,7 +26,6 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import net.taler.common.errorcodes.TalerErrorCode -import tech.libeufin.util.stripIbanPayto fun Routing.talerIntegrationHandlers(db: Database, ctx: BankApplicationContext) { get("/taler-integration/config") { @@ -56,7 +55,7 @@ fun Routing.talerIntegrationHandlers(db: Database, ctx: BankApplicationContext) selection_done = op.selectionDone, transfer_done = op.confirmationDone, amount = op.amount, - sender_wire = relatedBankAccount.internalPaytoUri, + sender_wire = relatedBankAccount.internalPaytoUri.stripped, suggested_exchange = suggestedExchange, confirm_transfer_url = confirmUrl ) @@ -77,9 +76,8 @@ fun Routing.talerIntegrationHandlers(db: Database, ctx: BankApplicationContext) if (db.bankTransactionCheckExists(req.reserve_pub) != null) throw conflict( "Reserve pub. already used", TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT ) - val exchangePayto = stripIbanPayto(req.selected_exchange) ?: throw badRequest("selected_exchange payto is invalid") db.talerWithdrawalSetDetails( - op.withdrawalUuid, exchangePayto, req.reserve_pub + op.withdrawalUuid, req.selected_exchange, req.reserve_pub ) } else { // Nothing to do in the database, i.e. we were successful true diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt b/bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt @@ -30,7 +30,7 @@ sealed interface TxMetadata { // OutgoingTxMetadata try { val (wtid, exchangeBaseUrl) = subject.split(" ", limit=2) ; - return OutgoingTxMetadata(ShortHashCode(wtid), exchangeBaseUrl) + return OutgoingTxMetadata(ShortHashCode(wtid), ExchangeUrl(exchangeBaseUrl)) } catch (e: Exception) { } // No well formed metadata @@ -40,15 +40,13 @@ sealed interface TxMetadata { fun encode(metadata: TxMetadata): String { return when (metadata) { is IncomingTxMetadata -> "${metadata.reservePub}" - is OutgoingTxMetadata -> "${metadata.wtid} ${metadata.exchangeBaseUrl}" + is OutgoingTxMetadata -> "${metadata.wtid} ${metadata.exchangeBaseUrl.url}" } } } + + fun encode(): String = TxMetadata.encode(this) } -data class IncomingTxMetadata(val reservePub: EddsaPublicKey): TxMetadata { - override fun toString(): String = TxMetadata.encode(this) -} -data class OutgoingTxMetadata(val wtid: ShortHashCode, val exchangeBaseUrl: String): TxMetadata { - override fun toString(): String = TxMetadata.encode(this) -} -\ No newline at end of file +data class IncomingTxMetadata(val reservePub: EddsaPublicKey): TxMetadata +data class OutgoingTxMetadata(val wtid: ShortHashCode, val exchangeBaseUrl: ExchangeUrl): TxMetadata +\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt @@ -29,6 +29,7 @@ import java.time.Instant import java.time.temporal.ChronoUnit import java.util.concurrent.TimeUnit import java.util.* +import java.net.* import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -352,4 +353,93 @@ data class RelativeTime( override val descriptor: SerialDescriptor = JsonElement.serializer().descriptor } -} -\ No newline at end of file +} + + +@Serializable(with = ExchangeUrl.Serializer::class) +class ExchangeUrl { + val url: String + + constructor(raw: String) { + url = URL(raw).toString() + } + + override fun toString(): String = url + + internal object Serializer : KSerializer<ExchangeUrl> { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("ExchangeUrl", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: ExchangeUrl) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): ExchangeUrl { + return ExchangeUrl(decoder.decodeString()) + } + } +} + + +@Serializable(with = IbanPayTo.Serializer::class) +class IbanPayTo { + val parsed: URI + val stripped: String + // represent query param "sender-name" or "receiver-name". + val receiverName: String? + val iban: String + val bic: String? + // Typically, a wire transfer's subject. + val message: String? + val amount: String? + + constructor(raw: String) { + parsed = URI(raw) + require(parsed.scheme == "payto") { "expect a payto URI" } + require(parsed.host == "iban") { "expect a IBAN payto URI" } + val splitPath = parsed.path.split("/").filter { it.isNotEmpty() } + require(splitPath.size < 3 && splitPath.isNotEmpty()) { "too many path segments" } + val parts = if (splitPath.size == 1) { + Pair(splitPath[0], null) + } else Pair(splitPath[1], splitPath[0]) + // TODO normalize IBAN & BIC ? + iban = parts.first.uppercase() + bic = parts.second?.uppercase() + stripped = "payto://iban/$iban" + + val params: List<Pair<String, String>>? = if (parsed.query != null) { + val queryString: List<String> = parsed.query.split("&") + queryString.map { + val split = it.split("="); + require(split.size == 2) { "parameter '$it' was malformed" } + Pair(split[0], split[1]) + } + } else null + + // Return the value of query string parameter 'name', or null if not found. + // 'params' is the list of key-value elements of all the query parameters found in the URI. + fun getQueryParamOrNull(name: String): String? { + if (params == null) return null + return params.firstNotNullOfOrNull { pair -> + URLDecoder.decode(pair.second, Charsets.UTF_8).takeIf { pair.first == name } + } + } + + amount = getQueryParamOrNull("amount") + message = getQueryParamOrNull("message") + receiverName = getQueryParamOrNull("receiver-name") + } + + internal object Serializer : KSerializer<IbanPayTo> { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("IbanPayTo", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: IbanPayTo) { + encoder.encodeString(value.parsed.toString()) + } + + override fun deserialize(decoder: Decoder): IbanPayTo { + return IbanPayTo(decoder.decodeString()) + } + } +} diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -91,7 +91,7 @@ data class RegisterAccountRequest( // External bank account where to send cashout amounts. val cashout_payto_uri: String? = null, // Bank account internal to Libeufin-Bank. - val internal_payto_uri: String? = null + val internal_payto_uri: IbanPayTo? = null ) @@ -151,7 +151,7 @@ data class Customer( * from/to the database. */ data class BankAccount( - val internalPaytoUri: String, + val internalPaytoUri: IbanPayTo, // Database row ID of the customer that owns this bank account. val owningCustomerId: Long, val bankAccountId: Long? = null, // null at INSERT. @@ -270,7 +270,7 @@ data class TalerWithdrawalOperation( val aborted: Boolean = false, val confirmationDone: Boolean = false, val reservePub: String?, - val selectedExchangePayto: String?, + val selectedExchangePayto: IbanPayTo?, val walletBankAccount: Long ) @@ -345,7 +345,7 @@ data class ListBankAccountsResponse( data class AccountData( val name: String, val balance: Balance, - val payto_uri: String, + val payto_uri: IbanPayTo, val debit_threshold: TalerAmount, val contact_data: ChallengeContactData? = null, val cashout_payto_uri: String? = null, @@ -356,7 +356,7 @@ data class AccountData( */ @Serializable data class BankAccountTransactionCreate( - val payto_uri: String, + val payto_uri: IbanPayTo, val amount: TalerAmount ) @@ -400,7 +400,7 @@ data class BankAccountGetWithdrawalResponse( val confirmation_done: Boolean, val selection_done: Boolean, val selected_reserve_pub: String? = null, - val selected_exchange_account: String? = null + val selected_exchange_account: IbanPayTo? = null ) typealias ResourceName = String @@ -492,7 +492,7 @@ data class BankWithdrawalOperationStatus( @Serializable data class BankWithdrawalOperationPostRequest( val reserve_pub: String, - val selected_exchange: String, + val selected_exchange: IbanPayTo, ) /** @@ -513,7 +513,7 @@ data class BankWithdrawalOperationPostResponse( data class AddIncomingRequest( val amount: TalerAmount, val reserve_pub: EddsaPublicKey, - val debit_account: String + val debit_account: IbanPayTo ) /** @@ -571,9 +571,9 @@ data class OutgoingTransaction( val row_id: Long, // DB row ID of the payment. val date: TalerProtocolTimestamp, val amount: TalerAmount, - val credit_account: String, // Payto of the receiver. + val credit_account: IbanPayTo, // Payto of the receiver. val wtid: ShortHashCode, - val exchange_base_url: String, + val exchange_base_url: ExchangeUrl, ) /** @@ -583,9 +583,9 @@ data class OutgoingTransaction( data class TransferRequest( val request_uid: HashCode, val amount: TalerAmount, - val exchange_base_url: String, + val exchange_base_url: ExchangeUrl, val wtid: ShortHashCode, - val credit_account: String + val credit_account: IbanPayTo ) /** diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -359,7 +359,7 @@ suspend fun maybeCreateAdminAccount(db: Database, ctx: BankApplicationContext): } val adminBankAccount = BankAccount( hasDebt = false, - internalPaytoUri = adminInternalPayto, + internalPaytoUri = IbanPayTo(adminInternalPayto), owningCustomerId = adminCustomerId, isPublic = false, isTalerExchange = false, diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt @@ -68,7 +68,7 @@ class DatabaseTest { cashoutCurrency = "KUDOS" ) private val bankAccountFoo = BankAccount( - internalPaytoUri = "payto://iban/FOO-IBAN-XYZ".lowercase(), + internalPaytoUri = IbanPayTo("payto://iban/FOO-IBAN-XYZ"), lastNexusFetchRowId = 1L, owningCustomerId = 1L, hasDebt = false, @@ -76,7 +76,7 @@ class DatabaseTest { isTalerExchange = true ) private val bankAccountBar = BankAccount( - internalPaytoUri = "payto://iban/BAR-IBAN-ABC".lowercase(), + internalPaytoUri = IbanPayTo("payto://iban/BAR-IBAN-ABC"), lastNexusFetchRowId = 1L, owningCustomerId = 2L, hasDebt = false, @@ -114,8 +114,8 @@ class DatabaseTest { fun talerTransferTest() = setupDb { db -> val exchangeReq = TransferRequest( amount = TalerAmount(9, 0, "KUDOS"), - credit_account = "payto://iban/BAR-IBAN-ABC".lowercase(), // foo pays bar - exchange_base_url = "example.com/exchange", + credit_account = IbanPayTo("payto://iban/BAR-IBAN-ABC"), + exchange_base_url = ExchangeUrl("https://example.com/exchange"), request_uid = randHashCode(), wtid = randShortHashCode() ) @@ -319,7 +319,7 @@ class DatabaseTest { // Setting the details. assert(db.talerWithdrawalSetDetails( opUuid = uuid, - exchangePayto = "payto://iban/BAR-IBAN-ABC".lowercase(), + exchangePayto = IbanPayTo("payto://iban/BAR-IBAN-ABC"), reservePub = "UNCHECKED-RESERVE-PUB" )) val opSelected = db.talerWithdrawalGet(uuid) @@ -443,7 +443,7 @@ class DatabaseTest { assert(db.bankAccountCreate( BankAccount( isPublic = true, - internalPaytoUri = "payto://iban/non-used", + internalPaytoUri = IbanPayTo("payto://iban/non-used"), lastNexusFetchRowId = 1L, owningCustomerId = this!!, hasDebt = false, @@ -500,10 +500,10 @@ class DatabaseTest { // Getting the updated account from the database and checking values. db.customerGetFromLogin("foo").apply { assertNotNull(this) - assert((this.login == "foo") && - (this.name == "Bar") && - (this.cashoutPayto) == "payto://cashout" && - (this.email) == "foo@example.com" && + assert(this.login == "foo" && + this.name == "Bar" && + this.cashoutPayto == "payto://cashout" && + this.email == "foo@example.com" && this.phone == "+99" ) db.bankAccountGetFromOwnerId(this.expectRowId()).apply { diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt b/bank/src/test/kotlin/LibeuFinApiTest.kt @@ -43,7 +43,7 @@ class LibeuFinApiTest { private fun genBankAccount(rowId: Long) = BankAccount( hasDebt = false, - internalPaytoUri = "payto://iban/ac${rowId}", + internalPaytoUri = IbanPayTo("payto://iban/ac${rowId}"), maxDebt = TalerAmount(100, 0, "KUDOS"), owningCustomerId = rowId ) @@ -248,7 +248,7 @@ class LibeuFinApiTest { assert(db.bankAccountCreate( BankAccount( hasDebt = false, - internalPaytoUri = "payto://iban/DE1234", + internalPaytoUri = IbanPayTo("payto://iban/DE1234"), maxDebt = TalerAmount(100, 0, "KUDOS"), owningCustomerId = 1 ) @@ -276,7 +276,7 @@ class LibeuFinApiTest { db.bankAccountCreate( BankAccount( isPublic = true, - internalPaytoUri = "payto://iban/non-used", + internalPaytoUri = IbanPayTo("payto://iban/non-used"), lastNexusFetchRowId = 1L, owningCustomerId = this!!, hasDebt = false, @@ -439,7 +439,7 @@ class LibeuFinApiTest { db.bankAccountCreate( BankAccount( hasDebt = false, - internalPaytoUri = "payto://iban/DE1234", + internalPaytoUri = IbanPayTo("payto://iban/DE1234"), maxDebt = TalerAmount(100, 0, "KUDOS"), owningCustomerId = customerRowId!! ) @@ -468,7 +468,7 @@ class LibeuFinApiTest { db.bankAccountCreate( BankAccount( hasDebt = false, - internalPaytoUri = "payto://iban/SANDBOXX/ADMIN-IBAN", + internalPaytoUri = IbanPayTo("payto://iban/SANDBOXX/ADMIN-IBAN"), maxDebt = TalerAmount(100, 0, "KUDOS"), owningCustomerId = adminRowId!! ) diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt @@ -28,7 +28,7 @@ class TalerApiTest { cashoutCurrency = "KUDOS" ) private val bankAccountFoo = BankAccount( - internalPaytoUri = "payto://iban/FOO-IBAN-XYZ".lowercase(), + internalPaytoUri = IbanPayTo("payto://iban/FOO-IBAN-XYZ"), lastNexusFetchRowId = 1L, owningCustomerId = 1L, hasDebt = false, @@ -45,7 +45,7 @@ class TalerApiTest { cashoutCurrency = "KUDOS" ) val bankAccountBar = BankAccount( - internalPaytoUri = stripIbanPayto("payto://iban/BAR-IBAN-ABC")!!, + internalPaytoUri = IbanPayTo("payto://iban/BAR-IBAN-ABC"), lastNexusFetchRowId = 1L, owningCustomerId = 2L, hasDebt = false, @@ -59,28 +59,29 @@ class TalerApiTest { req = TransferRequest( request_uid = randHashCode(), amount = TalerAmount(amount), - exchange_base_url = "http://exchange.example.com/", + exchange_base_url = ExchangeUrl("http://exchange.example.com/"), wtid = randShortHashCode(), - credit_account ="${stripIbanPayto(to.internalPaytoUri)}" + credit_account = to.internalPaytoUri ), username = from, timestamp = Instant.now() - ) + ).run { + assertEquals(TalerTransferResult.SUCCESS, txResult) + } } - suspend fun Database.genIncoming(from: Long, to: Long) { - bankTransactionCreate( - BankInternalTransaction( - creditorAccountId = from, - debtorAccountId = to, - subject = IncomingTxMetadata(randShortHashCode()).toString(), + suspend fun Database.genIncoming(to: String, from: BankAccount) { + talerAddIncomingCreate( + req = AddIncomingRequest( + reserve_pub = randShortHashCode(), amount = TalerAmount( 10, 0, "KUDOS"), - accountServicerReference = "acct-svcr-ref", - endToEndId = "end-to-end-id", - paymentInformationId = "pmtinfid", - transactionDate = Instant.now() - ) - ).assertSuccess() + debit_account = from.internalPaytoUri, + ), + username = to, + timestamp = Instant.now() + ).run { + assertEquals(TalerAddIncomingResult.SUCCESS, txResult) + } } fun commonSetup(lambda: suspend (Database, BankApplicationContext) -> Unit) { @@ -138,7 +139,7 @@ class TalerApiTest { "amount" to "KUDOS:55" "exchange_base_url" to "http://exchange.example.com/" "wtid" to randShortHashCode() - "credit_account" to stripIbanPayto(bankAccountFoo.internalPaytoUri) + "credit_account" to bankAccountFoo.internalPaytoUri }; authRoutine(client, "/accounts/foo/taler-wire-gateway/transfer", valid_req) @@ -289,17 +290,19 @@ class TalerApiTest { basicAuth("bar", "secret") }.assertStatus(HttpStatusCode.NoContent) - // Foo pays Bar (the exchange) three time + // Gen three transactions using clean add incoming logic repeat(3) { - db.genIncoming(2, 1) + db.genIncoming("bar", bankAccountFoo) } // Should not show up in the taler wire gateway API history db.bankTransactionCreate(genTx("bogus foobar")).assertSuccess() // Bar pays Foo once, but that should not appear in the result. db.bankTransactionCreate(genTx("payout", creditorId = 1, debtorId = 2)).assertSuccess() - // Foo pays Bar (the exchange) twice, we should see five valid transactions + // Gen two transactions using row bank transaction logic repeat(2) { - db.genIncoming(2, 1) + db.bankTransactionCreate( + genTx(IncomingTxMetadata(randShortHashCode()).encode(), 2, 1) + ).assertSuccess() } // Check ignore bogus subject @@ -356,14 +359,14 @@ class TalerApiTest { }, launch { delay(200) - db.genIncoming(2, 1) + db.genIncoming("bar", bankAccountFoo) } ) } // Testing ranges. repeat(300) { - db.genIncoming(2, 1) + db.genIncoming("bar", bankAccountFoo) } // forward range: @@ -428,7 +431,7 @@ class TalerApiTest { basicAuth("bar", "secret") }.assertStatus(HttpStatusCode.NoContent) - // Bar pays Foo three time + // Gen three transactions using clean transfer logic repeat(3) { db.genTransfer("bar", bankAccountFoo) } @@ -436,9 +439,11 @@ class TalerApiTest { db.bankTransactionCreate(genTx("bogus foobar", 1, 2)).assertSuccess() // Foo pays Bar once, but that should not appear in the result. db.bankTransactionCreate(genTx("payout")).assertSuccess() - // Bar pays Foo twice, we should see five valid transactions + // Gen two transactions using row bank transaction logic repeat(2) { - db.genTransfer("bar", bankAccountFoo) + db.bankTransactionCreate( + genTx(OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode(), 1, 2) + ).assertSuccess() } // Check ignore bogus subject @@ -613,7 +618,7 @@ class TalerApiTest { val r = client.post("/taler-integration/withdrawal-operation/${uuid}") { jsonBody(BankWithdrawalOperationPostRequest( reserve_pub = "RESERVE-FOO", - selected_exchange = "payto://iban/ABC123" + selected_exchange = IbanPayTo("payto://iban/ABC123") )) }.assertOk() println(r.bodyAsText()) @@ -653,7 +658,7 @@ class TalerApiTest { )) val op = db.talerWithdrawalGet(uuid) assert(op?.aborted == false) - assert(db.talerWithdrawalSetDetails(uuid, "exchange-payto", "reserve_pub")) + assert(db.talerWithdrawalSetDetails(uuid, IbanPayTo("payto://iban/exchange-payto"), "reserve_pub")) testApplication { application { corebankWebApp(db, ctx) @@ -700,7 +705,7 @@ class TalerApiTest { // Specifying Bar as the exchange, via its Payto URI. assert(db.talerWithdrawalSetDetails( opUuid = uuid, - exchangePayto = "payto://iban/BAR-IBAN-ABC".lowercase(), + exchangePayto = IbanPayTo("payto://iban/BAR-IBAN-ABC"), reservePub = "UNCHECKED-RESERVE-PUB" )) diff --git a/database-versioning/procedures.sql b/database-versioning/procedures.sql @@ -387,7 +387,7 @@ END IF; -- Perform bank transfer SELECT out_balance_insufficient, - out_debit_row_id, + out_credit_row_id, transfer.out_same_account INTO out_debitor_balance_insufficient, diff --git a/util/src/main/kotlin/IbanPayto.kt b/util/src/main/kotlin/IbanPayto.kt @@ -104,6 +104,6 @@ fun buildIbanPaytoUri( */ fun stripIbanPayto(paytoUri: String): String? { val parsedPayto = parsePayto(paytoUri) ?: return null - val canonIban = parsedPayto.iban.lowercase() + val canonIban = parsedPayto.iban.uppercase() return "payto://iban/${canonIban}" } \ No newline at end of file