Testing.kt (9027B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2024, 2025, 2026 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 package tech.libeufin.nexus.cli 21 22 import com.github.ajalt.clikt.core.CliktCommand 23 import com.github.ajalt.clikt.core.Context 24 import com.github.ajalt.clikt.core.subcommands 25 import com.github.ajalt.clikt.parameters.arguments.* 26 import com.github.ajalt.clikt.parameters.groups.provideDelegate 27 import com.github.ajalt.clikt.parameters.options.convert 28 import com.github.ajalt.clikt.parameters.options.default 29 import com.github.ajalt.clikt.parameters.options.flag 30 import com.github.ajalt.clikt.parameters.options.option 31 import com.github.ajalt.clikt.parameters.options.required 32 import com.github.ajalt.clikt.parameters.types.* 33 import com.github.ajalt.mordant.terminal.* 34 import tech.libeufin.common.* 35 import tech.libeufin.nexus.* 36 import tech.libeufin.nexus.iso20022.* 37 import tech.libeufin.ebics.* 38 import tech.libeufin.ebics.test.txCheck 39 import java.util.zip.* 40 import java.time.Instant 41 import java.io.* 42 43 class Wss : TalerCmd() { 44 override fun help(context: Context) = "Listen to EBICS instant notification over websocket" 45 46 private val ebicsLog by ebicsLogOption() 47 48 override fun run() = cliCmd(logger) { 49 nexusConfig(config).withDb { db, cfg -> 50 val (clientKeys, bankKeys) = expectFullKeys(cfg.ebics) 51 val client = EbicsClient( 52 cfg.ebics.host, 53 httpClient(), 54 db.ebics, 55 EbicsLogger(ebicsLog), 56 clientKeys, 57 bankKeys 58 ) 59 val wssNotifications = listenForNotification(client) 60 if (wssNotifications != null) { 61 while (true) { 62 wssNotifications.receive() 63 logger.debug("{}", wssNotifications) 64 } 65 } 66 } 67 } 68 } 69 70 class FakeIncoming : TalerCmd() { 71 override fun help(context: Context) = "Genere a fake incoming payment" 72 73 private val amount by option( 74 "--amount", 75 help = "The payment amount, credit-payto 'amount' parameter takes the precedence" 76 ).convert { TalerAmount(it) } 77 private val creditFee by option( 78 "--credit-fee", 79 help = "The payment credit fee" 80 ).convert { TalerAmount(it) } 81 private val subject by option( 82 "--subject", 83 help = "The payment subject, credit-payto 'message' parameter takes the precedence" 84 ) 85 private val chQrr by option( 86 "--ch-qrr", 87 help = "The payment reference, credit-payto 'ch-qrr' parameter takes the precedence" 88 ) 89 private val creditPayto by option( 90 "--credit-payto", 91 help = "The credited account IBAN payto URI" 92 ).convert { Payto.parse(it).expectIban() } 93 private val debitPayto by option( 94 "--debit-payto", 95 help = "The debited account IBAN payto URI" 96 ).convert { Payto.parse(it).expectIban() } 97 private val payto by argument( 98 name = "deprecated", 99 help = "deprecated" 100 ).convert { Payto.parse(it).expectIban() }.optional() 101 102 override fun run() = cliCmd(logger) { 103 nexusConfig(config).withDb { db, cfg -> 104 val amount = requireNotNull(creditPayto?.amount ?: payto?.amount ?: amount) { "Missing amount" } 105 val subject = creditPayto?.message ?: payto?.message ?: subject 106 val reference = creditPayto?.chQrr ?: payto?.chQrr ?: chQrr 107 108 creditPayto?.let { 109 if (reference != null) { 110 require(it.iban == cfg.ebics.qrIban) { 111 "Creditor must be the exchange QRR account expected ${cfg.ebics.account.iban} got ${it.iban}" 112 } 113 } else { 114 require(it.iban == cfg.ebics.account.iban) { 115 "Creditor must be the exchange expected ${cfg.ebics.account.iban} got ${it.iban}" 116 } 117 } 118 } 119 120 require(amount.currency == cfg.currency) { 121 "Wrong currency: expected ${cfg.currency} got ${amount.currency}" 122 } 123 registerIncomingPayment( 124 db, cfg.ingest, 125 IncomingPayment( 126 amount = amount, 127 debtor = debitPayto ?: payto ?: IbanPayto.rand("Testing Account", Country.valueOf(cfg.ebics.account.iban.value.substring(0 until 2))), 128 subject = reference ?: subject ?: "", 129 creditFee = creditFee, 130 executionTime = Instant.now(), 131 id = IncomingId(null, randEbicsId(), null) 132 ) 133 ) 134 } 135 } 136 } 137 138 class TxCheck : TalerCmd() { 139 override fun help(context: Context) = "Check transaction semantic" 140 141 override fun run() = cliCmd(logger) { 142 val nexusCgf = nexusConfig(config) 143 val cfg = nexusCgf.ebics 144 val (clientKeys, bankKeys) = expectFullKeys(cfg) 145 val order = cfg.dialect.downloadDoc(OrderDoc.acknowledgement) 146 val client = httpClient() 147 val result = txCheck(client, cfg.host, clientKeys, bankKeys, order[0], cfg.dialect.directDebit()) 148 println("$result") 149 } 150 } 151 152 enum class ListKind { 153 incoming, 154 outgoing, 155 initiated; 156 157 fun description(): String = when (this) { 158 incoming -> "Incoming transactions" 159 outgoing -> "Outgoing transactions" 160 initiated -> "Initiated transactions" 161 } 162 } 163 164 class EbicsDownload : TalerCmd("ebics-btd") { 165 override fun help(context: Context) = "Perform EBICS requests" 166 167 private val type by option().default("BTD") 168 private val name by option() 169 private val scope by option() 170 private val messageName by option() 171 private val messageVersion by option() 172 private val container by option() 173 private val option by option() 174 private val ebicsLog by ebicsLogOption() 175 private val pinnedStart by option( 176 help = "Constant YYYY-MM-DD date for the earliest document" + 177 " to download (only consumed in --transient mode). The" + 178 " latest document is always until the current time." 179 ) 180 private val peek by option( 181 "--peek", 182 help = "Do not consume fetched documents" 183 ).flag() 184 private val dryRun by option().flag() 185 186 class DryRun : Exception() 187 188 override fun run() = cliCmd(logger) { 189 nexusConfig(config).withDb { db, cfg -> 190 val (clientKeys, bankKeys) = expectFullKeys(cfg.ebics) 191 val pinnedStartVal = pinnedStart 192 val pinnedStartArg = if (pinnedStartVal != null) { 193 logger.debug("Pinning start date to: $pinnedStartVal") 194 dateToInstant(pinnedStartVal) 195 } else null 196 val client = EbicsClient( 197 cfg.ebics.host, 198 httpClient(), 199 db.ebics, 200 EbicsLogger(ebicsLog), 201 clientKeys, 202 bankKeys 203 ) 204 try { 205 client.download( 206 EbicsOrder.V3(type, name, scope, messageName, messageVersion, container, option), 207 pinnedStartArg, 208 null, 209 peek 210 ) { stream -> 211 if (container == "ZIP") { 212 stream.unzipEach { fileName, xmlContent -> 213 println(fileName) 214 println(xmlContent.readText()) 215 } 216 } else { 217 println(stream.readText()) 218 } 219 if (dryRun) throw DryRun() 220 } 221 } catch (e: DryRun) { 222 // We throw DryRun to not consume files while testing 223 } 224 } 225 } 226 } 227 228 class IbanGen : CliktCommand("gen") { 229 override fun help(context: Context) = "Generate fake IBANs for testing" 230 231 private val country by option().enum<Country>().required() 232 233 override fun run() { 234 println(IBAN.rand(country)) 235 } 236 } 237 238 class IbanCmd : CliktCommand("iban") { 239 init { 240 subcommands(IbanGen()) 241 } 242 243 override fun run() = Unit 244 } 245 246 class TestingCmd : CliktCommand("testing") { 247 init { 248 subcommands(IbanCmd(), FakeIncoming(), ListCmd(), EbicsDownload(), TxCheck(), Wss()) 249 } 250 251 override fun help(context: Context) = "Testing helper commands" 252 253 override fun run() = Unit 254 }