[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.