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:
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")