commit ef2124c9949991cd4a0fbb78356184d06564b5e0
parent 326a349eb491ab80f01e42619fec5c48b37d50da
Author: Antoine A <>
Date: Fri, 3 May 2024 19:23:54 +0900
nexus: support normal account that never bounce
Diffstat:
10 files changed, 89 insertions(+), 34 deletions(-)
diff --git a/common/src/main/kotlin/TalerConfig.kt b/common/src/main/kotlin/TalerConfig.kt
@@ -444,8 +444,8 @@ class TalerConfig internal constructor(
return str
}
- fun requireString(section: String, option: String, type: String = "string"): String =
- lookupString(section, option) ?: throw TalerConfigError.missing(type, section, option)
+ fun requireString(section: String, option: String, type: String? = null): String =
+ lookupString(section, option) ?: throw TalerConfigError.missing(type ?: "string", section, option)
fun requireNumber(section: String, option: String): Int {
val raw = lookupString(section, option) ?: throw TalerConfigError.missing("number", section, option)
diff --git a/contrib/nexus.conf b/contrib/nexus.conf
@@ -40,6 +40,11 @@ CLIENT_PRIVATE_KEYS_FILE = ${LIBEUFIN_NEXUS_HOME}/client-ebics-keys.json
# Typically, it is named after the bank itself.
BANK_DIALECT = postfinance
+# Specify the account type and therefore the indexing behavior.
+# This can either can be normal or exchange.
+# Exchange accounts bounce invalid incoming Taler transactions.
+ACCOUNT_TYPE = exchange
+
[libeufin-nexusdb-postgres]
# Where are the SQL files to setup our tables?
SQL_DIR = $DATADIR/sql/
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt
@@ -37,7 +37,7 @@ class ApiConfig(config: TalerConfig, section: String) {
/** Configuration for libeufin-nexus */
class NexusConfig(val config: TalerConfig) {
- private fun requireString(option: String): String = config.requireString("nexus-ebics", option)
+ private fun requireString(option: String, type: String? = null): String = config.requireString("nexus-ebics", option, type)
private fun requirePath(option: String): Path = config.requirePath("nexus-ebics", option)
/** The bank's currency */
@@ -64,12 +64,16 @@ class NexusConfig(val config: TalerConfig) {
val clientPrivateKeysPath = requirePath("client_private_keys_file")
val fetch = NexusFetchConfig(config)
- val dialect = when (val type = requireString("bank_dialect")) {
+ val dialect = when (val type = requireString("bank_dialect", "dialect")) {
"postfinance" -> Dialect.postfinance
"gls" -> Dialect.gls
- else -> throw TalerConfigError.invalid("dialct", "libeufin-nexus", "bank_dialect", "expected 'postfinance' or 'gls' got '$type'")
+ else -> throw TalerConfigError.invalid("bank dialect", "libeufin-nexus", "bank_dialect", "expected 'postfinance' or 'gls' got '$type'")
+ }
+ val accountType = when (val type = requireString("account_type", "account type")) {
+ "normal" -> AccountType.normal
+ "exchange" -> AccountType.exchange
+ else -> throw TalerConfigError.invalid("account type", "libeufin-nexus", "account_type", "expected 'normal' or 'exchange' got '$type'")
}
-
val wireGatewayApiCfg = config.apiConf("nexus-httpd-wire-gateway-api")
val revenueApiCfg = config.apiConf("nexus-httpd-revenue-api")
}
@@ -104,4 +108,9 @@ fun TalerConfig.apiConf(section: String): ApiConfig? {
sealed interface AuthMethod {
data object None: AuthMethod
data class Bearer(val token: String): AuthMethod
+}
+
+enum class AccountType {
+ normal,
+ exchange
}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -118,18 +118,31 @@ suspend fun ingestOutgoingPayment(
*/
suspend fun ingestIncomingPayment(
db: Database,
- payment: IncomingPayment
+ payment: IncomingPayment,
+ accountType: AccountType
) {
suspend fun bounce(msg: String) {
- val result = db.payment.registerMalformedIncoming(
- payment,
- payment.amount,
- Instant.now()
- )
- if (result.new) {
- logger.info("$payment bounced in '${result.bounceId}': $msg")
- } else {
- logger.debug("$payment already seen and bounced in '${result.bounceId}': $msg")
+ when (accountType) {
+ AccountType.exchange -> {
+ val result = db.payment.registerMalformedIncoming(
+ payment,
+ payment.amount,
+ Instant.now()
+ )
+ if (result.new) {
+ logger.info("$payment bounced in '${result.bounceId}': $msg")
+ } else {
+ logger.debug("$payment already seen and bounced in '${result.bounceId}': $msg")
+ }
+ }
+ AccountType.normal -> {
+ val res = db.payment.registerIncoming(payment)
+ if (res.new) {
+ logger.info("$payment")
+ } else {
+ logger.debug("$payment already seen")
+ }
+ }
}
}
runCatching { parseIncomingTxMetadata(payment.wireTransferSubject) }.fold(
@@ -164,7 +177,7 @@ private suspend fun ingestDocument(
logger.debug("IGNORE $it")
} else {
when (it) {
- is IncomingPayment -> ingestIncomingPayment(db, it)
+ is IncomingPayment -> ingestIncomingPayment(db, it, cfg.accountType)
is OutgoingPayment -> ingestOutgoingPayment(db, it)
is TxNotification.Reversal -> {
logger.error("BOUNCE '${it.msgId}': ${it.reason}")
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -297,15 +297,14 @@ class FakeIncoming: CliktCommand("Genere a fake incoming payment") {
).convert { Payto.parse(it).expectIban() }
override fun run() = cliCmd(logger, common.log) {
- val cfg = loadConfig(common.config)
- val dbCfg = cfg.dbConfig()
- val currency = cfg.requireString("nexus-ebics", "currency")
+ val cfg = loadNexusConfig(common.config)
+ val dbCfg = cfg.config.dbConfig()
val subject = payto.message ?: subject ?: throw Exception("Missing subject")
val amount = payto.amount ?: amount ?: throw Exception("Missing amount")
- if (amount.currency != currency)
- throw Exception("Wrong currency: expected $currency got ${amount.currency}")
+ if (amount.currency != cfg.currency)
+ throw Exception("Wrong currency: expected ${cfg.currency} got ${amount.currency}")
val bankId = run {
val bytes = ByteArray(16)
@@ -313,7 +312,7 @@ class FakeIncoming: CliktCommand("Genere a fake incoming payment") {
Base32Crockford.encode(bytes)
}
- Database(dbCfg, currency).use { db ->
+ Database(dbCfg, amount.currency).use { db ->
ingestIncomingPayment(db,
IncomingPayment(
amount = amount,
@@ -321,7 +320,8 @@ class FakeIncoming: CliktCommand("Genere a fake incoming payment") {
wireTransferSubject = subject,
executionTime = Instant.now(),
bankId = bankId
- )
+ ),
+ cfg.accountType
)
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
@@ -142,6 +142,29 @@ class PaymentDAO(private val db: Database) {
}
}
+ /** Register an incoming payment */
+ suspend fun registerIncoming(
+ paymentData: IncomingPayment
+ ): IncomingRegistrationResult.Success = db.conn { conn ->
+ val stmt = conn.prepareStatement("""
+ SELECT out_found, out_tx_id
+ FROM register_incoming((?,?)::taler_amount,?,?,?,?)
+ """)
+ val executionTime = paymentData.executionTime.micros()
+ stmt.setLong(1, paymentData.amount.value)
+ stmt.setInt(2, paymentData.amount.frac)
+ stmt.setString(3, paymentData.wireTransferSubject)
+ stmt.setLong(4, executionTime)
+ stmt.setString(5, paymentData.debitPaytoUri)
+ stmt.setString(6, paymentData.bankId)
+ stmt.one {
+ IncomingRegistrationResult.Success(
+ it.getLong("out_tx_id"),
+ !it.getBoolean("out_found")
+ )
+ }
+ }
+
/** Query history of incoming transactions */
suspend fun revenueHistory(
params: HistoryParams
diff --git a/nexus/src/test/kotlin/RevenueApiTest.kt b/nexus/src/test/kotlin/RevenueApiTest.kt
@@ -46,7 +46,7 @@ class RevenueApiTest {
},
{
// Common credit transactions
- ingestIncomingPayment(db, genInPay("ignored"))
+ ingestIn(db)
}
),
ignored = listOf(
diff --git a/nexus/src/test/kotlin/WireGatewayApiTest.kt b/nexus/src/test/kotlin/WireGatewayApiTest.kt
@@ -134,7 +134,7 @@ class WireGatewayApiTest {
ignored = listOf(
{
// Ignore malformed incoming transaction
- ingestIncomingPayment(db, genInPay("ignored"))
+ ingestIn(db)
},
{
// Ignore outgoing transaction
@@ -167,7 +167,7 @@ class WireGatewayApiTest {
},
{
// Ignore malformed incoming transaction
- ingestIncomingPayment(db, genInPay("ignored"))
+ ingestIn(db)
},
{
// Ignore malformed outgoing transaction
diff --git a/nexus/src/test/kotlin/helpers.kt b/nexus/src/test/kotlin/helpers.kt
@@ -149,7 +149,12 @@ suspend fun talerableOut(db: Database) {
/** Ingest a talerable incoming transaction */
suspend fun talerableIn(db: Database) {
val reserve_pub = ShortHashCode.rand()
- ingestIncomingPayment(db, genInPay("history test with $reserve_pub reserve pub"))
+ ingestIncomingPayment(db, genInPay("history test with $reserve_pub reserve pub"), AccountType.exchange)
+}
+
+/** Ingest an incoming transaction */
+suspend fun ingestIn(db: Database) {
+ ingestIncomingPayment(db, genInPay("ignored"), AccountType.normal)
}
diff --git a/testbench/src/test/kotlin/IntegrationTest.kt b/testbench/src/test/kotlin/IntegrationTest.kt
@@ -135,7 +135,7 @@ class IntegrationTest {
}
}
- setup("conf/integration.conf") { db ->
+ setup("conf/integration.conf") { db ->
val userPayTo = IbanPayto.rand()
val fiatPayTo = IbanPayto.rand()
@@ -156,14 +156,14 @@ class IntegrationTest {
)
assertException("ERROR: cashin failed: missing exchange account") {
- ingestIncomingPayment(db, payment)
+ ingestIncomingPayment(db, payment, AccountType.exchange)
}
// Create exchange account
bankCmd.run("create-account $flags -u exchange -p password --name 'Mr Money' --exchange")
assertException("ERROR: cashin currency conversion failed: missing conversion rates") {
- ingestIncomingPayment(db, payment)
+ ingestIncomingPayment(db, payment, AccountType.exchange)
}
// Start server
@@ -199,7 +199,7 @@ class IntegrationTest {
checkCount(db, 0, 0, 0)
ingestIncomingPayment(db, payment.copy(
amount = TalerAmount("EUR:0.01"),
- ))
+ ), AccountType.exchange)
checkCount(db, 1, 1, 0)
client.get("http://0.0.0.0:8080/accounts/exchange/transactions") {
basicAuth("exchange", "password")
@@ -213,14 +213,14 @@ class IntegrationTest {
executionTime = Instant.now(),
bankId = "success"
)
- ingestIncomingPayment(db, valid_payment)
+ ingestIncomingPayment(db, valid_payment, AccountType.exchange)
checkCount(db, 2, 1, 1)
client.get("http://0.0.0.0:8080/accounts/exchange/transactions") {
basicAuth("exchange", "password")
}.assertOkJson<BankAccountTransactionsResponse>()
// Check idempotency
- ingestIncomingPayment(db, valid_payment)
+ ingestIncomingPayment(db, valid_payment, AccountType.exchange)
checkCount(db, 2, 1, 1)
// TODO check double insert cashin with different subject
}