gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] 02/03: Moving CaMt-JSON mapping to util.


From: gnunet
Subject: [libeufin] 02/03: Moving CaMt-JSON mapping to util.
Date: Fri, 21 Apr 2023 20:23:32 +0200

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository libeufin.

commit 5bca2e1eedeb85d6b206413e7166018dce748f44
Author: MS <ms@taler.net>
AuthorDate: Fri Apr 21 20:17:15 2023 +0200

    Moving CaMt-JSON mapping to util.
    
    In the context of the buy-in monitor, that lets
    Sandbox use such mapping to process Nexus transactions.
---
 .../main/kotlin/tech/libeufin/nexus/Anastasis.kt   |  14 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt    |  11 +-
 .../main/kotlin/tech/libeufin/nexus/FacadeUtil.kt  |   6 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt |  45 +--
 .../tech/libeufin/nexus/iso20022/GbicRules.kt      |   1 +
 .../tech/libeufin/nexus/iso20022/Iso20022.kt       | 299 ++----------------
 .../kotlin/tech/libeufin/nexus/server/Helpers.kt   |   7 +-
 .../main/kotlin/tech/libeufin/nexus/server/JSON.kt |  52 +---
 .../tech/libeufin/nexus/server/NexusServer.kt      |   1 -
 .../nexus/xlibeufinbank/XLibeufinBankNexus.kt      |  15 +-
 util/src/main/kotlin/CamtJsonMapping.kt            | 334 +++++++++++++++++++++
 util/src/main/kotlin/DB.kt                         |   2 +-
 util/src/main/kotlin/HTTP.kt                       |   6 +-
 util/src/main/kotlin/amounts.kt                    |   3 +
 util/src/main/kotlin/strings.kt                    |   9 +-
 15 files changed, 418 insertions(+), 387 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt
index a7b51fd1..b75755ef 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt
@@ -1,9 +1,9 @@
 package tech.libeufin.nexus
 
+import TransactionDetails
 import io.ktor.client.*
 import io.ktor.http.*
 import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.iso20022.TransactionDetails
 import tech.libeufin.nexus.server.PermissionQuery
 import tech.libeufin.nexus.server.expectNonNull
 import tech.libeufin.nexus.server.expectUrlParameter
@@ -49,7 +49,13 @@ fun anastasisFilter(payment: NexusBankTransactionEntity, 
txDtls: TransactionDeta
         logger.warn("missing debtor agent")
         return
     }
