diff options
author | Antoine A <> | 2024-04-23 15:39:42 +0900 |
---|---|---|
committer | Antoine A <> | 2024-04-23 15:39:42 +0900 |
commit | 0c7b0d7ce73c43eff03302f6c986689c8091fb1f (patch) | |
tree | 06814a20352c78bed5d2cb9f981037903b49d58a /nexus/src/main/kotlin/tech | |
parent | da656c2d89d09e3829b7884d5dc5f976c78bc088 (diff) | |
download | libeufin-0c7b0d7ce73c43eff03302f6c986689c8091fb1f.tar.gz libeufin-0c7b0d7ce73c43eff03302f6c986689c8091fb1f.tar.bz2 libeufin-0c7b0d7ce73c43eff03302f6c986689c8091fb1f.zip |
Use better unique bank provided ID for incoming transactions and parse return's reason
Diffstat (limited to 'nexus/src/main/kotlin/tech')
3 files changed, 355 insertions, 161 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt index 978c7d2d..96710648 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt @@ -158,7 +158,7 @@ private suspend fun ingestDocument( when (whichDocument) { SupportedDocument.CAMT_053, SupportedDocument.CAMT_054 -> { try { - parseTx(xml, cfg.currency).forEach { + parseTx(xml, cfg.currency, cfg.dialect).forEach { if (cfg.fetch.ignoreBefore != null && it.executionTime < cfg.fetch.ignoreBefore) { logger.debug("IGNORE $it") } else { @@ -307,8 +307,6 @@ enum class EbicsDocument { acknowledgement, /// Payment status - CustomerPaymentStatusReport pain.002 status, - /// Account intraday reports - BankToCustomerAccountReport camt.052 - // report, TODO add support /// Debit & credit notifications - BankToCustomerDebitCreditNotification camt.054 notification, /// Account statements - BankToCustomerStatement camt.053 @@ -318,32 +316,22 @@ enum class EbicsDocument { fun shortDescription(): String = when (this) { acknowledgement -> "EBICS acknowledgement" status -> "Payment status" - //Document.report -> "Account intraday reports" - notification -> "Debit & credit notifications" statement -> "Account statements" + notification -> "Debit & credit notifications" } fun fullDescription(): String = when (this) { acknowledgement -> "EBICS acknowledgement - CustomerAcknowledgement HAC pain.002" status -> "Payment status - CustomerPaymentStatusReport pain.002" - //report -> "Account intraday reports - BankToCustomerAccountReport camt.052" - notification -> "Debit & credit notifications - BankToCustomerDebitCreditNotification camt.054" statement -> "Account statements - BankToCustomerStatement camt.053" + notification -> "Debit & credit notifications - BankToCustomerDebitCreditNotification camt.054" } fun doc(): SupportedDocument = when (this) { acknowledgement -> SupportedDocument.PAIN_002_LOGS status -> SupportedDocument.PAIN_002 - //Document.report -> SupportedDocument.CAMT_052 - notification -> SupportedDocument.CAMT_054 statement -> SupportedDocument.CAMT_053 - } - - companion object { - fun defaults(dialect: Dialect) = when (dialect) { - Dialect.postfinance -> listOf(acknowledgement, status, notification) - Dialect.gls -> listOf(acknowledgement, status, statement) - } + notification -> SupportedDocument.CAMT_054 } } @@ -393,7 +381,7 @@ class EbicsFetch: CliktCommand("Fetches EBICS files") { null, FileLogger(ebicsLog) ) - val docs = if (documents.isEmpty()) EbicsDocument.defaults(cfg.dialect) else documents.toList() + val docs = if (documents.isEmpty()) EbicsDocument.entries else documents.toList() if (transient) { logger.info("Transient mode: fetching once and returning.") val pinnedStartVal = pinnedStart diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt index e5a665ac..c907b9aa 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -89,7 +89,7 @@ fun createPain001( el("PmtInf") { el("PmtInfId", "NOTPROVIDED") el("PmtMtd", "TRF") - el("BtchBookg", "true") + el("BtchBookg", "false") el("NbOfTxs", "1") el("CtrlSum", amountWithoutCurrency) el("PmtTpInf/SvcLvl/Cd", @@ -312,116 +312,218 @@ private fun XmlDestructor.payto(prefix: String): String? { } } +private class TxErr(val msg: String): Exception(msg) + +private enum class Kind { + CRDT, + DBIT +} + /** Parse camt.054 or camt.053 file */ fun parseTx( notifXml: InputStream, - acceptedCurrency: String + acceptedCurrency: String, + dialect: Dialect ): List<TxNotification> { /* In ISO 20022 specifications, most fields are optional and the same information can be written several times in different places. For libeufin, we're only interested in a subset of the available values that can be found in both camt.053 and camt.054. As there are many similarities between these files, we use the same - function to share as much code as possible. This function should not fail on - legitimate files and should simply warn when available informations are insufficient. + function to share code. This function should not fail on legitimate files and should + simply warn when available informations are insufficient. */ /** Assert that transaction status is BOOK */ - fun XmlDestructor.assertBooked() { + fun XmlDestructor.assertBooked(ref: String?) { one("Sts") { val status = opt("Cd")?.text() ?: text() require(status == "BOOK") { - "Found non booked transaction, stop parsing: expected BOOK got $status" + "Found non booked entry $ref, stop parsing: expected BOOK got $status" } } } - /** Parse information commonly founded a the top of the XML tree */ - fun XmlDestructor.parseHead(): TxHead = TxHead( - reversal = opt("RvslInd")?.bool() ?: false, - entryInfo = opt("AddtlNtryInf")?.text(), - date = one("BookgDt").one("Dt").date().atStartOfDay().toInstant(ZoneOffset.UTC), - entryRef = opt("AcctSvcrRef")?.text() - ) - /** Parse transaction code */ - fun XmlDestructor.parseCode(head: TxHead) { - opt("BkTxCd") { - opt("Domn") { - // TODO automate enum generation for all those code - val domainCode = one("Cd") - one("Fmly") { - val familyCode = one("Cd") - val subFamilyCode = one("SubFmlyCd").text() - if (subFamilyCode == "RRTN" || subFamilyCode == "RPCR") { - head.reversal = true - } - } + + fun XmlDestructor.bookDate() = + one("BookgDt").one("Dt").date().atStartOfDay().toInstant(ZoneOffset.UTC) + + /** Check if transaction code is reversal */ + fun XmlDestructor.isReversalCode(): Boolean { + return one("BkTxCd").one("Domn") { + // TODO automate enum generation for all those code + val domainCode = one("Cd").text() + one("Fmly") { + val familyCode = one("Cd").text() + val subFamilyCode = one("SubFmlyCd").text() + + subFamilyCode == "RRTN" || subFamilyCode == "RPCR" } } } - /** Parse information commonly founded a the bottom or the top of the XML tree */ - fun XmlDestructor.parseMid(): TxMid = TxMid( - kind = one("CdtDbtInd").text(), - amount = one("Amt") { - val currency = attr("Ccy") - /** FIXME: test by sending non-CHF to PoFi and see which currency gets here. */ - if (currency != acceptedCurrency) throw Exception("Currency $currency not supported") - TalerAmount("$currency:${text()}") - } - ) - /** Parse information commonly founded a the bottom of the XML tree */ - fun XmlDestructor.parseBtm(): TxBtm = TxBtm( - msgId = opt("Refs")?.opt("MsgId")?.text(), - ref = opt("Refs")?.opt("AcctSvcrRef")?.text(), - subject = opt("RmtInf")?.map("Ustrd") { text() }?.joinToString(""), - // TODO RltdAgts can have more info on debtor and creditor - debtorPayto = opt("RltdPties") { payto("Dbtr") }, - creditorPayto = opt("RltdPties") { payto("Cdtr") }, - ) - /** Parse camt.054 entry */ - fun XmlDestructor.parseNotif(): List<RawTx> { - assertBooked() - val head = parseHead() - parseCode(head) - return one("NtryDtls").map("TxDtls") { - val mid = parseMid() - val btm = parseBtm() - RawTx(head, mid, btm) - } - } - /** Parse camt.053 entry */ - fun XmlDestructor.parseStatement(): RawTx { - assertBooked() - val head = parseHead() - val mid = parseMid() - return one("NtryDtls").one("TxDtls") { - parseCode(head) - val btm = parseBtm() - RawTx(head, mid, btm) + + val txsInfo = mutableListOf<TxInfo>() + + XmlDestructor.fromStream(notifXml, "Document") { when (dialect) { + Dialect.gls -> { + opt("BkToCstmrStmt")?.each("Stmt") { // Camt.053 + opt("Acct") { + // Sanity check on currency and IBAN ? + } + each("Ntry") { + val entryRef = opt("AcctSvcrRef")?.text() + assertBooked(entryRef) + val bookDate = bookDate() + val kind = one("CdtDbtInd").enum<Kind>() + val amount = one("Amt") { + val currency = attr("Ccy") + /** FIXME: test by sending non-CHF to PoFi and see which currency gets here. */ + if (currency != acceptedCurrency) throw Exception("Currency $currency not supported") + TalerAmount("$currency:${text()}") + } + one("NtryDtls").one("TxDtls") { + val txRef = opt("Refs")?.opt("AcctSvcrRef")?.text() + val reversal = isReversalCode() + val nexusId = opt("Refs")?.opt("MsgId")?.text() // TODO and end-to-end ID + if (reversal) { + if (kind == Kind.CRDT) { + val reason = one("RtrInf") { + val code = one("Rsn").one("Cd").enum<ExternalReturnReasonCode>() + val info = opt("AddtlInf")?.text() + buildString { + append("${code.isoCode} '${code.description}'") + if (info != null) { + append(" - '$info'") + } + } + } + txsInfo.add(TxInfo.CreditReversal( + ref = nexusId ?: txRef ?: entryRef, + bookDate = bookDate, + nexusId = nexusId, + reason = reason + )) + } + } else { + val subject = opt("RmtInf")?.map("Ustrd") { text() }?.joinToString("") + when (kind) { + Kind.CRDT -> { + val bankId = one("Refs").opt("TxId")?.text() + val debtorPayto = opt("RltdPties") { payto("Dbtr") } + txsInfo.add(TxInfo.Credit( + ref = bankId ?: txRef ?: entryRef, + bookDate = bookDate, + bankId = bankId, + amount = amount, + subject = subject, + debtorPayto = debtorPayto + )) + } + Kind.DBIT -> { + val creditorPayto = opt("RltdPties") { payto("Cdtr") } + txsInfo.add(TxInfo.Debit( + ref = nexusId ?: txRef ?: entryRef, + bookDate = bookDate, + nexusId = nexusId, + amount = amount, + subject = subject, + creditorPayto = creditorPayto + )) + } + } + } + } + } + } } - } - val raws = mutableListOf<RawTx>() - XmlDestructor.fromStream(notifXml, "Document") { - opt("BkToCstmrDbtCdtNtfctn") { // Camt.054 - each("Ntfctn") { + Dialect.postfinance -> { + opt("BkToCstmrStmt")?.each("Stmt") { // Camt.053 opt("Acct") { // Sanity check on currency and IBAN ? } each("Ntry") { - raws.addAll(parseNotif()) + val entryRef = opt("AcctSvcrRef")?.text() + assertBooked(entryRef) + val bookDate = bookDate() + if (isReversalCode()) { + one("NtryDtls").one("TxDtls") { + val kind = one("CdtDbtInd").enum<Kind>() + if (kind == Kind.CRDT) { + val txRef = opt("Refs")?.opt("AcctSvcrRef")?.text() + val nexusId = opt("Refs")?.opt("MsgId")?.text() // TODO and end-to-end ID + val reason = one("RtrInf") { + val code = one("Rsn").one("Cd").enum<ExternalReturnReasonCode>() + val info = opt("AddtlInf")?.text() + buildString { + append("${code.isoCode} '${code.description}'") + if (info != null) { + append(" - '$info'") + } + } + } + txsInfo.add(TxInfo.CreditReversal( + ref = nexusId ?: txRef ?: entryRef, + bookDate = bookDate, + nexusId = nexusId, + reason = reason + )) + } + } + } } } - } ?: opt("BkToCstmrStmt") { // Camt.053 - each("Stmt") { + opt("BkToCstmrDbtCdtNtfctn")?.each("Ntfctn") { // Camt.054 opt("Acct") { // Sanity check on currency and IBAN ? } each("Ntry") { - raws.add(parseStatement()) + val entryRef = opt("AcctSvcrRef")?.text() + assertBooked(entryRef) + val bookDate = bookDate() + if (!isReversalCode()) { + one("NtryDtls").each("TxDtls") { + val kind = one("CdtDbtInd").enum<Kind>() + val amount = one("Amt") { + val currency = attr("Ccy") + /** FIXME: test by sending non-CHF to PoFi and see which currency gets here. */ + if (currency != acceptedCurrency) throw Exception("Currency $currency not supported") + TalerAmount("$currency:${text()}") + } + val txRef = one("Refs").opt("AcctSvcrRef")?.text() + val subject = opt("RmtInf")?.map("Ustrd") { text() }?.joinToString("") + when (kind) { + Kind.CRDT -> { + val bankId = one("Refs").opt("UETR")?.text() + val debtorPayto = opt("RltdPties") { payto("Dbtr") } + txsInfo.add(TxInfo.Credit( + ref = bankId ?: txRef ?: entryRef, + bookDate = bookDate, + bankId = bankId, + amount = amount, + subject = subject, + debtorPayto = debtorPayto + )) + } + Kind.DBIT -> { + val nexusId = opt("Refs")?.opt("MsgId")?.text() // TODO and end-to-end ID + val creditorPayto = opt("RltdPties") { payto("Cdtr") } + txsInfo.add(TxInfo.Debit( + ref = nexusId ?: txRef ?: entryRef, + bookDate = bookDate, + nexusId = nexusId, + amount = amount, + subject = subject, + creditorPayto = creditorPayto + )) + } + } + } + } } } - } ?: throw Exception("Missing BkToCstmrDbtCdtNtfctn or BkToCstmrStmt") - } - return raws.mapNotNull { it -> + } + }} + + return txsInfo.mapNotNull { it -> try { parseTxLogic(it) } catch (e: TxErr) { @@ -432,78 +534,74 @@ fun parseTx( } } +private sealed interface TxInfo { + // Bank provider ref for debugging + val ref: String? + // When was this transaction booked + val bookDate: Instant + data class CreditReversal( + override val ref: String?, + override val bookDate: Instant, + // Unique ID generated by libeufin-nexus + val nexusId: String?, + val reason: String? + ): TxInfo + data class Credit( + override val ref: String?, + override val bookDate: Instant, + // Unique ID generated by payment provider + val bankId: String?, + val amount: TalerAmount, + val subject: String?, + val debtorPayto: String? + ): TxInfo + data class Debit( + override val ref: String?, + override val bookDate: Instant, + // Unique ID generated by libeufin-nexus + val nexusId: String?, + val amount: TalerAmount, + val subject: String?, + val creditorPayto: String? + ): TxInfo +} -private data class TxHead( - var reversal: Boolean, - val entryInfo: String?, - val date: Instant, - val entryRef: String? -) - -private data class TxMid( - val kind: String, - val amount: TalerAmount, -) - -private data class TxBtm( - val msgId: String?, - val ref: String?, - val subject: String?, - val debtorPayto: String?, - val creditorPayto: String? -) - -private data class RawTx( - val head: TxHead, - val mid: TxMid, - val btm: TxBtm -) - -private class TxErr(val msg: String): Exception(msg) - -private fun parseTxLogic(raw: RawTx): TxNotification { - val (reversal, entryInfo, date, entryRef) = raw.head - val (kind, amount) = raw.mid - val (msgId, ref, subject, debtorPayto, creditorPayto) = raw.btm - val dbgRef = ref ?: entryRef - if (reversal) { - // TODO parse reason code if present - require("CRDT" == kind) // TODO handle DBIT reversal - if (msgId == null) - throw TxErr("missing msg ID for Credit reversal $dbgRef") - return TxNotification.Reversal( - msgId = msgId, - reason = entryInfo, - executionTime = date - ) - } - return when (kind) { - "CRDT" -> { - if (dbgRef == null) - throw TxErr("missing ref for Credit $dbgRef") - if (subject == null) - throw TxErr("missing subject for Credit $dbgRef") - if (debtorPayto == null) - throw TxErr("missing debtor info for Credit $dbgRef") +private fun parseTxLogic(info: TxInfo): TxNotification { + return when (info) { + is TxInfo.CreditReversal -> { + if (info.nexusId == null) + throw TxErr("missing nexus ID for Credit reversal ${info.ref}") + TxNotification.Reversal( + msgId = info.nexusId, + reason = info.reason, + executionTime = info.bookDate + ) + } + is TxInfo.Credit -> { + if (info.bankId == null) + throw TxErr("missing bank ID for Credit ${info.ref}") + if (info.subject == null) + throw TxErr("missing subject for Credit ${info.ref}") + if (info.debtorPayto == null) + throw TxErr("missing debtor info for Credit ${info.ref}") IncomingPayment( - amount = amount, - bankId = dbgRef, - debitPaytoUri = debtorPayto, - executionTime = date, - wireTransferSubject = subject + amount = info.amount, + bankId = info.bankId, + debitPaytoUri = info.debtorPayto, + executionTime = info.bookDate, + wireTransferSubject = info.subject ) } - "DBIT" -> { - if (msgId == null) - throw TxErr("missing msg ID for Debit $dbgRef") + is TxInfo.Debit -> { + if (info.nexusId == null) + throw TxErr("missing nexus ID for Debit ${info.ref}") OutgoingPayment( - amount = amount, - messageId = msgId, - executionTime = date, - creditPaytoUri = creditorPayto, - wireTransferSubject = subject + amount = info.amount, + messageId = info.nexusId, + executionTime = info.bookDate, + creditPaytoUri = info.creditorPayto, + wireTransferSubject = info.subject ) } - else -> throw Exception("Unknown transaction notification kind '$kind'") } }
\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt index 624a6b0e..2d21b234 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt @@ -327,4 +327,112 @@ enum class ExternalPaymentTransactionStatusCode(val isoCode: String, val descrip PRES("Presented", "Request for Payment has been presented to the Debtor."), RCVD("Received", "Payment instruction has been received."), RJCT("Rejected", "Payment instruction has been rejected."), -}
\ No newline at end of file +} + +enum class ExternalReturnReasonCode(val isoCode: String, val description: String) { + AC01("IncorrectAccountNumber", "Format of the account number specified is not correct"), + AC02("InvalidDebtorAccountNumber", "Debtor account number invalid or missing."), + AC03("InvalidCreditorAccountNumber", "Wrong IBAN in SCT"), + AC04("ClosedAccountNumber", "Account number specified has been closed on the bank of account's books"), + AC06("BlockedAccount", "Account specified is blocked, prohibiting posting of transactions against it."), + AC07("ClosedCreditorAccountNumber", "Creditor account number closed."), + AC13("InvalidDebtorAccountType", "Debtor account type is missing or invalid"), + AC14("InvalidAgent", "An agent in the payment chain is invalid."), + AC15("AccountDetailsChanged", "Account details have changed."), + AC16("AccountInSequestration", "Account is in sequestration."), + AC17("AccountInLiquidation", "Account is in liquidation."), + AG01("TransactionForbidden", "Transaction forbidden on this type of account (formerly NoAgreement)"), + AG02("InvalidBankOperationCode", "Bank Operation code specified in the message is not valid for receiver"), + AG07("UnsuccesfulDirectDebit", "Debtor account cannot be debited for a generic reason."), + AGNT("IncorrectAgent", "Agent in the payment workflow is incorrect."), + AM01("ZeroAmount", "Specified message amount is equal to zero"), + AM02("NotAllowedAmount", "Specific transaction/message amount is greater than allowed maximum"), + AM03("NotAllowedCurrency", "Specified message amount is an non processable currency outside of existing agreement"), + AM04("InsufficientFunds", "Amount of funds available to cover specified message amount is insufficient."), + AM05("Duplication", "Duplication"), + AM06("TooLowAmount", "Specified transaction amount is less than agreed minimum."), + AM07("BlockedAmount", "Amount specified in message has been blocked by regulatory authorities."), + AM09("WrongAmount", "Amount received is not the amount agreed or expected"), + AM10("InvalidControlSum", "Sum of instructed amounts does not equal the control sum."), + ARDT("AlreadyReturnedTransaction", "Already returned original SCT"), + BE01("InconsistenWithEndCustomer", "Identification of end customer is not consistent with associated account number, organisation ID or private ID."), + BE04("MissingCreditorAddress", "Specification of creditor's address, which is required for payment, is missing/not correct (formerly IncorrectCreditorAddress)."), + BE05("UnrecognisedInitiatingParty", "Party who initiated the message is not recognised by the end customer"), + BE06("UnknownEndCustomer", "End customer specified is not known at associated Sort/National Bank Code or does no longer exist in the books"), + BE07("MissingDebtorAddress", "Specification of debtor's address, which is required for payment, is missing/not correct."), + BE08("BankError", "Returned as a result of a bank error."), + BE10("InvalidDebtorCountry", "Debtor country code is missing or invalid."), + BE11("InvalidCreditorCountry", "Creditor country code is missing or invalid."), + BE16("InvalidDebtorIdentificationCode", "Debtor or Ultimate Debtor identification code missing or invalid."), + BE17("InvalidCreditorIdentificationCode", "Creditor or Ultimate Creditor identification code missing or invalid."), + CN01("AuthorisationCancelled", "Authorisation is cancelled."), + CNOR("CreditorBankIsNotRegistered", "Creditor bank is not registered under this BIC in the CSM"), + CNPC("CashNotPickedUp", "Cash not picked up by Creditor or cash could not be delivered to Creditor"), + CURR("IncorrectCurrency", "Currency of the payment is incorrect"), + CUST("RequestedByCustomer", "Cancellation requested by the Debtor"), + DC04("NoCustomerCreditTransferReceived", "Return of Covering Settlement due to the underlying Credit Transfer details not being received."), + DNOR("DebtorBankIsNotRegistered", "Debtor bank is not registered under this BIC in the CSM"), + DS28("ReturnForTechnicalReason", "Return following technical problems resulting in erroneous transaction."), + DT01("InvalidDate", "Invalid date (eg, wrong settlement date)"), + DT02("ChequeExpired", "Cheque has been issued but not deposited and is considered expired."), + DT04("FutureDateNotSupported", "Future date not supported."), + DUPL("DuplicatePayment", "Payment is a duplicate of another payment."), + ED01("CorrespondentBankNotPossible", "Correspondent bank not possible."), + ED03("BalanceInfoRequest", "Balance of payments complementary info is requested"), + ED05("SettlementFailed", "Settlement of the transaction has failed."), + EMVL("EMVLiabilityShift", "The card payment is fraudulent and was not processed with EMV technology for an EMV card."), + ERIN("ERIOptionNotSupported", "The Extended Remittance Information (ERI) option is not supported."), + FF03("InvalidPaymentTypeInformation", "Payment Type Information is missing or invalid."), + FF04("InvalidServiceLevelCode", "Service Level code is missing or invalid."), + FF05("InvalidLocalInstrumentCode", "Local Instrument code is missing or invalid"), + FF06("InvalidCategoryPurposeCode", "Category Purpose code is missing or invalid."), + FF07("InvalidPurpose", "Purpose is missing or invalid."), + FOCR("FollowingCancellationRequest", "Return following a cancellation request"), + FR01("Fraud", "Returned as a result of fraud."), + FRTR("FinalResponseMandateCancelled", "Final response/tracking is recalled as mandate is cancelled."), + G004("CreditPendingFunds", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account is pending, status Originator is waiting for funds provided via a cover. Update will follow from the Status Originator."), + MD01("NoMandate", "No Mandate"), + MD02("MissingMandatoryInformationInMandate", "Mandate related information data required by the scheme is missing."), + MD05("CollectionNotDue", "Creditor or creditor's agent should not have collected the direct debit."), + MD06("RefundRequestByEndCustomer", "Return of funds requested by end customer"), + MD07("EndCustomerDeceased", "End customer is deceased."), + MS02("NotSpecifiedReasonCustomerGenerated", "Reason has not been specified by end customer"), + MS03("NotSpecifiedReasonAgentGenerated", "Reason has not been specified by agent."), + NARR("Narrative", "Reason is provided as narrative information in the additional reason information."), + NOAS("NoAnswerFromCustomer", "No response from Beneficiary"), + NOCM("NotCompliant", "Customer account is not compliant with regulatory requirements, for example FICA (in South Africa) or any other regulatory requirements which render an account inactive for certain processing."), + NOOR("NoOriginalTransactionReceived", "Original SCT never received"), + PINL("PINLiabilityShift", "The card payment is fraudulent (lost and stolen fraud) and was processed as EMV transaction without PIN verification."), + RC01("BankIdentifierIncorrect", "Bank Identifier code specified in the message has an incorrect format (formerly IncorrectFormatForRoutingCode)."), + RC03("InvalidDebtorBankIdentifier", "Debtor bank identifier is invalid or missing."), + RC04("InvalidCreditorBankIdentifier", "Creditor bank identifier is invalid or missing."), + RC07("InvalidCreditorBICIdentifier", "Incorrrect BIC of the beneficiary Bank in the SCTR"), + RC08("InvalidClearingSystemMemberIdentifier", "ClearingSystemMemberidentifier is invalid or missing."), + RC11("InvalidIntermediaryAgent", "Intermediary Agent is invalid or missing."), + RF01("NotUniqueTransactionReference", "Transaction reference is not unique within the message."), + RR01("MissingDebtorAccountOrIdentification", "Specification of the debtor’s account or unique identification needed for reasons of regulatory requirements is insufficient or missing"), + RR02("MissingDebtorNameOrAddress", "Specification of the debtor’s name and/or address needed for regulatory requirements is insufficient or missing."), + RR03("MissingCreditorNameOrAddress", "Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing."), + RR04("RegulatoryReason", "Regulatory Reason"), + RR05("RegulatoryInformationInvalid", "Regulatory or Central Bank Reporting information missing, incomplete or invalid."), + RR06("TaxInformationInvalid", "Tax information missing, incomplete or invalid."), + RR07("RemittanceInformationInvalid", "Remittance information structure does not comply with rules for payment type."), + RR08("RemittanceInformationTruncated", "Remittance information truncated to comply with rules for payment type."), + RR09("InvalidStructuredCreditorReference", "Structured creditor reference invalid or missing."), + RR11("InvalidDebtorAgentServiceIdentification", "Invalid or missing identification of a bank proprietary service."), + RR12("InvalidPartyIdentification", "Invalid or missing identification required within a particular country or payment type."), + RUTA("ReturnUponUnableToApply", "Return following investigation request and no remediation possible."), + SL01("SpecificServiceOfferedByDebtorAgent", "Due to specific service offered by the Debtor Agent"), + SL02("SpecificServiceOfferedByCreditorAgent", "Due to specific service offered by the Creditor Agent"), + SL11("CreditorNotOnWhitelistOfDebtor", "Whitelisting service offered by the Debtor Agent; Debtor has not included the Creditor on its “Whitelist” (yet). In the Whitelist the Debtor may list all allowed Creditors to debit Debtor bank account."), + SL12("CreditorOnBlacklistOfDebtor", "Blacklisting service offered by the Debtor Agent; Debtor included the Creditor on his “Blacklist”. In the Blacklist the Debtor may list all Creditors not allowed to debit Debtor bank account."), + SL13("MaximumNumberOfDirectDebitTransactionsExceeded", "Due to Maximum allowed Direct Debit Transactions per period service offered by the Debtor Agent."), + SL14("MaximumDirectDebitTransactionAmountExceeded", "Due to Maximum allowed Direct Debit Transaction amount service offered by the Debtor Agent."), + SP01("PaymentStopped", "Payment is stopped by account holder."), + SP02("PreviouslyStopped", "Previously stopped by means of a stop payment advise."), + SVNR("ServiceNotRendered", "The card payment is returned since a cash amount rendered was not correct or goods or a service was not rendered to the customer, e.g. in an e-commerce situation."), + TM01("CutOffTime", "Associated message was received after agreed processing cut-off time."), + TRAC("RemovedFromTracking", "Return following direct debit being removed from tracking process."), + UPAY("UnduePayment", "Payment is not justified."), +} + |