commit 0dc33d16270b91eb0d2e3a78d08e319c5e4423cd
parent 8f18b1b5c872a6e83d52539e88fafc89d5dba7a9
Author: Antoine A <>
Date: Fri, 3 May 2024 17:53:27 +0900
nexus: add list cmd
Diffstat:
8 files changed, 329 insertions(+), 72 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
@@ -37,65 +37,6 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
-@Serializable(with = DecimalNumber.Serializer::class)
-class DecimalNumber {
- val value: Long
- val frac: Int
-
- constructor(value: Long, frac: Int) {
- this.value = value
- this.frac = frac
- }
- constructor(encoded: String) {
- val match = PATTERN.matchEntire(encoded) ?: throw badRequest("Invalid decimal number format")
- val (value, frac) = match.destructured
- this.value = value.toLongOrNull() ?: throw badRequest("Invalid value")
- if (this.value > TalerAmount.MAX_VALUE) throw badRequest("Value specified in decimal number is too large")
- this.frac = if (frac.isEmpty()) {
- 0
- } else {
- var tmp = frac.toIntOrNull() ?: throw badRequest("Invalid fractional value")
- repeat(8 - frac.length) {
- tmp *= 10
- }
- tmp
- }
- }
-
- override fun equals(other: Any?): Boolean {
- return other is DecimalNumber &&
- other.value == this.value &&
- other.frac == this.frac
- }
-
- override fun toString(): String {
- if (frac == 0) {
- return "$value"
- } else {
- return "$value.${frac.toString().padStart(8, '0')}"
- .dropLastWhile { it == '0' } // Trim useless fractional trailing 0
- }
- }
-
- internal object Serializer : KSerializer<DecimalNumber> {
- override val descriptor: SerialDescriptor =
- PrimitiveSerialDescriptor("DecimalNumber", PrimitiveKind.STRING)
-
- override fun serialize(encoder: Encoder, value: DecimalNumber) {
- encoder.encodeString(value.toString())
- }
-
- override fun deserialize(decoder: Decoder): DecimalNumber {
- return DecimalNumber(decoder.decodeString())
- }
- }
-
- companion object {
- private val PATTERN = Regex("([0-9]+)(?:\\.([0-9]{1,8}))?")
- }
-}
-
-
/**
* Internal representation of relative times. The
* "forever" case is represented with Long.MAX_VALUE.
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
@@ -20,7 +20,6 @@
package tech.libeufin.bank.db
import tech.libeufin.bank.ConversionRate
-import tech.libeufin.bank.DecimalNumber
import tech.libeufin.bank.RoundingMode
import tech.libeufin.common.*
import tech.libeufin.common.db.*
diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt
@@ -18,7 +18,6 @@
*/
import org.junit.Test
-import tech.libeufin.bank.DecimalNumber
import tech.libeufin.bank.db.TransactionDAO.BankTransactionResult
import tech.libeufin.bank.db.WithdrawalDAO.WithdrawalCreationResult
import tech.libeufin.common.*
diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt
@@ -115,6 +115,64 @@ class ExchangeUrl {
}
}
+@Serializable(with = DecimalNumber.Serializer::class)
+class DecimalNumber {
+ val value: Long
+ val frac: Int
+
+ constructor(value: Long, frac: Int) {
+ this.value = value
+ this.frac = frac
+ }
+ constructor(encoded: String) {
+ val match = PATTERN.matchEntire(encoded) ?: throw badRequest("Invalid decimal number format")
+ val (value, frac) = match.destructured
+ this.value = value.toLongOrNull() ?: throw badRequest("Invalid value")
+ if (this.value > TalerAmount.MAX_VALUE) throw badRequest("Value specified in decimal number is too large")
+ this.frac = if (frac.isEmpty()) {
+ 0
+ } else {
+ var tmp = frac.toIntOrNull() ?: throw badRequest("Invalid fractional value")
+ repeat(8 - frac.length) {
+ tmp *= 10
+ }
+ tmp
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is DecimalNumber &&
+ other.value == this.value &&
+ other.frac == this.frac
+ }
+
+ override fun toString(): String {
+ if (frac == 0) {
+ return "$value"
+ } else {
+ return "$value.${frac.toString().padStart(8, '0')}"
+ .dropLastWhile { it == '0' } // Trim useless fractional trailing 0
+ }
+ }
+
+ internal object Serializer : KSerializer<DecimalNumber> {
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("DecimalNumber", PrimitiveKind.STRING)
+
+ override fun serialize(encoder: Encoder, value: DecimalNumber) {
+ encoder.encodeString(value.toString())
+ }
+
+ override fun deserialize(decoder: Decoder): DecimalNumber {
+ return DecimalNumber(decoder.decodeString())
+ }
+ }
+
+ companion object {
+ private val PATTERN = Regex("([0-9]+)(?:\\.([0-9]{1,8}))?")
+ }
+}
+
@Serializable(with = TalerAmount.Serializer::class)
class TalerAmount {
val value: Long
@@ -149,6 +207,8 @@ class TalerAmount {
}
}
+ fun number(): DecimalNumber = DecimalNumber(value, frac)
+
override fun equals(other: Any?): Boolean {
return other is TalerAmount &&
other.value == this.value &&
diff --git a/common/src/main/kotlin/db/types.kt b/common/src/main/kotlin/db/types.kt
@@ -22,6 +22,7 @@ package tech.libeufin.common.db
import tech.libeufin.common.BankPaytoCtx
import tech.libeufin.common.Payto
import tech.libeufin.common.TalerAmount
+import tech.libeufin.common.DecimalNumber
import java.sql.ResultSet
fun ResultSet.getAmount(name: String, currency: String): TalerAmount {
@@ -32,6 +33,13 @@ fun ResultSet.getAmount(name: String, currency: String): TalerAmount {
)
}
+fun ResultSet.getDecimal(name: String): DecimalNumber {
+ return DecimalNumber(
+ getLong("${name}_val"),
+ getInt("${name}_frac")
+ )
+}
+
fun ResultSet.getBankPayto(payto: String, name: String, ctx: BankPaytoCtx): String {
return Payto.parse(getString(payto)).bank(getString(name), ctx)
}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -27,14 +27,14 @@ package tech.libeufin.nexus
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
-import com.github.ajalt.clikt.parameters.arguments.argument
-import com.github.ajalt.clikt.parameters.arguments.convert
-import com.github.ajalt.clikt.parameters.groups.provideDelegate
+import com.github.ajalt.clikt.parameters.arguments.*
+import com.github.ajalt.clikt.parameters.groups.*
import com.github.ajalt.clikt.parameters.options.*
-import com.github.ajalt.clikt.parameters.types.path
+import com.github.ajalt.clikt.parameters.types.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlin.io.path.*
+import kotlin.math.max
import io.ktor.server.application.*
import org.slf4j.Logger
import org.slf4j.event.Level
@@ -190,9 +190,142 @@ class FakeIncoming: CliktCommand("Genere a fake incoming payment") {
}
}
+enum class ListKind {
+ incoming,
+ outgoing,
+ initiated;
+
+ fun description(): String = when (this) {
+ incoming -> "Incoming transactions"
+ outgoing -> "Outgoing transactions"
+ initiated -> "Initiated transactions"
+ }
+}
+
+class ListCmd: CliktCommand("List nexus transactions", name = "list") {
+ private val common by CommonOption()
+ private val kind: ListKind by argument(
+ help = "Which list to print",
+ helpTags = ListKind.entries.map { Pair(it.name, it.description()) }.toMap()
+ ).enum<ListKind>()
+
+ override fun run() = cliCmd(logger, common.log) {
+ val cfg = loadConfig(common.config)
+ val dbCfg = cfg.dbConfig()
+ val currency = cfg.requireString("nexus-ebics", "currency")
+
+ Database(dbCfg).use { db ->
+ fun fmtPayto(payto: String?): String {
+ if (payto == null) return ""
+ try {
+ val parsed = Payto.parse(payto).expectIban()
+ return buildString {
+ append(parsed.iban.toString())
+ if (parsed.bic != null) append(" ${parsed.bic}")
+ if (parsed.receiverName != null) append(" ${parsed.receiverName}")
+ }
+ } catch (e: Exception) {
+ return payto
+ }
+ }
+ val (columnNames, rows) = when (kind) {
+ ListKind.incoming -> {
+ val txs = db.payment.metadataIncoming()
+ Pair(
+ listOf(
+ "transaction", "id", "reserve_pub", "debtor", "subject"
+ ),
+ txs.map {
+ listOf(
+ "${it.date} ${it.amount}",
+ it.id,
+ it.reservePub?.toString() ?: "",
+ fmtPayto(it.debtor),
+ it.subject
+ )
+ }
+ )
+ }
+ ListKind.outgoing -> {
+ val txs = db.payment.metadataOutgoing()
+ Pair(
+ listOf(
+ "transaction", "id", "creditor", "subject"
+ ),
+ txs.map {
+ listOf(
+ "${it.date} ${it.amount}",
+ it.id,
+ fmtPayto(it.creditor),
+ it.subject ?: ""
+ )
+ }
+ )
+ }
+ ListKind.initiated -> {
+ val txs = db.payment.metadataInitiated()
+ Pair(
+ listOf(
+ "transaction", "id", "submission", "creditor", "status", "subject"
+ ),
+ txs.map {
+ listOf(
+ "${it.date} ${it.amount}",
+ it.id,
+ "${it.submissionTime} ${it.submissionCounter}",
+ fmtPayto(it.creditor),
+ "${it.status} ${it.msg ?: ""}".trim(),
+ it.subject
+ )
+ }
+ )
+ }
+ }
+ val cols: List<Pair<String, Int>> = columnNames.mapIndexed { i, name ->
+ val maxRow: Int = rows.asSequence().map { it[i].length }.maxOrNull() ?: 0
+ Pair(name, max(name.length, maxRow))
+ }
+ val table = buildString {
+ fun padding(length: Int) {
+ repeat(length) { append (" ") }
+ }
+ var first = true
+ for ((name, len) in cols) {
+ if (!first) {
+ append("|")
+ } else {
+ first = false
+ }
+ val pad = len - name.length
+ padding(pad / 2)
+ append(name)
+ padding(pad / 2 + if (pad % 2 == 0) { 0 } else { 1 })
+ }
+ append("\n")
+ for (row in rows) {
+ var first = true
+ for ((met, str) in cols.zip(row)) {
+ if (!first) {
+ append("|")
+ } else {
+ first = false
+ }
+ val (name, len) = met
+ val pad = len - str.length
+ append(str)
+ padding(pad)
+ }
+ append("\n")
+ }
+ }
+ print(table)
+ }
+ }
+}
+
class TestingCmd : CliktCommand("Testing helper commands", name = "testing") {
init {
- subcommands(FakeIncoming())
+ subcommands(FakeIncoming(), ListCmd())
}
override fun run() = Unit
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
@@ -19,10 +19,8 @@
package tech.libeufin.nexus.db
-import tech.libeufin.common.EddsaPublicKey
-import tech.libeufin.common.TalerAmount
-import tech.libeufin.common.db.one
-import tech.libeufin.common.micros
+import tech.libeufin.common.db.*
+import tech.libeufin.common.*
import tech.libeufin.nexus.IncomingPayment
import tech.libeufin.nexus.OutgoingPayment
import java.time.Instant
@@ -128,4 +126,120 @@ class PaymentDAO(private val db: Database) {
}
}
}
-}
-\ No newline at end of file
+
+ /** List incoming transaction metadata for debugging */
+ suspend fun metadataIncoming(): List<IncomingTxMetadata> = db.conn { conn ->
+ val stmt = conn.prepareStatement("""
+ SELECT
+ (amount).val as amount_val
+ ,(amount).frac AS amount_frac
+ ,wire_transfer_subject
+ ,execution_time
+ ,debit_payto_uri
+ ,bank_id
+ ,reserve_public_key
+ FROM incoming_transactions
+ LEFT OUTER JOIN talerable_incoming_transactions using (incoming_transaction_id)
+ ORDER BY execution_time
+ """)
+ stmt.all {
+ IncomingTxMetadata(
+ date = it.getLong("execution_time").asInstant(),
+ amount = it.getDecimal("amount"),
+ subject = it.getString("wire_transfer_subject"),
+ debtor = it.getString("debit_payto_uri"),
+ id = it.getString("bank_id"),
+ reservePub = it.getBytes("reserve_public_key")?.run { EddsaPublicKey(this) }
+ )
+ }
+ }
+
+ /** List outgoing transaction metadata for debugging */
+ suspend fun metadataOutgoing(): List<OutgoingTxMetadata> = db.conn { conn ->
+ val stmt = conn.prepareStatement("""
+ SELECT
+ (amount).val as amount_val
+ ,(amount).frac AS amount_frac
+ ,wire_transfer_subject
+ ,execution_time
+ ,credit_payto_uri
+ ,message_id
+ FROM outgoing_transactions
+ ORDER BY execution_time
+ """)
+ stmt.all {
+ OutgoingTxMetadata(
+ date = it.getLong("execution_time").asInstant(),
+ amount = it.getDecimal("amount"),
+ subject = it.getString("wire_transfer_subject"),
+ creditor = it.getString("credit_payto_uri"),
+ id = it.getString("message_id")
+ )
+ }
+ }
+
+ /** List initiated transaction metadata for debugging */
+ suspend fun metadataInitiated(): List<InitiatedTxMetadata> = db.conn { conn ->
+ val stmt = conn.prepareStatement("""
+ SELECT
+ (amount).val as amount_val
+ ,(amount).frac AS amount_frac
+ ,wire_transfer_subject
+ ,initiation_time
+ ,last_submission_time
+ ,submission_counter
+ ,credit_payto_uri
+ ,submitted
+ ,request_uid
+ ,failure_message
+ FROM initiated_outgoing_transactions
+ ORDER BY initiation_time
+ """)
+ stmt.all {
+ InitiatedTxMetadata(
+ date = it.getLong("initiation_time").asInstant(),
+ amount = it.getDecimal("amount"),
+ subject = it.getString("wire_transfer_subject"),
+ creditor = it.getString("credit_payto_uri"),
+ id = it.getString("request_uid"),
+ status = it.getString("submitted"),
+ msg = it.getString("failure_message"),
+ submissionTime = it.getLong("last_submission_time").asInstant(),
+ submissionCounter = it.getInt("submission_counter")
+ )
+ }
+ }
+}
+
+/** Incoming transaction metadata for debugging */
+data class IncomingTxMetadata(
+ val date: Instant,
+ val amount: DecimalNumber,
+ val subject: String,
+ val debtor: String,
+ val id: String,
+ val reservePub: EddsaPublicKey?
+)
+
+/** Outgoing transaction metadata for debugging */
+data class OutgoingTxMetadata(
+ val date: Instant,
+ val amount: DecimalNumber,
+ val subject: String?,
+ val creditor: String?,
+ val id: String,
+ // TODO when merged with v11
+)
+
+/** Initiated metadata for debugging */
+data class InitiatedTxMetadata(
+ val date: Instant,
+ val amount: DecimalNumber,
+ val subject: String,
+ val creditor: String,
+ val id: String,
+ val status: String,
+ val msg: String?,
+ val submissionTime: Instant,
+ val submissionCounter: Int
+)
+\ No newline at end of file
diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt
@@ -163,6 +163,9 @@ class Cli : CliktCommand("Run integration tests on banks provider") {
put("report", "Fetch BankToCustomerAccountReport", "ebics-fetch $ebicsFlags report")
put("notification", "Fetch BankToCustomerDebitCreditNotification", "ebics-fetch $ebicsFlags notification")
put("statement", "Fetch BankToCustomerStatement", "ebics-fetch $ebicsFlags statement")
+ put("list-incoming", "List incoming transaction", "testing list $flags incoming")
+ put("list-outgoing", "List outgoing transaction", "testing list $flags outgoing")
+ put("list-initiated", "List initiated payments", "testing list $flags initiated")
put("submit", "Submit pending transactions", "ebics-submit $ebicsFlags")
put("setup", "Setup", "ebics-setup $flags")
put("reset-keys", suspend {