libeufin

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

commit fcf52f42da84ecec516d2b614f3cecb0ac272686
parent ac0ad01037b742d39bf9007b2caba91dbf6bdc3d
Author: Antoine A <>
Date:   Thu, 28 May 2026 18:16:18 +0200

common: add credit account payto into Prepared Transfer API

Diffstat:
Mbuild.gradle | 19+++++++++++++++++++
Mdatabase-versioning/libeufin-bank-procedures.sql | 10+++++-----
Mdatabase-versioning/libeufin-nexus-0014.sql | 2+-
Mlibeufin-bank/src/main/kotlin/tech/libeufin/bank/api/PreparedTransferApi.kt | 17++++++-----------
Mlibeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TransferDAO.kt | 10+++++-----
Mlibeufin-bank/src/test/kotlin/PreparedTransferApiTest.kt | 68+++++++++++++++++++++++++++++++++++++-------------------------------
Mlibeufin-bank/src/test/kotlin/WireGatewayApiTest.kt | 3++-
Mlibeufin-bank/src/test/kotlin/bench.kt | 9+++++----
Mlibeufin-common/src/main/kotlin/TalerMessage.kt | 12+++++++-----
Mlibeufin-nexus/conf/test.conf | 1+
Mlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt | 1+
Mlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/PreparedTransferApi.kt | 21++++++++++++++++-----
Mlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/db/TransferDAO.kt | 2+-
Mlibeufin-nexus/src/test/kotlin/PreparedTransferApiTest.kt | 42++++++++++++++++++++++++++++++++++--------
Mlibeufin-nexus/src/test/kotlin/WireGatewayApiTest.kt | 1+
Mlibeufin-nexus/src/test/kotlin/bench.kt | 1+
16 files changed, 142 insertions(+), 77 deletions(-)