-    if (debtorAgent.bic == null) {
+    /**
+     * This block either assigns a non-null BIC to the 'bic'
+     * variable, or causes this function (anastasisFilter())
+     * to return.  This last action ensures that the payment
+     * being processed won't show up in the Anastasis facade.
+     */
+    val bic: String = debtorAgent.bic ?: run {
         logger.warn("Not allowing transactions missing the BIC.  IBAN and 
name: ${debtorIban}, $debtorName")
         return
     }
@@ -58,7 +64,9 @@ fun anastasisFilter(payment: NexusBankTransactionEntity, 
txDtls: TransactionDeta
         subject = txDtls.unstructuredRemittanceInformation
         timestampMs = System.currentTimeMillis()
         debtorPaytoUri = buildIbanPaytoUri(
-            debtorIban, debtorAgent.bic, debtorName,
+            debtorIban,
+            bic,
+            debtorName,
         )
     }
 }
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
index 6b152858..239515e8 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -19,6 +19,7 @@
 
 package tech.libeufin.nexus
 
+import EntryStatus
 import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import org.jetbrains.exposed.dao.*
@@ -31,16 +32,6 @@ import tech.libeufin.util.*
 import java.sql.Connection
 import kotlin.reflect.typeOf
 
-
-enum class EntryStatus {
-    // Booked
-    BOOK,
-    // Pending
-    PDNG,
-    // Informational
-    INFO,
-}
-
 /**
  * This table holds the values that exchange gave to issue a payment,
  * plus a reference to the prepared pain.001 version of.  Note that
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt
index 9d1e1fe4..bab3bb48 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt
@@ -1,5 +1,8 @@
 package tech.libeufin.nexus
 
+import CamtBankAccountEntry
+import EntryStatus
+import TransactionDetails
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import io.ktor.http.*
 import org.jetbrains.exposed.dao.flushCache
@@ -7,9 +10,6 @@ import org.jetbrains.exposed.sql.SortOrder
 import org.jetbrains.exposed.sql.and
 import org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
-import tech.libeufin.nexus.iso20022.CreditDebitIndicator
-import tech.libeufin.nexus.iso20022.TransactionDetails
 import tech.libeufin.nexus.server.NexusFacadeType
 
 // Mainly used to resort the last processed transaction ID.
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
index 8cdbae02..131e4a76 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
@@ -19,6 +19,8 @@
 
 package tech.libeufin.nexus
 
+import CamtBankAccountEntry
+import TransactionDetails
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import io.ktor.server.application.ApplicationCall
 import io.ktor.server.application.call
@@ -36,7 +38,6 @@ import io.ktor.server.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.async
 import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.currentCoroutineContext
 import org.jetbrains.exposed.dao.Entity
 import org.jetbrains.exposed.dao.id.IdTable
 import org.jetbrains.exposed.sql.*
@@ -44,7 +45,6 @@ import 
org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
 import tech.libeufin.nexus.bankaccount.addPaymentInitiation
 import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
-import tech.libeufin.nexus.bankaccount.getBankAccount
 import tech.libeufin.nexus.iso20022.*
 import tech.libeufin.nexus.server.*
 import tech.libeufin.util.*
@@ -159,15 +159,6 @@ fun customConverter(body: Any): String {
     return jacksonObjectMapper().writeValueAsString(body)
 }
 
-/**
- * Tries to extract a valid reserve public key from the raw subject line
- */
-fun extractReservePubFromSubject(rawSubject: String): String? {
-    val re = "\\b[a-z0-9A-Z]{52}\\b".toRegex()
-    val result = re.find(rawSubject.replace("[\n]+".toRegex(), "")) ?: return 
null
-    return result.value.uppercase()
-}
-
 // Handle a Taler Wire Gateway /transfer request.
 private suspend fun talerTransfer(call: ApplicationCall) {
     val transferRequest = call.receive<TalerTransferRequest>()
@@ -266,13 +257,8 @@ fun talerFilter(
         logger.warn("non-iban debtor account")
         return
     }
-    val debtorAgent = txDtls.debtorAgent
-    if (debtorAgent == null) {
-        // FIXME: Report payment, we can't even send it back
-        logger.warn("missing debtor agent")
-        return
-    }
-    if (debtorAgent.bic == null) {
+    val debtorBic = txDtls.debtorAgent?.bic
+    if (debtorBic == null) {
         logger.warn("Not allowing transactions missing the BIC.  IBAN and 
name: ${debtorIban}, $debtorName")
         return
     }
@@ -314,7 +300,7 @@ fun talerFilter(
         timestampMs = System.currentTimeMillis()
         debtorPaytoUri = buildIbanPaytoUri(
             debtorIban,
-            debtorAgent.bic,
+            debtorBic,
             debtorName
         )
     }
@@ -360,7 +346,8 @@ fun maybeTalerRefunds(bankAccount: NexusBankAccountEntity, 
lastSeenId: Long) {
                 it[NexusBankTransactionsTable.transactionJson],
                 CamtBankAccountEntry::class.java
             )
-            if (paymentData.batches == null) {
+            val batches = paymentData.batches
+            if (batches == null) {
                 logger.error(
                     "Empty wire details encountered in transaction with" +
                             " AcctSvcrRef: ${paymentData.accountServicerRef}." 
+
@@ -371,23 +358,23 @@ fun maybeTalerRefunds(bankAccount: 
NexusBankAccountEntity, lastSeenId: Long) {
                     "Unexpected void payment, cannot refund"
                 )
             }
-            val debtorAccount = 
paymentData.batches[0].batchTransactions[0].details.debtorAccount
-            if (debtorAccount?.iban == null) {
+            val debtorIban = 
batches[0].batchTransactions[0].details.debtorAccount?.iban
+            if (debtorIban == null) {
                 logger.error("Could not find a IBAN to refund in transaction 
(AcctSvcrRef): ${paymentData.accountServicerRef}, aborting refund")
                 throw NexusError(HttpStatusCode.InternalServerError, "IBAN to 
refund not found")
             }
-            val debtorAgent = 
paymentData.batches[0].batchTransactions[0].details.debtorAgent
+            val debtorAgent = 
batches[0].batchTransactions[0].details.debtorAgent
             if (debtorAgent?.bic == null) {
                 logger.error("Could not find the BIC of refundable IBAN at 
transaction (AcctSvcrRef): ${paymentData.accountServicerRef}, aborting refund")
                 throw NexusError(HttpStatusCode.InternalServerError, "BIC to 
refund not found")
             }
-            val debtorPerson = 
paymentData.batches[0].batchTransactions[0].details.debtor
-            if (debtorPerson?.name == null) {
+            val debtorName = 
batches[0].batchTransactions[0].details.debtor?.name
+            if (debtorName == null) {
                 logger.error("Could not find the owner's name of refundable 
IBAN at transaction (AcctSvcrRef): ${paymentData.accountServicerRef}, aborting 
refund")
                 throw NexusError(HttpStatusCode.InternalServerError, "Name to 
refund not found")
             }
             // FIXME: investigate this amount!
-            val amount = paymentData.batches[0].batchTransactions[0].amount
+            val amount = batches[0].batchTransactions[0].amount
             NexusAssert(
                 it[NexusBankTransactionsTable.creditDebitIndicator] == "CRDT" 
&&
                         it[NexusBankTransactionsTable.bankAccount] == 
bankAccount.id,
@@ -396,10 +383,10 @@ fun maybeTalerRefunds(bankAccount: 
NexusBankAccountEntity, lastSeenId: Long) {
             // FIXME #7116
             addPaymentInitiation(
                 Pain001Data(
-                    creditorIban = debtorAccount.iban,
+                    creditorIban = debtorIban,
                     creditorBic = debtorAgent.bic,
-                    creditorName = debtorPerson.name,
-                    subject = "Taler refund of: 
${paymentData.batches[0].batchTransactions[0].details.unstructuredRemittanceInformation}",
+                    creditorName = debtorName,
+                    subject = "Taler refund of: 
${batches[0].batchTransactions[0].details.unstructuredRemittanceInformation}",
                     sum = amount.value,
                     currency = amount.currency
                 ),
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt
index df6fc4a7..2a83e847 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt
@@ -18,6 +18,7 @@
  */
 
 package tech.libeufin.nexus.iso20022
+import CreditDebitIndicator
 
 /**
  * Extra rules for German Banking Industry Committee (GBIC) for ISO 20022.
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
index a8509323..9d7ed3ea 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
@@ -22,6 +22,21 @@
  */
 package tech.libeufin.nexus.iso20022
 
+import AgentIdentification
+import Batch
+import BatchTransaction
+import CamtBankAccountEntry
+import CashAccount
+import CreditDebitIndicator
+import CurrencyAmount
+import CurrencyExchange
+import GenericId
+import OrganizationIdentification
+import PartyIdentification
+import PostalAddress
+import PrivateIdentification
+import ReturnInfo
+import TransactionDetails
 import com.fasterxml.jackson.annotation.JsonIgnore
 import com.fasterxml.jackson.annotation.JsonInclude
 import com.fasterxml.jackson.annotation.JsonValue
@@ -34,18 +49,13 @@ import org.w3c.dom.Document
 import tech.libeufin.nexus.*
 import tech.libeufin.nexus.bankaccount.IngestedTransactionsCount
 import tech.libeufin.nexus.bankaccount.findDuplicate
-import tech.libeufin.nexus.server.CurrencyAmount
-import tech.libeufin.nexus.server.toPlainString
 import tech.libeufin.util.*
+import toPlainString
 import java.time.Instant
 import java.time.ZoneId
 import java.time.ZonedDateTime
 import java.time.format.DateTimeFormatter
 
-enum class CreditDebitIndicator {
-    DBIT,
-    CRDT
-}
 
 enum class CashManagementResponseType(@get:JsonValue val jsonName: String) {
     Report("report"), Statement("statement"), Notification("notification")
@@ -66,22 +76,6 @@ data class CamtReport(
     val entries: List<CamtBankAccountEntry>
 )
 
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class GenericId(
-    val id: String,
-    val schemeName: String?,
-    val proprietarySchemeName: String?,
-    val issuer: String?
-)
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class CashAccount(
-    val name: String?,
-    val currency: String?,
-    val iban: String?,
-    val otherId: GenericId?
-)
-
 @JsonInclude(JsonInclude.Include.NON_NULL)
 data class Balance(
     val type: String?,
@@ -107,265 +101,6 @@ data class CamtParseResult(
     val reports: List<CamtReport>
 )
 
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class PrivateIdentification(
-    val birthDate: String?,
-    val provinceOfBirth: String?,
-    val cityOfBirth: String?,
-    val countryOfBirth: String?
-)
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class OrganizationIdentification(
-    val bic: String?,
-    val lei: String?
-)
-
-/**
- * Identification of a party, which can be a private party
- * or an organization.
- *
- * Mapping of ISO 20022 PartyIdentification135.
- */
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class PartyIdentification(
-    val name: String?,
-    val countryOfResidence: String?,
-    val privateId: PrivateIdentification?,
-    val organizationId: OrganizationIdentification?,
-    val postalAddress: PostalAddress?,
-
-    /**
-     * Identification that applies to both private parties and organizations.
-     */
-    val otherId: GenericId?
-)
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class PostalAddress(
-    val addressCode: String?,
-    val addressProprietaryId: String?,
-    val addressProprietarySchemeName: String?,
-    val addressProprietaryIssuer: String?,
-    val department: String?,
-    val subDepartment: String?,
-    val streetName: String?,
-    val buildingNumber: String?,
-    val buildingName: String?,
-    val floor: String?,
-    val postBox: String?,
-    val room: String?,
-    val postCode: String?,
-    val townName: String?,
-    val townLocationName: String?,
-    val districtName: String?,
-    val countrySubDivision: String?,
-    val country: String?,
-    val addressLines: List<String>
-)
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class AgentIdentification(
-    val name: String?,
-
-    val bic: String?,
-
-    /**
-     * Legal entity identification.
-     */
-    val lei: String?,
-
-    val clearingSystemMemberId: String?,
-
-    val clearingSystemCode: String?,
-
-    val proprietaryClearingSystemCode: String?,
-
-    val postalAddress: PostalAddress?,
-
-    val otherId: GenericId?
-)
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class CurrencyExchange(
-    val sourceCurrency: String,
-    val targetCurrency: String,
-    val unitCurrency: String?,
-    val exchangeRate: String,
-    val contractId: String?,
-    val quotationDate: String?
-)
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class TransactionDetails(
-    val debtor: PartyIdentification?,
-    val debtorAccount: CashAccount?,
-    val debtorAgent: AgentIdentification?,
-    val creditor: PartyIdentification?,
-    val creditorAccount: CashAccount?,
-    val creditorAgent: AgentIdentification?,
-    val ultimateCreditor: PartyIdentification?,
-    val ultimateDebtor: PartyIdentification?,
-
-    val endToEndId: String? = null,
-    val paymentInformationId: String? = null,
-    val messageId: String? = null,
-
-    val purpose: String?,
-    val proprietaryPurpose: String?,
-
-    /**
-     * Currency exchange information for the transaction's amount.
-     */
-    val currencyExchange: CurrencyExchange?,
-
-    /**
-     * Amount as given in the payment initiation.
-     * Can be same or different currency as account currency.
-     */
-    val instructedAmount: CurrencyAmount?,
-
-    /**
-     * Raw amount used for currency exchange, before extra charges.
-     * Can be same or different currency as account currency.
-     */
-    val counterValueAmount: CurrencyAmount?,
-
-    /**
-     * Money that was moved between banks.
-     *
-     * For CH, we use the "TxAmt".
-     * For EPC, this amount is either blank or taken
-     * from the "IBC" proprietary amount.
-     */
-    val interBankSettlementAmount: CurrencyAmount?,
-
-    /**
-     * Unstructured remittance information (=subject line) of the transaction,
-     * or the empty string if missing.
-     */
-    val unstructuredRemittanceInformation: String,
-    val returnInfo: ReturnInfo?
-)
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class ReturnInfo(
-    val originalBankTransactionCode: String?,
-    val originator: PartyIdentification?,
-    val reason: String?,
-    val proprietaryReason: String?,
-    val additionalInfo: String?
-)
-
-data class BatchTransaction(
-    val amount: CurrencyAmount, // Fuels Taler withdrawal amount.
-    val creditDebitIndicator: CreditDebitIndicator,
-    val details: TransactionDetails
-)
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class Batch(
-    val messageId: String?,
-    val paymentInformationId: String?,
-    val batchTransactions: List<BatchTransaction>
-)
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-data class CamtBankAccountEntry(
-    val amount: CurrencyAmount,
-    /**
-     * Is this entry debiting or crediting the account
-     * it is reported for?
-     */
-    val creditDebitIndicator: CreditDebitIndicator,
-
-    /**
-     * Booked, pending, etc.
-     */
-    val status: EntryStatus,
-
-    /**
-     * Code that describes the type of bank transaction
-     * in more detail
-     */
-    val bankTransactionCode: String,
-
-    val valueDate: String?,
-
-    val bookingDate: String?,
-
-    val accountServicerRef: String?,
-
-    val entryRef: String?,
-
-    /**
-     * Currency exchange information for the entry's amount.
-     * Only present if currency exchange happened at the entry level.
-     */
-    val currencyExchange: CurrencyExchange?,
-
-    /**
-     * Value before/after currency exchange before charges have been applied.
-     * Only present if currency exchange happened at the entry level.
-     */
-    val counterValueAmount: CurrencyAmount?,
-
-    /**
-     * Instructed amount.
-     * Only present if currency exchange happens at the entry level.
-     */
-    val instructedAmount: CurrencyAmount?,
-
-    // list of sub-transactions participating in this money movement.
-    val batches: List<Batch>?
-) {
-    /**
-     * This function returns the subject of the unique transaction
-     * accounted in this object.  If the transaction is not unique,
-     * it throws an exception.  NOTE: the caller has the responsibility
-     * of not passing an empty report; those usually should be discarded
-     * and never participate in the application logic.
-     */
-    @JsonIgnore
-    fun getSingletonSubject(): String {
-        // Checks that the given list contains only one element and returns it.
-        fun <T>checkAndGetSingleton(maybeTxs: List<T>?): T {
-            if (maybeTxs == null || maybeTxs.size > 1) throw 
internalServerError(
-                "Only a singleton transaction is " +
-                        "allowed inside ${this.javaClass}."
-            )
-            return maybeTxs[0]
-        }
-        /**
-         * Types breakdown until the last payment information is reached.
-         *
-         * CamtBankAccountEntry contains:
-         * - Batch 0
-         * - Batch 1
-         * - Batch N
-         *
-         * Batch X contains:
-         * - BatchTransaction 0
-         * - BatchTransaction 1
-         * - BatchTransaction N
-         *
-         * BatchTransaction X contains:
-         * - TransactionDetails
-         *
-         * TransactionDetails contains the involved parties
-         * and the payment subject but MAY NOT contain the amount.
-         * In this model, the amount is held in the BatchTransaction
-         * type, that is also -- so far -- required to be a singleton
-         * inside Batch.
-         */
-        checkAndGetSingleton<Batch>(this.batches)
-        val batchTransactions = this.batches?.get(0)?.batchTransactions
-        val tx = checkAndGetSingleton<BatchTransaction>(batchTransactions)
-        val details: TransactionDetails = tx.details
-        return details.unstructuredRemittanceInformation
-    }
-}
-
 class CamtParsingError(msg: String) : Exception(msg)
 
 /**
@@ -1098,7 +833,7 @@ fun processCamtMessage(
             }
             rawEntity.flush()
             newTransactions++
-            newPaymentsLog += "\n- " + 
entry.batches[0].batchTransactions[0].details.unstructuredRemittanceInformation
+            newPaymentsLog += "\n- " + entry.getSingletonSubject()
             // This block tries to acknowledge a former outgoing payment as 
booked.
             if (singletonBatchedTransaction.creditDebitIndicator == 
CreditDebitIndicator.DBIT) {
                 val t0 = singletonBatchedTransaction.details
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
index aecc58eb..52069270 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
@@ -1,6 +1,8 @@
 package tech.libeufin.nexus.server
 
+import CamtBankAccountEntry
 import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.databind.node.ObjectNode
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import io.ktor.http.*
@@ -10,7 +12,6 @@ import org.jetbrains.exposed.sql.and
 import org.jetbrains.exposed.sql.transactions.transaction
 import tech.libeufin.nexus.*
 import tech.libeufin.nexus.bankaccount.getBankAccount
-import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
 import tech.libeufin.util.internalServerError
 import tech.libeufin.util.notFound
 
@@ -35,8 +36,10 @@ fun getIngestedTransactions(params: GetTransactionsParams): 
List<JsonNode> =
         }.sortedBy { it.id.value }.take(params.resultSize.toInt()) // Smallest 
index (= earliest transaction) first
         // Converting the result to the HTTP response type.
         maybeResult.map {
-            val element: ObjectNode = 
jacksonObjectMapper().readTree(it.transactionJson) as ObjectNode
+            val element: ObjectNode = jacksonObjectMapper().createObjectNode()
             element.put("index", it.id.value.toString())
+            val txObj: JsonNode = 
jacksonObjectMapper().readTree(it.transactionJson)
+            element.set<JsonNode>("camtData", txObj)
             return@map element
         }
     }
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
index 1db14ab9..312961c2 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
@@ -19,21 +19,14 @@
 
 package tech.libeufin.nexus.server
 
+import CamtBankAccountEntry
+import CurrencyAmount
+import EntryStatus
 import com.fasterxml.jackson.annotation.JsonSubTypes
 import com.fasterxml.jackson.annotation.JsonTypeInfo
 import com.fasterxml.jackson.annotation.JsonTypeName
 import com.fasterxml.jackson.annotation.JsonValue
-import com.fasterxml.jackson.core.JsonGenerator
-import com.fasterxml.jackson.core.JsonParser
-import com.fasterxml.jackson.databind.DeserializationContext
 import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.SerializerProvider
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize
-import com.fasterxml.jackson.databind.annotation.JsonSerialize
-import com.fasterxml.jackson.databind.deser.std.StdDeserializer
-import com.fasterxml.jackson.databind.ser.std.StdSerializer
-import tech.libeufin.nexus.EntryStatus
-import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
 import tech.libeufin.util.*
 import java.time.Instant
 import java.time.ZoneId
@@ -92,7 +85,7 @@ class EbicsStandardOrderParamsDateJson(
     private val end: String
 ) : EbicsOrderParamsJson() {
     override fun toOrderParams(): EbicsOrderParams {
-        val dateRange: EbicsDateRange? =
+        val dateRange =
             EbicsDateRange(
                 ZonedDateTime.parse(this.start, EbicsDateFormat.fmt),
                 ZonedDateTime.parse(this.end, EbicsDateFormat.fmt)
@@ -419,43 +412,6 @@ data class ImportBankAccount(
     val nexusBankAccountId: String
 )
 
-
-class CurrencyAmountDeserializer(jc: Class<*> = CurrencyAmount::class.java) : 
StdDeserializer<CurrencyAmount>(jc) {
-    override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): 
CurrencyAmount {
-        if (p == null) {
-            throw UnsupportedOperationException();
-        }
-        val s = p.valueAsString
-        val components = s.split(":")
-        // FIXME: error handling!
-        return CurrencyAmount(components[0], components[1])
-    }
-}
-
-class CurrencyAmountSerializer(jc: Class<CurrencyAmount> = 
CurrencyAmount::class.java) : StdSerializer<CurrencyAmount>(jc) {
-    override fun serialize(value: CurrencyAmount?, gen: JsonGenerator?, 
provider: SerializerProvider?) {
-        if (gen == null) {
-            throw UnsupportedOperationException()
-        }
-        if (value == null) {
-            gen.writeNull()
-        } else {
-            gen.writeString("${value.currency}:${value.value}")
-        }
-    }
-}
-
-// FIXME: this type duplicates AmountWithCurrency.
-@JsonDeserialize(using = CurrencyAmountDeserializer::class)
-@JsonSerialize(using = CurrencyAmountSerializer::class)
-data class CurrencyAmount(
-    val currency: String,
-    val value: String
-)
-fun CurrencyAmount.toPlainString(): String {
-    return "${this.currency}:${this.value}"
-}
-
 data class InitiatedPayments(
     val initiatedPayments: MutableList<PaymentStatus> = mutableListOf()
 )
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
index bca2f0a1..542f2d4f 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -52,7 +52,6 @@ import org.slf4j.event.Level
 import tech.libeufin.nexus.*
 import tech.libeufin.nexus.bankaccount.*
 import tech.libeufin.nexus.ebics.*
-import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
 import tech.libeufin.nexus.iso20022.processCamtMessage
 import tech.libeufin.util.*
 import java.net.BindException
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt
index 6cec7e41..22c07e38 100644
--- 
a/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt
+++ 
b/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt
@@ -1,5 +1,14 @@
 package tech.libeufin.nexus.xlibeufinbank
 
+import AgentIdentification
+import Batch
+import BatchTransaction
+import CamtBankAccountEntry
+import CashAccount
+import CreditDebitIndicator
+import CurrencyAmount
+import PartyIdentification
+import TransactionDetails
 import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@@ -234,12 +243,6 @@ class XlibeufinBankConnectionProtocol : 
BankConnectionProtocol {
         accountId: String
     ): List<Exception>? {
         val conn = getBankConnection(bankConnectionId)
-        /**
-         * Note: fetchSpec.level is ignored because Sandbox does not
-         * differentiate between booked and non-booked transactions.
-         * Just logging if the unaware client specified non-REPORT for
-         * the level.  FIXME: docs have to mention this.
-         */
         if (fetchSpec.level == FetchLevel.REPORT || fetchSpec.level == 
FetchLevel.ALL)
             throw badRequest("level '${fetchSpec.level}' on x-libeufin-bank" +
                     "connection (${conn.connectionId}) is not supported:" +
diff --git a/util/src/main/kotlin/CamtJsonMapping.kt 
b/util/src/main/kotlin/CamtJsonMapping.kt
new file mode 100644
index 00000000..06a042a6
--- /dev/null
+++ b/util/src/main/kotlin/CamtJsonMapping.kt
@@ -0,0 +1,334 @@
+import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import tech.libeufin.util.internalServerError
+
+enum class CreditDebitIndicator {
+    DBIT,
+    CRDT
+}
+
+enum class EntryStatus {
+    BOOK, // Booked
+    PDNG, // Pending
+    INFO, // Informational
+}
+
+class CurrencyAmountDeserializer(jc: Class<*> = CurrencyAmount::class.java) : 
StdDeserializer<CurrencyAmount>(jc) {
+    override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): 
CurrencyAmount {
+        if (p == null) {
+            throw UnsupportedOperationException();
+        }
+        val s = p.valueAsString
+        val components = s.split(":")
+        // FIXME: error handling!
+        return CurrencyAmount(components[0], components[1])
+    }
+}
+
+class CurrencyAmountSerializer(jc: Class<CurrencyAmount> = 
CurrencyAmount::class.java) : StdSerializer<CurrencyAmount>(jc) {
+    override fun serialize(value: CurrencyAmount?, gen: JsonGenerator?, 
provider: SerializerProvider?) {
+        if (gen == null) {
+            throw UnsupportedOperationException()
+        }
+        if (value == null) {
+            gen.writeNull()
+        } else {
+            gen.writeString("${value.currency}:${value.value}")
+        }
+    }
+}
+
+// FIXME: this type duplicates AmountWithCurrency.
+@JsonDeserialize(using = CurrencyAmountDeserializer::class)
+@JsonSerialize(using = CurrencyAmountSerializer::class)
+data class CurrencyAmount(
+    val currency: String,
+    val value: String
+)
+
+fun CurrencyAmount.toPlainString(): String {
+    return "${this.currency}:${this.value}"
+}
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class CashAccount(
+    val name: String?,
+    val currency: String?,
+    val iban: String?,
+    val otherId: GenericId?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class GenericId(
+    val id: String,
+    val schemeName: String?,
+    val proprietarySchemeName: String?,
+    val issuer: String?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class PrivateIdentification(
+    val birthDate: String?,
+    val provinceOfBirth: String?,
+    val cityOfBirth: String?,
+    val countryOfBirth: String?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class OrganizationIdentification(
+    val bic: String?,
+    val lei: String?
+)
+
+/**
+ * Identification of a party, which can be a private party
+ * or an organization.
+ *
+ * Mapping of ISO 20022 PartyIdentification135.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class PartyIdentification(
+    val name: String?,
+    val countryOfResidence: String?,
+    val privateId: PrivateIdentification?,
+    val organizationId: OrganizationIdentification?,
+    val postalAddress: PostalAddress?,
+
+    /**
+     * Identification that applies to both private parties and organizations.
+     */
+    val otherId: GenericId?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class PostalAddress(
+    val addressCode: String?,
+    val addressProprietaryId: String?,
+    val addressProprietarySchemeName: String?,
+    val addressProprietaryIssuer: String?,
+    val department: String?,
+    val subDepartment: String?,
+    val streetName: String?,
+    val buildingNumber: String?,
+    val buildingName: String?,
+    val floor: String?,
+    val postBox: String?,
+    val room: String?,
+    val postCode: String?,
+    val townName: String?,
+    val townLocationName: String?,
+    val districtName: String?,
+    val countrySubDivision: String?,
+    val country: String?,
+    val addressLines: List<String>
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class AgentIdentification(
+    val name: String?,
+
+    val bic: String?,
+
+    /**
+     * Legal entity identification.
+     */
+    val lei: String?,
+
+    val clearingSystemMemberId: String?,
+
+    val clearingSystemCode: String?,
+
+    val proprietaryClearingSystemCode: String?,
+
+    val postalAddress: PostalAddress?,
+
+    val otherId: GenericId?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class CurrencyExchange(
+    val sourceCurrency: String,
+    val targetCurrency: String,
+    val unitCurrency: String?,
+    val exchangeRate: String,
+    val contractId: String?,
+    val quotationDate: String?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class Batch(
+    val messageId: String?,
+    val paymentInformationId: String?,
+    val batchTransactions: List<BatchTransaction>
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class TransactionDetails(
+    val debtor: PartyIdentification?,
+    val debtorAccount: CashAccount?,
+    val debtorAgent: AgentIdentification?,
+    val creditor: PartyIdentification?,
+    val creditorAccount: CashAccount?,
+    val creditorAgent: AgentIdentification?,
+    val ultimateCreditor: PartyIdentification?,
+    val ultimateDebtor: PartyIdentification?,
+
+    val endToEndId: String? = null,
+    val paymentInformationId: String? = null,
+    val messageId: String? = null,
+
+    val purpose: String?,
+    val proprietaryPurpose: String?,
+
+    /**
+     * Currency exchange information for the transaction's amount.
+     */
+    val currencyExchange: CurrencyExchange?,
+
+    /**
+     * Amount as given in the payment initiation.
+     * Can be same or different currency as account currency.
+     */
+    val instructedAmount: CurrencyAmount?,
+
+    /**
+     * Raw amount used for currency exchange, before extra charges.
+     * Can be same or different currency as account currency.
+     */
+    val counterValueAmount: CurrencyAmount?,
+
+    /**
+     * Money that was moved between banks.
+     *
+     * For CH, we use the "TxAmt".
+     * For EPC, this amount is either blank or taken
+     * from the "IBC" proprietary amount.
+     */
+    val interBankSettlementAmount: CurrencyAmount?,
+
+    /**
+     * Unstructured remittance information (=subject line) of the transaction,
+     * or the empty string if missing.
+     */
+    val unstructuredRemittanceInformation: String,
+    val returnInfo: ReturnInfo?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class ReturnInfo(
+    val originalBankTransactionCode: String?,
+    val originator: PartyIdentification?,
+    val reason: String?,
+    val proprietaryReason: String?,
+    val additionalInfo: String?
+)
+
+data class BatchTransaction(
+    val amount: CurrencyAmount, // Fuels Taler withdrawal amount.
+    val creditDebitIndicator: CreditDebitIndicator,
+    val details: TransactionDetails
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class CamtBankAccountEntry(
+    val amount: CurrencyAmount,
+    /**
+     * Is this entry debiting or crediting the account
+     * it is reported for?
+     */
+    val creditDebitIndicator: CreditDebitIndicator,
+
+    /**
+     * Booked, pending, etc.
+     */
+    val status: EntryStatus,
+
+    /**
+     * Code that describes the type of bank transaction
+     * in more detail
+     */
+    val bankTransactionCode: String,
+
+    val valueDate: String?,
+
+    val bookingDate: String?,
+
+    val accountServicerRef: String?,
+
+    val entryRef: String?,
+
+    /**
+     * Currency exchange information for the entry's amount.
+     * Only present if currency exchange happened at the entry level.
+     */
+    val currencyExchange: CurrencyExchange?,
+
+    /**
+     * Value before/after currency exchange before charges have been applied.
+     * Only present if currency exchange happened at the entry level.
+     */
+    val counterValueAmount: CurrencyAmount?,
+
+    /**
+     * Instructed amount.
+     * Only present if currency exchange happens at the entry level.
+     */
+    val instructedAmount: CurrencyAmount?,
+
+    // list of sub-transactions participating in this money movement.
+    val batches: List<Batch>?
+) {
+    /**
+     * This function returns the subject of the unique transaction
+     * accounted in this object.  If the transaction is not unique,
+     * it throws an exception.  NOTE: the caller has the responsibility
+     * of not passing an empty report; those usually should be discarded
+     * and never participate in the application logic.
+     */
+    @JsonIgnore
+    fun getSingletonSubject(): String {
+        // Checks that the given list contains only one element and returns it.
+        fun <T>checkAndGetSingleton(maybeTxs: List<T>?): T {
+            if (maybeTxs == null || maybeTxs.size > 1) throw 
internalServerError(
+                "Only a singleton transaction is " +
+                        "allowed inside ${this.javaClass}."
+            )
+            return maybeTxs[0]
+        }
+        /**
+         * Types breakdown until the meaningful payment information is reached.
+         *
+         * CamtBankAccountEntry contains:
+         * - Batch 0
+         * - Batch 1
+         * - Batch N
+         *
+         * Batch X contains:
+         * - BatchTransaction 0
+         * - BatchTransaction 1
+         * - BatchTransaction N
+         *
+         * BatchTransaction X contains:
+         * - TransactionDetails
+         *
+         * TransactionDetails contains the involved parties
+         * and the payment subject but MAY NOT contain the amount.
+         * In this model, the amount is held in the BatchTransaction
+         * type, that is also -- so far -- required to be a singleton
+         * inside Batch.
+         */
+        val batch: Batch = checkAndGetSingleton(this.batches)
+        val batchTransactions = batch.batchTransactions
+        val tx: BatchTransaction = checkAndGetSingleton(batchTransactions)
+        val details: TransactionDetails = tx.details
+        return details.unstructuredRemittanceInformation
+    }
+}
\ No newline at end of file
diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt
index 41e2a9d7..a0bc789a 100644
--- a/util/src/main/kotlin/DB.kt
+++ b/util/src/main/kotlin/DB.kt
@@ -150,7 +150,7 @@ class PostgresListenHandle(val channelName: String) {
         keepConnectionOpen: Boolean = false
         ): Boolean {
         if (timeoutMs == 0L)
-            logger.warn("Database notification checker has timeout == 0," +
+            logger.info("Database notification checker has timeout == 0," +
                     " that waits FOREVER until a notification arrives."
             )
         logger.debug("Waiting Postgres notifications on channel " +
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
index 67a0ccca..c41c6aa2 100644
--- a/util/src/main/kotlin/HTTP.kt
+++ b/util/src/main/kotlin/HTTP.kt
@@ -224,4 +224,8 @@ fun ApplicationCall.maybeLong(uriParamName: String): Long? {
     catch (e: Exception) {
         throw badRequest("Could not convert '$uriParamName' to Long")
     }
-}
\ No newline at end of file
+}
+
+// Join base URL and path ensuring one (and only one) slash in between.
+fun joinUrl(baseUrl: String, path: String): String =
+    baseUrl.dropLastWhile { it == '/' } + '/' + path.dropWhile { it == '/' }
\ No newline at end of file
diff --git a/util/src/main/kotlin/amounts.kt b/util/src/main/kotlin/amounts.kt
index 00eb7b6d..043e9af3 100644
--- a/util/src/main/kotlin/amounts.kt
+++ b/util/src/main/kotlin/amounts.kt
@@ -27,6 +27,9 @@ const val plainAmountRe = "^([0-9]+(\\.[0-9][0-9]?)?)$"
 const val plainAmountReWithSign = "^-?([0-9]+(\\.[0-9][0-9]?)?)$"
 const val amountWithCurrencyRe = "^([A-Z]+):([0-9]+(\\.[0-9][0-9]?)?)$"
 
+// Ensures that the number part of one amount matches the allowed format.
+// Currently, at most two fractional digits are allowed.  It returns true
+// in the matching case, false otherwise.
 fun validatePlainAmount(plainAmount: String, withSign: Boolean = false): 
Boolean {
     if (withSign) return Regex(plainAmountReWithSign).matches(plainAmount)
     return Regex(plainAmountRe).matches(plainAmount)
diff --git a/util/src/main/kotlin/strings.kt b/util/src/main/kotlin/strings.kt
index abfa0184..dce25861 100644
--- a/util/src/main/kotlin/strings.kt
+++ b/util/src/main/kotlin/strings.kt
@@ -195,4 +195,11 @@ fun hasWopidPlaceholder(captchaUrl: String): Boolean {
     if (captchaUrl.contains("{wopid}", ignoreCase = true))
         return true
     return false
-}
\ No newline at end of file
+}
+
+// Tries to extract a valid reserve public key from the raw subject line
+fun extractReservePubFromSubject(rawSubject: String): String? {
+    val re = "\\b[a-z0-9A-Z]{52}\\b".toRegex()
+    val result = re.find(rawSubject.replace("[\n]+".toRegex(), "")) ?: return 
null
+    return result.value.uppercase()
+}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]