libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit e783f1210223f6d976c347274b5d573a955e1d08
parent a840a8e7b28620ba59a58857800cdf2d01557c61
Author: tanhengyeow <E0032242@u.nus.edu>
Date:   Fri,  3 Jul 2020 12:48:21 +0800

Merge branch 'master' of ssh://git.taler.net/libeufin

Diffstat:
M.idea/dictionaries/dold.xml | 2++
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 2+-
Mnexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt | 336++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 21+++++++++++++--------
Mnexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt | 4++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt | 4++--
Mnexus/src/test/kotlin/Iso20022Test.kt | 2+-
7 files changed, 182 insertions(+), 189 deletions(-)

diff --git a/.idea/dictionaries/dold.xml b/.idea/dictionaries/dold.xml @@ -8,9 +8,11 @@ <w>cronspec</w> <w>dbit</w> <w>ebics</w> + <w>infos</w> <w>libeufin</w> <w>payto</w> <w>pdng</w> + <w>servicer</w> <w>sqlite</w> </words> </dictionary> diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -131,7 +131,7 @@ object NexusBankTransactionsTable : LongIdTable() { /** * Booked / pending / informational. */ - val status = enumerationByName("status", 16, TransactionStatus::class) + val status = enumerationByName("status", 16, EntryStatus::class) /** * Another, later transaction that updates the status of the current transaction. diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -23,8 +23,6 @@ package tech.libeufin.nexus import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonTypeInfo import com.fasterxml.jackson.annotation.JsonValue import org.w3c.dom.Document import tech.libeufin.nexus.server.CurrencyAmount @@ -38,7 +36,7 @@ enum class CreditDebitIndicator { DBIT, CRDT } -enum class TransactionStatus { +enum class EntryStatus { /** * Booked */ @@ -59,11 +57,27 @@ enum class CashManagementResponseType(@get:JsonValue val jsonName: String) { Report("report"), Statement("statement"), Notification("notification") } +@JsonInclude(JsonInclude.Include.NON_NULL) data class CamtReport( - val account: AccountIdentification, + val account: CashAccount, val entries: List<CamtBankAccountEntry> ) +@JsonInclude(JsonInclude.Include.NON_NULL) +data class GenericId( + val id: String, + val schemeName: String?, + val issuer: String? +) + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class CashAccount( + val name: String?, + val currency: String?, + val iban: String?, + val otherId: GenericId? +) + data class CamtParseResult( val reports: List<CamtReport>, val messageId: String, @@ -74,17 +88,58 @@ data class CamtParseResult( val creationDateTime: String ) +enum class PartyType(@get:JsonValue val jsonName: String) { + PRIVATE("private"), ORGANIZATION("organization") +} + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class PartyIdentification( + val name: String?, + val otherId: GenericId? +) + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class AgentIdentification( + val name: String?, + val bic: String?, + 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 TransactionInfo( val batchPaymentInformationId: String?, val batchMessageId: String?, - /** - * Related parties as JSON. - */ - val relatedParties: RelatedParties, - val amountDetails: AmountDetails, - val references: References, + val debtor: PartyIdentification?, + val debtorAccount: CashAccount?, + val debtorAgent: AgentIdentification?, + val creditor: PartyIdentification?, + val creditorAccount: CashAccount?, + val creditorAgent: AgentIdentification?, + + val endToEndId: String? = null, + val paymentInformationId: String? = null, + val messageId: String? = null, + + val amount: CurrencyAmount?, + val creditDebitIndicator: CreditDebitIndicator?, + + val instructedAmount: CurrencyAmount?, + val transactionAmount: CurrencyAmount?, + + val instructedAmountCurrencyExchange: CurrencyExchange?, + val transactionAmountCurrencyExchange: CurrencyExchange?, + /** * Unstructured remittance information (=subject line) of the transaction, * or the empty string if missing. @@ -92,87 +147,37 @@ data class TransactionInfo( val unstructuredRemittanceInformation: String ) -abstract class AccountIdentification : TypedEntity() - @JsonInclude(JsonInclude.Include.NON_NULL) -data class AccountIdentificationIban( - val iban: String -) : AccountIdentification() - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class AccountIdentificationGeneric( - val identification: String, - val issuer: String?, - val code: String?, - val proprietary: String? -) : AccountIdentification() - - data class CamtBankAccountEntry( val entryAmount: CurrencyAmount, /** - * Booked, pending, etc. + * Is this entry debiting or crediting the account + * it is reported for? */ - val status: TransactionStatus, + val creditDebitIndicator: CreditDebitIndicator, /** - * Is this transaction debiting or crediting the account - * it is reported for? + * Booked, pending, etc. */ - val creditDebitIndicator: CreditDebitIndicator, + val status: EntryStatus, + /** * Code that describes the type of bank transaction * in more detail */ + val bankTransactionCode: BankTransactionCode, /** * Transaction details, if this entry contains a single transaction. */ val transactionInfos: List<TransactionInfo>, - val valueDate: DateOrDateTime?, - val bookingDate: DateOrDateTime?, + val valueDate: String?, + val bookingDate: String?, val accountServicerRef: String?, val entryRef: String? ) -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "type" -) -@JsonSubTypes( - JsonSubTypes.Type(value = Agent::class, name = "agent"), - JsonSubTypes.Type(value = Party::class, name = "party"), - JsonSubTypes.Type(value = Date::class, name = "date"), - JsonSubTypes.Type(value = DateTime::class, name = "datetime"), - JsonSubTypes.Type(value = AccountIdentificationIban::class, name = "account-identification-iban"), - JsonSubTypes.Type(value = AccountIdentificationGeneric::class, name = "account-identification-generic") -) -abstract class TypedEntity - -@JsonInclude(JsonInclude.Include.NON_NULL) -class Agent( - val name: String?, - val bic: String -) : TypedEntity() - -@JsonInclude(JsonInclude.Include.NON_NULL) -class Party( - val name: String? -) : TypedEntity() - -abstract class DateOrDateTime : TypedEntity() - -class Date( - val date: String -) : DateOrDateTime() - -class DateTime( - val date: String -) : DateOrDateTime() - - @JsonInclude(JsonInclude.Include.NON_NULL) data class BankTransactionCode( val domain: String?, @@ -182,38 +187,6 @@ data class BankTransactionCode( val proprietaryIssuer: String? ) -data class AmountAndCurrencyExchangeDetails( - val amount: String, - val currency: String -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class AmountDetails( - val instructedAmount: AmountAndCurrencyExchangeDetails?, - val transactionAmount: AmountAndCurrencyExchangeDetails? -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class References( - val endToEndIdentification: String? = null, - val paymentInformationIdentification: String? = null, - val messageIdentification: String? = null -) - -/** - * This structure captures both "TransactionParties6" and "TransactionAgents5" - * of ISO 20022. - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -data class RelatedParties( - val debtor: Party?, - val debtorAccount: AccountIdentification?, - val debtorAgent: Agent?, - val creditor: Party?, - val creditorAccount: AccountIdentification?, - val creditorAgent: Agent? -) - class CamtParsingError(msg: String) : Exception(msg) /** @@ -347,18 +320,18 @@ fun createPain001document(paymentData: NexusPaymentInitiationData): String { return s } -private fun XmlElementDestructor.extractDateOrDateTime(): DateOrDateTime { +private fun XmlElementDestructor.extractDateOrDateTime(): String { return requireOnlyChild { when (it.localName) { - "Dt" -> Date(e.textContent) - "DtTm" -> DateTime(e.textContent) + "Dt" -> e.textContent + "DtTm" -> e.textContent else -> throw Exception("Invalid date / time: ${e.localName}") } } } -private fun XmlElementDestructor.extractAgent(): Agent { - return Agent( +private fun XmlElementDestructor.extractAgent(): AgentIdentification { + return AgentIdentification( name = maybeUniqueChildNamed("FinInstnId") { maybeUniqueChildNamed("Nm") { it.textContent @@ -368,98 +341,111 @@ private fun XmlElementDestructor.extractAgent(): Agent { requireUniqueChildNamed("BIC") { it.textContent } - } + }, + otherId = null ) } -private fun XmlElementDestructor.extractAccount(): AccountIdentification { - return requireUniqueChildNamed("Id") { +private fun XmlElementDestructor.extractAccount(): CashAccount { + var iban: String? = null + var otherId: GenericId? = null + val currency: String? = maybeUniqueChildNamed("Ccy") { it.textContent } + val name: String? = maybeUniqueChildNamed("Nm") { it.textContent } + requireUniqueChildNamed("Id") { requireOnlyChild { when (it.localName) { - "IBAN" -> AccountIdentificationIban(it.textContent) - "Othr" -> AccountIdentificationGeneric( - identification = requireUniqueChildNamed("Id") { it.textContent }, - proprietary = maybeUniqueChildNamed("Prtry") { it.textContent }, - code = maybeUniqueChildNamed("Cd") { it.textContent }, - issuer = maybeUniqueChildNamed("Issr") { it.textContent } - ) + "IBAN" -> { + iban = it.textContent + } + "Othr" -> { + otherId = GenericId( + id = requireUniqueChildNamed("Id") { it.textContent }, + schemeName = maybeUniqueChildNamed("SchmeNm") { it.textContent }, + issuer = maybeUniqueChildNamed("Issr") { it.textContent } + ) + } else -> throw Error("invalid account identification") } } } + return CashAccount(name, currency, iban, otherId) } -private fun XmlElementDestructor.extractParty(): Party { - return Party( - name = maybeUniqueChildNamed("Nm") { it.textContent } - ) -} - -private fun XmlElementDestructor.extractPartiesAndAgents(): RelatedParties { - return RelatedParties( - debtor = maybeUniqueChildNamed("RltdPties") { - maybeUniqueChildNamed("Dbtr") { - extractParty() - } - }, - creditor = maybeUniqueChildNamed("RltdPties") { - maybeUniqueChildNamed("Cdtr") { - extractParty() - } - }, - creditorAccount = maybeUniqueChildNamed("RltdPties") { - maybeUniqueChildNamed("CdtrAcct") { - extractAccount() - } - }, - debtorAccount = maybeUniqueChildNamed("RltdPties") { - maybeUniqueChildNamed("DbtrAcct") { - extractAccount() - } - }, - creditorAgent = maybeUniqueChildNamed("RltdAgts") { - maybeUniqueChildNamed("CdtrAgt") { - extractAgent() - } - }, - debtorAgent = maybeUniqueChildNamed("RltdAgts") { - maybeUniqueChildNamed("DbtrAgt") { - extractAgent() - } - } +private fun XmlElementDestructor.extractParty(): PartyIdentification { + return PartyIdentification( + name = maybeUniqueChildNamed("Nm") { it.textContent }, + otherId = null ) } -private fun XmlElementDestructor.extractAmountAndCurrencyExchangeDetails(): AmountAndCurrencyExchangeDetails { - return AmountAndCurrencyExchangeDetails( +private fun XmlElementDestructor.extractCurrencyAmount(): CurrencyAmount { + return CurrencyAmount( amount = requireUniqueChildNamed("Amt") { it.textContent }, currency = requireUniqueChildNamed("Amt") { it.getAttribute("Ccy") } ) } +private fun XmlElementDestructor.maybeExtractCurrencyAmount(): CurrencyAmount? { + return maybeUniqueChildNamed("Amt") { + CurrencyAmount( + it.textContent, + it.getAttribute("Ccy") + ) + } +} + +private fun XmlElementDestructor.extractMaybeCurrencyExchange(): CurrencyExchange? { + return maybeUniqueChildNamed("CcyXchg") { + CurrencyExchange( + sourceCurrency = requireUniqueChildNamed("SrcCcy") { it.textContent }, + targetCurrency = requireUniqueChildNamed("TgtCcy") { it.textContent }, + contractId = maybeUniqueChildNamed("CtrctId") { it.textContent }, + exchangeRate = requireUniqueChildNamed("XchgRate") { it.textContent }, + quotationDate = maybeUniqueChildNamed("QtnDt") { it.textContent }, + unitCurrency = maybeUniqueChildNamed("UnitCcy") { it.textContent } + ) + } +} + + private fun XmlElementDestructor.extractTransactionInfos(): List<TransactionInfo> { return requireUniqueChildNamed("NtryDtls") { mapEachChildNamed("TxDtls") { TransactionInfo( batchMessageId = null, batchPaymentInformationId = null, - relatedParties = extractPartiesAndAgents(), - amountDetails = maybeUniqueChildNamed("AmtDtls") { - AmountDetails( - instructedAmount = maybeUniqueChildNamed("InstrAmt") { extractAmountAndCurrencyExchangeDetails() }, - transactionAmount = maybeUniqueChildNamed("TxAmt") { extractAmountAndCurrencyExchangeDetails() } - ) - } ?: AmountDetails(null, null), - references = maybeUniqueChildNamed("Refs") { - References( - endToEndIdentification = maybeUniqueChildNamed("EndToEndId") { it.textContent }, - messageIdentification = maybeUniqueChildNamed("MsgId") { it.textContent }, - paymentInformationIdentification = maybeUniqueChildNamed("PmtInfId") { it.textContent } - ) - } ?: References(), + amount = maybeExtractCurrencyAmount(), + creditDebitIndicator = maybeUniqueChildNamed("CdtDbtInd") { it.textContent }?.let { + CreditDebitIndicator.valueOf(it) + }, + instructedAmount = maybeUniqueChildNamed("AmtDtls") { maybeUniqueChildNamed("InstrAmt") { extractCurrencyAmount() } }, + instructedAmountCurrencyExchange = maybeUniqueChildNamed("AmtDtls") { maybeUniqueChildNamed("InstrAmt") { extractMaybeCurrencyExchange() } }, + transactionAmount = maybeUniqueChildNamed("AmtDtls") { maybeUniqueChildNamed("TxAmt") { extractCurrencyAmount() } }, + transactionAmountCurrencyExchange = maybeUniqueChildNamed("AmtDtls") { maybeUniqueChildNamed("TxAmt") { extractMaybeCurrencyExchange() } }, + + endToEndId = maybeUniqueChildNamed("Refs") { + maybeUniqueChildNamed("EndToEndId") { it.textContent } + }, + messageId = maybeUniqueChildNamed("Refs") { + maybeUniqueChildNamed("MsgId") { it.textContent } + }, + paymentInformationId = maybeUniqueChildNamed("Refs") { + maybeUniqueChildNamed("PmtInfId") { it.textContent } + }, unstructuredRemittanceInformation = maybeUniqueChildNamed("RmtInf") { - requireUniqueChildNamed("Ustrd") { it.textContent } - } ?: "" + val chunks = mapEachChildNamed("Ustrd", { it.textContent }) + if (chunks.size == 0) { + null + } else { + chunks.joinToString() + } + } ?: "", + creditorAgent = maybeUniqueChildNamed("CdtrAgt") { extractAgent() }, + debtorAgent = maybeUniqueChildNamed("DbtrAgt") { extractAgent() }, + debtorAccount = maybeUniqueChildNamed("DbtrAgt") { extractAccount() }, + creditorAccount = maybeUniqueChildNamed("CdtrAgt") { extractAccount() }, + debtor = maybeUniqueChildNamed("Dbtr") { extractParty() }, + creditor = maybeUniqueChildNamed("Cdtr") { extractParty() } ) } } @@ -471,14 +457,14 @@ private fun XmlElementDestructor.extractInnerTransactions(): CamtReport { val amount = requireUniqueChildNamed("Amt") { it.textContent } val currency = requireUniqueChildNamed("Amt") { it.getAttribute("Ccy") } val status = requireUniqueChildNamed("Sts") { it.textContent }.let { - TransactionStatus.valueOf(it) + EntryStatus.valueOf(it) } val creditDebitIndicator = requireUniqueChildNamed("CdtDbtInd") { it.textContent }.let { CreditDebitIndicator.valueOf(it) } val btc = requireUniqueChildNamed("BkTxCd") { BankTransactionCode( - domain = maybeUniqueChildNamed("Domn") { maybeUniqueChildNamed("Cd") { it.textContent} }, + domain = maybeUniqueChildNamed("Domn") { maybeUniqueChildNamed("Cd") { it.textContent } }, family = maybeUniqueChildNamed("Domn") { maybeUniqueChildNamed("Fmly") { maybeUniqueChildNamed("Cd") { it.textContent } @@ -513,7 +499,7 @@ private fun XmlElementDestructor.extractInnerTransactions(): CamtReport { entryRef = entryRef ) } - return CamtReport(account, entries); + return CamtReport(account, entries) } /** diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt @@ -136,8 +136,12 @@ fun <T : Entity<Long>> SizedIterable<T>.orderTaler(delta: Int): List<T> { /** * Build an IBAN payto URI. */ -fun buildIbanPaytoUri(iban: String, bic: String, name: String): String { - return "payto://iban/$bic/$iban?receiver-name=$name" +fun buildIbanPaytoUri(iban: String, bic: String?, name: String): String { + if (bic != null) { + return "payto://iban/$bic/$iban?receiver-name=$name" + } else { + return "payto://iban/$iban?receiver-name=$name" + } } /** Builds the comparison operator for history entries based on the sign of 'delta' */ @@ -350,23 +354,24 @@ private suspend fun talerAddIncoming(call: ApplicationCall, httpClient: HttpClie private fun ingestIncoming(payment: NexusBankTransactionEntity, txDtls: TransactionInfo) { val subject = txDtls.unstructuredRemittanceInformation - val debtorName = txDtls.relatedParties.debtor?.name + val debtorName = txDtls.debtor?.name if (debtorName == null) { logger.warn("empty debtor name") return } - val debtorAcct = txDtls.relatedParties.debtorAccount + val debtorAcct = txDtls.debtorAccount if (debtorAcct == null) { // FIXME: Report payment, we can't even send it back logger.warn("empty debitor account") return } - if (debtorAcct !is AccountIdentificationIban) { + val debtorIban = debtorAcct.iban + if (debtorIban == null) { // FIXME: Report payment, we can't even send it back logger.warn("non-iban debitor account") return } - val debtorAgent = txDtls.relatedParties.debtorAgent + val debtorAgent = txDtls.debtorAgent if (debtorAgent == null) { // FIXME: Report payment, we can't even send it back logger.warn("missing debitor agent") @@ -387,7 +392,7 @@ private fun ingestIncoming(payment: NexusBankTransactionEntity, txDtls: Transact this.payment = payment reservePublicKey = reservePub timestampMs = System.currentTimeMillis() - incomingPaytoUri = buildIbanPaytoUri(debtorAcct.iban, debtorAgent.bic, debtorName) + incomingPaytoUri = buildIbanPaytoUri(debtorIban, debtorAgent.bic, debtorName) } return } @@ -409,7 +414,7 @@ fun ingestTalerTransactions() { /** Those with exchange bank account involved */ NexusBankTransactionsTable.bankAccount eq subscriberAccount.id.value and /** Those that are booked */ - (NexusBankTransactionsTable.status eq TransactionStatus.BOOK) and + (NexusBankTransactionsTable.status eq EntryStatus.BOOK) and /** Those that came later than the latest processed payment */ (NexusBankTransactionsTable.id.greater(lastId)) }.orderBy(Pair(NexusBankTransactionsTable.id, SortOrder.ASC)).forEach { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt @@ -163,8 +163,8 @@ fun processCamtMessage( rawEntity.flush() if (tx.creditDebitIndicator == CreditDebitIndicator.DBIT) { val t0 = tx.transactionInfos.getOrNull(0) - val msgId = t0?.references?.messageIdentification - val pmtInfId = t0?.references?.paymentInformationIdentification + val msgId = t0?.messageId + val pmtInfId = t0?.paymentInformationId if (t0 != null && msgId != null && pmtInfId != null) { val paymentInitiation = PaymentInitiationEntity.find { (PaymentInitiationsTable.messageId eq msgId) and diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt @@ -26,7 +26,7 @@ import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.databind.JsonNode import tech.libeufin.nexus.CamtBankAccountEntry import tech.libeufin.nexus.CreditDebitIndicator -import tech.libeufin.nexus.TransactionStatus +import tech.libeufin.nexus.EntryStatus import tech.libeufin.util.* import java.time.Instant import java.time.ZoneId @@ -359,5 +359,5 @@ data class AccountEntryItemJson( val accountServicerRef: String?, val creditDebitIndicator: CreditDebitIndicator, val entryAmount: CurrencyAmount, - val status: TransactionStatus + val status: EntryStatus ) diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt @@ -25,7 +25,7 @@ class Iso20022Test { assertEquals(r.reports.size, 1) assertEquals(r.reports[0].entries[0].entryAmount.amount, "100.00") assertEquals(r.reports[0].entries[0].entryAmount.currency, "EUR") - assertEquals(r.reports[0].entries[0].status, TransactionStatus.BOOK) + assertEquals(r.reports[0].entries[0].status, EntryStatus.BOOK) assertEquals(r.reports[0].entries[0].entryRef, null) assertEquals(r.reports[0].entries[0].accountServicerRef, "Bankreferenz") assertEquals(r.reports[0].entries[0].bankTransactionCode.domain, "PMNT")