diff --git a/build.gradle b/build.gradle @@ -42,7 +42,26 @@ subprojects { // Or when editing ISO20022 test samples inputs.dir("$rootDir/libeufin-nexus/sample").withPathSensitivity(PathSensitivity.RELATIVE) inputs.dir("$rootDir/testbench/sample").withPathSensitivity(PathSensitivity.RELATIVE) + + def failedTests = [] + + afterTest { desc, result -> + if (result.resultType == TestResult.ResultType.FAILURE) { + failedTests << "${desc.className}.${desc.name}" + } + } + + afterSuite { desc, result -> + if (desc.parent == null && !failedTests.isEmpty()) { + logger.lifecycle("") + logger.lifecycle("==== FAILED TESTS ====") + failedTests.each { logger.lifecycle(it) } + logger.lifecycle("======================") + } + } + testLogging { + events "failed" exceptionFormat = 'full' } } diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -2285,7 +2285,7 @@ LANGUAGE sql AS $$ $$; CREATE FUNCTION register_prepared_transfers ( - IN in_exchange_username TEXT, + IN in_credit_account TEXT, IN in_type taler_incoming_type, IN in_account_pub BYTEA, IN in_authorization_pub BYTEA, @@ -2295,7 +2295,7 @@ CREATE FUNCTION register_prepared_transfers ( IN in_timestamp INT8, IN in_subject TEXT, -- Error status - OUT out_unknown_account BOOLEAN, + OUT out_unknown_creditor BOOLEAN, OUT out_not_exchange BOOLEAN, OUT out_reserve_pub_reuse BOOLEAN, -- Success status @@ -2313,9 +2313,9 @@ SELECT bank_account_id, NOT is_taler_exchange INTO exchange_account_id, out_not_exchange FROM bank_accounts JOIN customers ON customer_id=owning_customer_id - WHERE username = in_exchange_username; -out_unknown_account=NOT FOUND; -if out_unknown_account OR out_not_exchange THEN RETURN; END IF; + WHERE internal_payto = in_credit_account; +out_unknown_creditor=NOT FOUND; +if out_unknown_creditor OR out_not_exchange THEN RETURN; END IF; -- Check idempotency SELECT withdrawal_uuid, prepared_transfers.type = in_type diff --git a/database-versioning/libeufin-nexus-0014.sql b/database-versioning/libeufin-nexus-0014.sql @@ -38,7 +38,7 @@ CREATE TABLE prepared_transfers ( authorization_pub BYTEA UNIQUE NOT NULL CHECK (LENGTH(authorization_pub)=32), authorization_sig BYTEA NOT NULL CHECK (LENGTH(authorization_sig)=64), recurrent BOOLEAN NOT NULL, - reference_number TEXT UNIQUE NOT NULL CHECK(reference_number ~ '^\d{27}$'), + reference_number TEXT UNIQUE CHECK(reference_number ~ '^\d{27}$'), registered_at INT8 NOT NULL, incoming_transaction_id INT8 UNIQUE REFERENCES incoming_transactions(incoming_transaction_id) ON DELETE CASCADE ); diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/PreparedTransferApi.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/PreparedTransferApi.kt @@ -35,7 +35,7 @@ import java.time.Instant import java.time.Duration fun Routing.preparedTransferApi(db: Database, cfg: BankConfig) { - get("/accounts/{USERNAME}/taler-prepared-transfer/config", { + get("/taler-prepared-transfer/config", { operationId = "getPreparedTransferConfig" description = "Get the configuration of the prepared transfer API" tags = listOf("Prepared Transfer") @@ -53,12 +53,11 @@ fun Routing.preparedTransferApi(db: Database, cfg: BankConfig) { ) ) } - post("/accounts/{USERNAME}/taler-prepared-transfer/registration", { + post("/taler-prepared-transfer/registration", { operationId = "registerTransfer" description = "Register a prepared transfer" tags = listOf("Prepared Transfer") request { - pathParameter<String>("USERNAME") { description = "Account username" } body<SubjectRequest>() } response { @@ -66,7 +65,6 @@ fun Routing.preparedTransferApi(db: Database, cfg: BankConfig) { code(HttpStatusCode.Conflict) { description = "Reserve pub or subject derivation already used" } } }) { - val username = call.pathUsername val req = call.receive<SubjectRequest>(); cfg.checkRegionalCurrency(req.credit_amount) @@ -77,7 +75,7 @@ fun Routing.preparedTransferApi(db: Database, cfg: BankConfig) { ) when (val result = db.transfer.register( - username, + req.credit_account, req.type, req.account_pub, req.authorization_pub, @@ -86,12 +84,12 @@ fun Routing.preparedTransferApi(db: Database, cfg: BankConfig) { req.credit_amount, Instant.now() )) { - RegistrationResult.UnknownAccount -> throw unknownAccount(username) - RegistrationResult.NotExchange -> throw notExchange(username) + RegistrationResult.NotExchange -> throw notExchange(req.credit_account.canonical) RegistrationResult.ReservePubReuse -> throw conflict( "reserve_pub used already", TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT ) + RegistrationResult.UnknownCreditor -> throw unknownCreditorAccount(req.credit_account.canonical) is RegistrationResult.Success -> { val subjects = mutableListOf<TransferSubject>() if (result.uuid != null) @@ -106,13 +104,10 @@ fun Routing.preparedTransferApi(db: Database, cfg: BankConfig) { } } } - post("/accounts/{USERNAME}/taler-prepared-transfer/unregistration", { + post("/taler-prepared-transfer/unregistration", { operationId = "unregisterTransfer" description = "Unregister a prepared subject" tags = listOf("Prepared Transfer") - request { - pathParameter<String>("USERNAME") { description = "Account username" } - } response { code(HttpStatusCode.NoContent) { description = "Successfully unregistered" } code(HttpStatusCode.NotFound) { description = "Prepared transfer not found" } diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TransferDAO.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/db/TransferDAO.kt @@ -29,14 +29,14 @@ class TransferDAO(private val db: Database) { /** Result of prepared transfer registration */ sealed interface RegistrationResult { data class Success(val uuid: UUID?): RegistrationResult - data object UnknownAccount: RegistrationResult data object NotExchange: RegistrationResult data object ReservePubReuse: RegistrationResult + data object UnknownCreditor: RegistrationResult } /** Register a prepared transfer */ suspend fun register( - exchangeUsername: String, + account: Payto, type: TransferType, accountPub: EddsaPublicKey, authPub: EddsaPublicKey, @@ -46,13 +46,13 @@ class TransferDAO(private val db: Database) { timestamp: Instant ): RegistrationResult = db.serializable( """ - SELECT out_unknown_account, out_not_exchange, out_reserve_pub_reuse, out_withdrawal_uuid + SELECT out_unknown_creditor, out_not_exchange, out_reserve_pub_reuse, out_withdrawal_uuid FROM register_prepared_transfers ( ?, ?::taler_incoming_type, ?, ?, ?, ?, (?, ?)::taler_amount, ?, ? ) """ ) { - bind(exchangeUsername) + bind(account.canonical) bind(type) bind(accountPub) bind(authPub) @@ -63,7 +63,7 @@ class TransferDAO(private val db: Database) { bind("Taler prepared MAP:$authPub") one { when { - it.getBoolean("out_unknown_account") -> RegistrationResult.UnknownAccount + it.getBoolean("out_unknown_creditor") -> RegistrationResult.UnknownCreditor it.getBoolean("out_not_exchange") -> RegistrationResult.NotExchange it.getBoolean("out_reserve_pub_reuse") -> RegistrationResult.ReservePubReuse else -> RegistrationResult.Success(it.getOptObject("out_withdrawal_uuid")) diff --git a/libeufin-bank/src/test/kotlin/PreparedTransferApiTest.kt b/libeufin-bank/src/test/kotlin/PreparedTransferApiTest.kt @@ -26,18 +26,19 @@ import java.time.Instant import kotlin.test.* class PreparedTransferApiTest { - // GET /accounts/{USERNAME}/taler-prepared-transfer/config + // GET /taler-prepared-transfer/config @Test fun config() = bankSetup { - client.get("/accounts/merchant/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>() + client.get("/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>() } - // POST /accounts/{USERNAME}/taler-prepared-transfer/registration + // POST /taler-prepared-transfer/registration @Test fun registration() = bankSetup { val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() val amount = TalerAmount("KUDOS:1") val valid_req = obj { + "credit_account" to exchangePayto "credit_amount" to amount "type" to "reserve" "alg" to "EdDSA" @@ -50,7 +51,7 @@ class PreparedTransferApiTest { val simpleSubject = TransferSubject.Simple("Taler MAP:$pub", amount) // Valid - val subjects = client.post("/accounts/exchange/taler-prepared-transfer/registration") { + val subjects = client.post("/taler-prepared-transfer/registration") { json(valid_req) }.assertOkJson<SubjectResult> { assertEquals(it.subjects[1], simpleSubject) @@ -58,14 +59,14 @@ class PreparedTransferApiTest { }.subjects // Idempotent - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) }.assertOkJson<SubjectResult> { assertEquals(it.subjects, subjects) } // KYC has a different withdrawal uri - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "type" to "kyc" } @@ -76,7 +77,7 @@ class PreparedTransferApiTest { } // Recurrent only has simple subject - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "recurrent" to true } @@ -85,27 +86,31 @@ class PreparedTransferApiTest { } // Bad signature - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "authorization_sig" to EddsaSignature.rand() } }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE) // Not exchange - client.post("/accounts/merchant/taler-prepared-transfer/registration") { - json(valid_req) + client.post("/taler-prepared-transfer/registration") { + json(valid_req) { + "credit_account" to merchantPayto + } }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) // Unknown account - client.post("/accounts/unknown/taler-prepared-transfer/registration") { - json(valid_req) - }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + client.post("/taler-prepared-transfer/registration") { + json(valid_req) { + "credit_account" to unknownPayto + } + }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR) assertBalance("customer", "+KUDOS:0") assertBalance("exchange", "+KUDOS:0") // Non recurrent accept on then bounce - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "type" to "reserve" } @@ -119,7 +124,7 @@ class PreparedTransferApiTest { } // Withdrawal is aborted on completion - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "type" to "kyc" } @@ -136,7 +141,7 @@ class PreparedTransferApiTest { // Recurrent accept one and delay others val newKey = EddsaPublicKey.randEdsaKey() - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "account_pub" to newKey "authorization_sig" to CryptoUtil.eddsaSign(newKey.raw, priv) @@ -153,7 +158,7 @@ class PreparedTransferApiTest { // Complete pending on recurrent update val kycKey = EddsaPublicKey.randEdsaKey() - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "type" to "kyc" "account_pub" to kycKey @@ -161,7 +166,7 @@ class PreparedTransferApiTest { "recurrent" to true } }.assertOkJson<SubjectResult>() - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "type" to "reserve" "account_pub" to kycKey @@ -178,7 +183,7 @@ class PreparedTransferApiTest { assertBalance("exchange", "+KUDOS:8") // Switching to non recurrent cancel pending - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "type" to "kyc" "account_pub" to kycKey @@ -193,7 +198,7 @@ class PreparedTransferApiTest { val testKey = EddsaPublicKey.randEdsaKey() val testSig = CryptoUtil.eddsaSign(testKey.raw, testPriv) val qr = subjectFmtQrBill(testAuth) - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "account_pub" to testKey "authorization_pub" to testAuth @@ -204,7 +209,7 @@ class PreparedTransferApiTest { tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "type" to "kyc" "account_pub" to testKey @@ -215,7 +220,7 @@ class PreparedTransferApiTest { }.assertOkJson<SubjectResult>() val otherPub = EddsaPublicKey.randEdsaKey() val otherSig = CryptoUtil.eddsaSign(otherPub.raw, testPriv) - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "type" to "reserve" "account_pub" to otherPub @@ -244,11 +249,12 @@ class PreparedTransferApiTest { )) } - // DELETE /accounts/{USERNAME}/taler-prepared-transfer/registration + // DELETE /taler-prepared-transfer/registration @Test fun unregistration() = bankSetup { val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() val valid_req = obj { + "credit_account" to exchangePayto "credit_amount" to "KUDOS:1" "type" to "reserve" "alg" to "EdDSA" @@ -259,7 +265,7 @@ class PreparedTransferApiTest { } // Unknown - client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().toString() json { "timestamp" to now @@ -269,10 +275,10 @@ class PreparedTransferApiTest { }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) // Know - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) }.assertOkJson<SubjectResult>() - client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().toString() json { "timestamp" to now @@ -282,7 +288,7 @@ class PreparedTransferApiTest { }.assertNoContent() // Idempotent - client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().toString() json { "timestamp" to now @@ -292,7 +298,7 @@ class PreparedTransferApiTest { }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) // Bad signature - client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().toString() json { "timestamp" to now @@ -302,7 +308,7 @@ class PreparedTransferApiTest { }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE) // Old timestamp - client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().minusSeconds(1000000).toString() json { "timestamp" to now @@ -320,7 +326,7 @@ class PreparedTransferApiTest { // Pending bounced after deletion val newKey = EddsaPublicKey.randEdsaKey() - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) { "account_pub" to newKey "authorization_sig" to CryptoUtil.eddsaSign(newKey.raw, priv) @@ -332,7 +338,7 @@ class PreparedTransferApiTest { tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending assertBalance("customer", "-KUDOS:3") assertBalance("exchange", "+KUDOS:3") - client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().toString() json { "timestamp" to now diff --git a/libeufin-bank/src/test/kotlin/WireGatewayApiTest.kt b/libeufin-bank/src/test/kotlin/WireGatewayApiTest.kt @@ -396,8 +396,9 @@ class WireGatewayApiTest { } val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json { + "credit_account" to exchangePayto "credit_amount" to "KUDOS:44" "type" to "reserve" "alg" to "EdDSA" diff --git a/libeufin-bank/src/test/kotlin/bench.kt b/libeufin-bank/src/test/kotlin/bench.kt @@ -389,6 +389,7 @@ class Bench { measureAction("wt_register") { val (priv, pub) = accountPubs[it] val valid_req = obj { + "credit_account" to exchangePayto "credit_amount" to "KUDOS:55" "type" to "reserve" "alg" to "EdDSA" @@ -397,10 +398,10 @@ class Bench { "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv) "recurrent" to false } - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) }.assertOkJson<SubjectResult>() - client.post("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/registration") { json(valid_req) }.assertOkJson<SubjectResult>() } @@ -412,10 +413,10 @@ class Bench { "authorization_pub" to pub "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) } - client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + client.post("/taler-prepared-transfer/unregistration") { json(valid_req) }.assertNoContent() - client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + client.post("/taler-prepared-transfer/unregistration") { json(valid_req) }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } diff --git a/libeufin-common/src/main/kotlin/TalerMessage.kt b/libeufin-common/src/main/kotlin/TalerMessage.kt @@ -73,7 +73,7 @@ data class TransferRequest( if (metadata != null && !METADATA_REGEX.matches(metadata)) throw badRequest("metadata '$metadata' is malformed, must match [a-zA-Z0-9-.+:]{1,40}") } - + companion object { private val METADATA_REGEX = Regex("^[a-zA-Z0-9-.:]{1,40}$") } @@ -375,10 +375,14 @@ enum class TransferType { @Serializable @Description("Request to generate a transfer subject") data class SubjectRequest( - @Description("Credit amount for the transfer") - val credit_amount: TalerAmount, + @Description("Payto URI of the credit account") + val credit_account: Payto, @Description("Type of transfer") val type: TransferType, + @Description("Whether subject is recurrent") + val recurrent: Boolean, + @Description("Credit amount for the transfer") + val credit_amount: TalerAmount, @Description("Public key algorithm") val alg: PublicKeyAlg, @Description("Account public key") @@ -387,8 +391,6 @@ data class SubjectRequest( val authorization_pub: EddsaPublicKey, @Description("Authorization signature") val authorization_sig: EddsaSignature, - @Description("Whether subject is recurrent") - val recurrent: Boolean ) @Serializable diff --git a/libeufin-nexus/conf/test.conf b/libeufin-nexus/conf/test.conf @@ -5,6 +5,7 @@ HOST_BASE_URL = https://isotest.postfinance.ch/ebicsweb/ebicsweb BANK_PUBLIC_KEYS_FILE = test/tmp/bank-keys.json CLIENT_PRIVATE_KEYS_FILE = test/tmp/client-keys.json IBAN = CH7789144474425692816 +QR_IBAN = CH4431999123000889012 HOST_ID = PFEBICS USER_ID = PFC00563 PARTNER_ID = PFC00563 diff --git a/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt b/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt @@ -94,6 +94,7 @@ class NexusEbicsConfig( bic = sect.string("bic").require(), name = sect.string("name").require() ) + val qrIban = sect.string("qr_iban").orNull() /** Bank account payto */ val payto = IbanPayto.build(account.iban, account.bic, account.name) diff --git a/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/PreparedTransferApi.kt b/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/PreparedTransferApi.kt @@ -69,7 +69,17 @@ fun Routing.preparedTransferAPI(db: Database, cfg: NexusConfig) = conditional(cf TalerErrorCode.BANK_BAD_SIGNATURE ) - val reference = subjectFmtQrBill(req.authorization_pub) + val iban = req.credit_account.expectIban().iban.toString() + val reference = if (iban == cfg.ebics.account.iban) { + null + } else if (iban == cfg.ebics.qrIban) { + subjectFmtQrBill(req.authorization_pub) + } else { + throw conflict( + "Creditor account '${req.credit_account}' is not supported", + TalerErrorCode.BANK_UNKNOWN_CREDITOR + ) + } when (val result = db.transfer.register( req.type, @@ -91,10 +101,11 @@ fun Routing.preparedTransferAPI(db: Database, cfg: NexusConfig) = conditional(cf RegistrationResult.Success -> { call.respond( SubjectResult( - listOf( - TransferSubject.QrBill(reference, req.credit_amount), - TransferSubject.Simple(fmtIncomingSubject(IncomingType.map, req.authorization_pub), req.credit_amount) - ), + if (reference != null) { + listOf(TransferSubject.QrBill(reference, req.credit_amount)) + } else { + listOf(TransferSubject.Simple(fmtIncomingSubject(IncomingType.map, req.authorization_pub), req.credit_amount)) + }, TalerTimestamp.never() ) ) diff --git a/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/db/TransferDAO.kt b/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/db/TransferDAO.kt @@ -39,7 +39,7 @@ class TransferDAO(private val db: Database) { authPub: EddsaPublicKey, authSig: EddsaSignature, recurrent: Boolean, - referenceNumber: String, + referenceNumber: String?, timestamp: Instant ): RegistrationResult = db.serializable( """ diff --git a/libeufin-nexus/src/test/kotlin/PreparedTransferApiTest.kt b/libeufin-nexus/src/test/kotlin/PreparedTransferApiTest.kt @@ -39,6 +39,7 @@ class PreparedTransferApiTest { val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() val amount = TalerAmount("KUDOS:55") val valid_req = obj { + "credit_account" to "payto://iban/CH7789144474425692816" "credit_amount" to amount "type" to "reserve" "alg" to "EdDSA" @@ -48,23 +49,39 @@ class PreparedTransferApiTest { "recurrent" to false } - val subjects = listOf( - TransferSubject.QrBill(subjectFmtQrBill(pub), amount), - TransferSubject.Simple("Taler MAP:$pub", amount), - ) + val simple = listOf(TransferSubject.Simple("Taler MAP:$pub", amount)) + val qrs = listOf(TransferSubject.QrBill(subjectFmtQrBill(pub), amount)) - // Valid + // Valid simple client.post("/taler-prepared-transfer/registration") { json(valid_req) }.assertOkJson<SubjectResult> { - assertEquals(it.subjects, subjects) + assertEquals(it.subjects, simple) } - // Idempotent + // Idempotent simple client.post("/taler-prepared-transfer/registration") { json(valid_req) }.assertOkJson<SubjectResult> { - assertEquals(it.subjects, subjects) + assertEquals(it.subjects, simple) + } + + // Valid qr + client.post("/taler-prepared-transfer/registration") { + json(valid_req) { + "credit_account" to "payto://iban/CH4431999123000889012" + } + }.assertOkJson<SubjectResult> { + assertEquals(it.subjects, qrs) + } + + // Idempotent qr + client.post("/taler-prepared-transfer/registration") { + json(valid_req) { + "credit_account" to "payto://iban/CH4431999123000889012" + } + }.assertOkJson<SubjectResult> { + assertEquals(it.subjects, qrs) } // Bad signature @@ -74,6 +91,13 @@ class PreparedTransferApiTest { } }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE) + // Unknown account + client.post("/taler-prepared-transfer/registration") { + json(valid_req) { + "credit_account" to grothoffPayto + } + }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR) + // Check authorization field in incoming history val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair() val testKey = EddsaPublicKey.randEdsaKey() @@ -81,6 +105,7 @@ class PreparedTransferApiTest { val qr = subjectFmtQrBill(testAuth) client.post("/taler-prepared-transfer/registration") { json(valid_req) { + "credit_account" to "payto://iban/CH4431999123000889012" "account_pub" to testKey "authorization_pub" to testAuth "authorization_sig" to testSig @@ -149,6 +174,7 @@ class PreparedTransferApiTest { // Know client.post("/taler-prepared-transfer/registration") { json { + "credit_account" to "payto://iban/CH7789144474425692816" "credit_amount" to "KUDOS:55" "type" to "reserve" "alg" to "EdDSA" diff --git a/libeufin-nexus/src/test/kotlin/WireGatewayApiTest.kt b/libeufin-nexus/src/test/kotlin/WireGatewayApiTest.kt @@ -341,6 +341,7 @@ class WireGatewayApiTest { val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() client.post("/taler-prepared-transfer/registration") { json { + "credit_account" to "payto://iban/CH7789144474425692816" "credit_amount" to "CHF:44" "type" to "reserve" "alg" to "EdDSA" diff --git a/libeufin-nexus/src/test/kotlin/bench.kt b/libeufin-nexus/src/test/kotlin/bench.kt @@ -197,6 +197,7 @@ class Bench { measureAction("wt_register") { val (priv, pub) = accountPubs[it] val valid_req = obj { + "credit_account" to "payto://iban/CH7789144474425692816" "credit_amount" to "KUDOS:55" "type" to "reserve" "alg" to "EdDSA"