libeufin

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

commit 213256b16aab1b1e8ae6fac98f886a14d4537860
parent 744031ef89e1c0fec471fa52e199326e09596b47
Author: MS <ms@taler.net>
Date:   Wed, 17 May 2023 14:16:11 +0200

Introducing the "pf" dialect.

This dialect was tested on the PostFinance test platform.
Along the core changes, the handling of dialects themselves
was also introduced.

Diffstat:
Mcli/bin/libeufin-cli | 8+++++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt | 7++++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 7+++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 47+++++++++++++++++++++++++++++++++++++++++++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 4+++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt | 34+++++++++++++++++++++++-----------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt | 39+++++++++++++++++++++++----------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 220++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt | 244+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt | 27++++++++++++++++++++++++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt | 17++++++++++++-----
Mnexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt | 52++++++++++++++++++++++++++++++++++------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt | 1+
Mnexus/src/test/kotlin/Iso20022Test.kt | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/test/kotlin/MakeEnv.kt | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Anexus/src/test/kotlin/PostFinance.kt | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/test/kotlin/TalerTest.kt | 1+
Mutil/src/main/kotlin/CamtJsonMapping.kt | 59++++++++++++++++++++++++++++++++++++-----------------------
Mutil/src/main/kotlin/Ebics.kt | 16+++++++++++-----
Mutil/src/main/kotlin/JSON.kt | 3++-
Mutil/src/main/kotlin/XMLUtil.kt | 3++-
Mutil/src/main/kotlin/ebics_h004/EbicsRequest.kt | 6+++---
Autil/src/main/resources/xsd/pain.001.001.03.ch.02.xsd | 1212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
23 files changed, 2199 insertions(+), 185 deletions(-)

diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli @@ -542,10 +542,15 @@ def new_xlibeufinbank_connection(obj, bank_url, username, password, connection_n @click.option("--host-id", help="Host ID", required=True) @click.option("--partner-id", help="Partner ID", required=True) @click.option("--ebics-user-id", help="Ebics user ID", required=True) +@click.option( + "--dialect", + help="EBICS dialect of this connection", + required=False +) @click.argument("connection-name") @click.pass_obj def new_ebics_connection( - obj, connection_name, ebics_url, host_id, partner_id, ebics_user_id + obj, connection_name, ebics_url, host_id, partner_id, ebics_user_id, dialect ): url = urljoin_nodrop(obj.nexus_base_url, "/bank-connections") body = dict( @@ -557,6 +562,7 @@ def new_ebics_connection( hostID=host_id, partnerID=partner_id, userID=ebics_user_id, + dialect=dialect ), ) try: diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Anastasis.kt @@ -15,6 +15,7 @@ import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* import tech.libeufin.util.buildIbanPaytoUri +import tech.libeufin.util.internalServerError data class AnastasisIncomingBankTransaction( val row_id: Long, @@ -59,9 +60,13 @@ fun anastasisFilter(payment: NexusBankTransactionEntity, txDtls: TransactionDeta logger.warn("Not allowing transactions missing the BIC. IBAN and name: ${debtorIban}, $debtorName") return } + val paymentSubject = txDtls.unstructuredRemittanceInformation + if (paymentSubject == null) { + throw internalServerError("Nexus payment '${payment.accountTransactionId}' has no subject.") + } AnastasisIncomingPaymentEntity.new { this.payment = payment - subject = txDtls.unstructuredRemittanceInformation + subject = paymentSubject timestampMs = System.currentTimeMillis() debtorPaytoUri = buildIbanPaytoUri( debtorIban, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -28,6 +28,7 @@ import org.jetbrains.exposed.dao.id.LongIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction +import tech.libeufin.nexus.server.FetchLevel import tech.libeufin.util.* import java.sql.Connection import kotlin.reflect.typeOf @@ -163,7 +164,7 @@ object NexusBankMessagesTable : LongIdTable() { val bankConnection = reference("bankConnection", NexusBankConnectionsTable) val message = blob("message") val messageId = text("messageId").nullable() - val code = text("code").nullable() + val fetchLevel = enumerationByName("fetchLevel", 16, FetchLevel::class) // true when the parser could not ingest one message: val errors = bool("errors").default(false) } @@ -172,7 +173,7 @@ class NexusBankMessageEntity(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<NexusBankMessageEntity>(NexusBankMessagesTable) var bankConnection by NexusBankConnectionEntity referencedOn NexusBankMessagesTable.bankConnection var messageId by NexusBankMessagesTable.messageId - var code by NexusBankMessagesTable.code + var fetchLevel by NexusBankMessagesTable.fetchLevel var message by NexusBankMessagesTable.message var errors by NexusBankMessagesTable.errors } @@ -405,6 +406,7 @@ class NexusUserEntity(id: EntityID<Long>) : LongEntity(id) { object NexusBankConnectionsTable : LongIdTable() { val connectionId = text("connectionId") val type = text("type") + val dialect = text("dialect").nullable() val owner = reference("user", NexusUsersTable) } @@ -417,6 +419,7 @@ class NexusBankConnectionEntity(id: EntityID<Long>) : LongEntity(id) { var connectionId by NexusBankConnectionsTable.connectionId var type by NexusBankConnectionsTable.type + var dialect by NexusBankConnectionsTable.dialect var owner by NexusUserEntity referencedOn NexusBankConnectionsTable.owner } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -34,6 +34,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import startServer +import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData +import tech.libeufin.nexus.iso20022.createPain001document import tech.libeufin.nexus.iso20022.parseCamtMessage import tech.libeufin.nexus.server.client import tech.libeufin.nexus.server.nexusApp @@ -94,10 +96,51 @@ class Serve : CliktCommand("Run nexus HTTP server") { } } -class ParseCamt : CliktCommand("Parse CAMT file, outputs JSON in libEufin internal representation.") { +/** + * This command purpose is to let the user then _manually_ + * tune the pain.001, to upload it to online verifiers. + */ +class GenPain : CliktCommand( + "Generate random pain.001 document for 'pf' dialect, printing to STDOUT." +) { private val logLevel by option( help = "Set the log level to: 'off', 'error', 'warn', 'info', 'debug', 'trace', 'all'" ) + private val dialect by option( + help = "EBICS dialect using the pain.001 being generated. Defaults to 'pf' (PostFinance)", + ).default("pf") + override fun run() { + setLogLevel(logLevel) + val pain001 = createPain001document( + NexusPaymentInitiationData( + debtorIban = "CH0889144371988976754", + debtorBic = "POFICHBEXXX", + debtorName = "Sample Debtor Name", + currency = "CHF", + amount = "5.00", + creditorIban = "CH9789144829733648596", + creditorName = "Sample Creditor Name", + creditorBic = "POFICHBEXXX", + paymentInformationId = "8aae7a2ded2f", + preparationTimestamp = getNow().toInstant().toEpochMilli(), + subject = "Unstructured remittance information", + instructionId = "InstructionId", + endToEndId = "71cfbdaf901f", + messageId = "2a16b35ed69c" + ), + dialect = this.dialect + ) + println(pain001) + } +} +class ParseCamt : CliktCommand("Parse camt.05x file, outputs JSON in libEufin internal representation.") { + private val logLevel by option( + help = "Set the log level to: 'off', 'error', 'warn', 'info', 'debug', 'trace', 'all'" + ) + private val withC54 by option( + help = "Treats the input as camt.054. Without this option, the" + + " parser expects a camt.052 or camt.053 and handles them equally." + ).flag(default = false) private val filename by argument("FILENAME", "File in CAMT format") override fun run() { setLogLevel(logLevel) @@ -151,6 +194,6 @@ class Superuser : CliktCommand("Add superuser or change pw") { fun main(args: Array<String>) { NexusCommand() - .subcommands(Serve(), Superuser(), ParseCamt(), ResetTables()) + .subcommands(Serve(), Superuser(), ParseCamt(), ResetTables(), GenPain()) .main(args) } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt @@ -239,7 +239,8 @@ fun talerFilter( txDtls: TransactionDetails ) { var isInvalid = false // True when pub is invalid or duplicate. - val subject = txDtls.unstructuredRemittanceInformation + val subject = txDtls.unstructuredRemittanceInformation ?: throw + internalServerError("Payment '${payment.accountTransactionId}' has no subject, can't extract reserve pub.") val debtorName = txDtls.debtor?.name if (debtorName == null) { logger.warn("empty debtor name") @@ -380,6 +381,7 @@ fun maybeTalerRefunds(bankAccount: NexusBankAccountEntity, lastSeenId: Long) { it[NexusBankTransactionsTable.bankAccount] == bankAccount.id, "Cannot refund an _outgoing_ payment!" ) + // FIXME #7116 addPaymentInitiation( Pain001Data( diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt @@ -206,20 +206,26 @@ fun ingestBankMessagesIntoAccount( ).forEach { val processingResult: IngestedTransactionsCount = when(BankConnectionType.parseBankConnectionType(conn.type)) { BankConnectionType.EBICS -> { - val doc = XMLUtil.parseStringIntoDom(it.message.bytes.toString(Charsets.UTF_8)) + val camtString = it.message.bytes.toString(Charsets.UTF_8) + val doc = XMLUtil.parseStringIntoDom(camtString) /** * Calling the CaMt handler. After its return, all the Neuxs-meaningful * payment data got stored into the database and is ready to being further * processed by any facade OR simply be communicated to the CLI via JSON. */ - processCamtMessage( - bankAccountId, - doc, - it.code ?: throw internalServerError( - "Bank message with ID ${it.id.value} in DB table" + - " NexusBankMessagesTable has no code, but one is expected." + try { + processCamtMessage( + bankAccountId, + doc, + it.fetchLevel, + conn.dialect ) - ) + } + catch (e: Exception) { + logger.error("Could not parse the following camt document:\n${camtString}") + // rethrowing. Here just to log the failing document + throw e + } } BankConnectionType.X_LIBEUFIN_BANK -> { val jMessage = try { jacksonObjectMapper().readTree(it.message.bytes) } @@ -287,7 +293,8 @@ fun getPaymentInitiation(uuid: Long): PaymentInitiationEntity { data class LastMessagesTimes( val lastStatement: ZonedDateTime?, - val lastReport: ZonedDateTime? + val lastReport: ZonedDateTime?, + val lastNotification: ZonedDateTime? ) /** * Get the last timestamps where a report and @@ -306,6 +313,9 @@ fun getLastMessagesTimes(acct: NexusBankAccountEntity): LastMessagesTimes { }, lastStatement = acct.lastStatementCreationTimestamp?.let { ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC) + }, + lastNotification = acct.lastNotificationCreationTimestamp?.let { + ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC) } ) } @@ -336,11 +346,13 @@ fun addPaymentInitiation( debtorAccount: NexusBankAccountEntity ): PaymentInitiationEntity { return transaction { + val now = Instant.now().toEpochMilli() val nowHex = now.toString(16) val painCounter = debtorAccount.pain001Counter++ val painHex = painCounter.toString(16) val acctHex = debtorAccount.id.value.toString(16) + PaymentInitiationEntity.new { currency = paymentData.currency bankAccount = debtorAccount @@ -350,9 +362,9 @@ fun addPaymentInitiation( creditorBic = paymentData.creditorBic creditorIban = paymentData.creditorIban preparationDate = now - endToEndId = "leuf-e-$nowHex-$painHex-$acctHex" + endToEndId = paymentData.endToEndId ?: "leuf-e-$nowHex-$painHex-$acctHex" messageId = "leuf-mp1-$nowHex-$painHex-$acctHex" - paymentInformationId = paymentData.pmtInfId ?: "leuf-p-$nowHex-$painHex-$acctHex" + paymentInformationId = "leuf-p-$nowHex-$painHex-$acctHex" instructionId = "leuf-i-$nowHex-$painHex-$acctHex" } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt @@ -81,6 +81,11 @@ class EbicsDownloadSuccessResult( val orderData: ByteArray ) : EbicsDownloadResult() +class EbicsDownloadEmptyResult( + val orderData: ByteArray = ByteArray(0) +) : EbicsDownloadResult() + + /** * A bank-technical error occurred. */ @@ -126,20 +131,16 @@ suspend fun doEbicsDownloadTransaction( ) } } - /** - * At this point, the EBICS init phase went through, - * therefore the message should carry a transaction ID! - */ - if (transactionID == null) throw NexusError( - HttpStatusCode.BadGateway, - "EBICS-correct init response should contain" + - " a transaction ID, $orderType did not!" - ) // Checking the 'bank technical' code. when (initResponse.bankReturnCode) { EbicsReturnCode.EBICS_OK -> { // Success, nothing to do! } + EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE -> { + // The 'pf' dialect might respond this value here (at init phase), + // in contrast to what the default dialect does (waiting the transfer phase) + return EbicsDownloadEmptyResult() + } else -> { logger.error( "Bank-technical error at init phase: ${initResponse.bankReturnCode}" + @@ -175,7 +176,12 @@ suspend fun doEbicsDownloadTransaction( // Transfer phase for (x in 2 .. numSegments) { val transferReqStr = - createEbicsRequestForDownloadTransferPhase(subscriberDetails, transactionID, x, numSegments) + createEbicsRequestForDownloadTransferPhase( + subscriberDetails, + transactionID, + x, + numSegments + ) logger.debug("EBICS download transfer phase of ${transactionID}: sending segment $x") val transferResponseStr = client.postToBank(subscriberDetails.ebicsUrl, transferReqStr) val transferResponse = parseAndValidateEbicsResponse(subscriberDetails, transferResponseStr) @@ -216,7 +222,10 @@ suspend fun doEbicsDownloadTransaction( val respPayload = decryptAndDecompressResponse(subscriberDetails, encryptionInfo, payloadChunks) // Acknowledgement phase - val ackRequest = createEbicsRequestForDownloadReceipt(subscriberDetails, transactionID) + val ackRequest = createEbicsRequestForDownloadReceipt( + subscriberDetails, + transactionID + ) val ackResponseStr = client.postToBank( subscriberDetails.ebicsUrl, ackRequest @@ -253,6 +262,7 @@ suspend fun doEbicsUploadTransaction( } val preparedUploadData = prepareUploadPayload(subscriberDetails, payload) val req = createEbicsRequestForUploadInitialization(subscriberDetails, orderType, orderParams, preparedUploadData) + logger.debug("EBICS upload message to: ${subscriberDetails.ebicsUrl}") val responseStr = client.postToBank(subscriberDetails.ebicsUrl, req) val initResponse = parseAndValidateEbicsResponse(subscriberDetails, responseStr) @@ -266,15 +276,12 @@ suspend fun doEbicsUploadTransaction( } // The bank did NOT indicate any error, but the response // lacks required information, blame the bank. - val transactionID = initResponse.transactionID ?: throw NexusError( - HttpStatusCode.BadGateway, - "Init response must have transaction ID" - ) + val transactionID = initResponse.transactionID if (initResponse.bankReturnCode != EbicsReturnCode.EBICS_OK) { throw NexusError( HttpStatusCode.InternalServerError, reason = "Bank-technical error at init phase:" + - " ${initResponse.technicalReturnCode}" + " ${initResponse.bankReturnCode}" ) } logger.debug("Bank acknowledges EBICS upload initialization. " + diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -72,10 +72,32 @@ private data class EbicsFetchSpec( val orderParams: EbicsOrderParams ) -fun storeCamt(bankConnectionId: String, camt: String, historyType: String) { +/** + * Maps EBICS specific history types to their camt + * counterparts. That allows the database to store + * camt types per-se, without any reference to the + * EBICS message that brought them. For example, a + * EBICS "Z52" and "C52" will both bring a camt.052. + * Such camt.052 is associated with the more generic + * type of FetchLevel.REPORT. + */ +private fun getFetchLevelFromEbicsOrder(ebicsHistoryType: String): FetchLevel { + return when(ebicsHistoryType) { + "C52", "Z52" -> FetchLevel.REPORT + "C53", "Z53" -> FetchLevel.STATEMENT + "C54", "Z54" -> FetchLevel.NOTIFICATION + else -> throw internalServerError("EBICS history type '$ebicsHistoryType' not supported") + } +} + +fun storeCamt( + bankConnectionId: String, + camt: String, + fetchLevel: FetchLevel +) { val camt53doc = XMLUtil.parseStringIntoDom(camt) val msgId = camt53doc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId") - logger.info("Camt document '$msgId' received via $historyType.") + logger.info("Camt document '$msgId' received via $fetchLevel.") transaction { val conn = NexusBankConnectionEntity.findByName(bankConnectionId) if (conn == null) { @@ -85,13 +107,12 @@ fun storeCamt(bankConnectionId: String, camt: String, historyType: String) { if (oldMsg == null) { NexusBankMessageEntity.new { this.bankConnection = conn - this.code = historyType + this.fetchLevel = fetchLevel this.messageId = msgId this.message = ExposedBlob(camt.toByteArray(Charsets.UTF_8)) } } } - } /** @@ -125,17 +146,29 @@ private suspend fun fetchEbicsC5x( } when (historyType) { + // default dialect "C52" -> {} "C53" -> {} + // 'pf' dialect + "Z52" -> {} + "Z53" -> {} + "Z54" -> {} else -> { - throw NexusError(HttpStatusCode.BadRequest, "history type '$historyType' not supported") + throw NexusError( + HttpStatusCode.BadRequest, + "history type '$historyType' not supported" + ) } } when (response) { is EbicsDownloadSuccessResult -> { response.orderData.unzipWithLambda { // logger.debug("Camt entry (filename (in the Zip archive): ${it.first}): ${it.second}") - storeCamt(bankConnectionId, it.second, historyType) + storeCamt( + bankConnectionId, + it.second, + getFetchLevelFromEbicsOrder(historyType) + ) } } is EbicsDownloadBankErrorResult -> { @@ -144,6 +177,9 @@ private suspend fun fetchEbicsC5x( response.returnCode.errorCode ) } + is EbicsDownloadEmptyResult -> { + // no-op + } } } @@ -194,7 +230,10 @@ fun getEbicsSubscriberDetails(bankConnectionId: String): EbicsClientSubscriberDe val subscriber = getSubscriberFromConnection(transport) // transport exists and belongs to caller. - return getEbicsSubscriberDetailsInternal(subscriber) + val ret = getEbicsSubscriberDetailsInternal(subscriber) + if (transport.dialect != null) + ret.dialect = transport.dialect + return ret } fun Route.ebicsBankProtocolRoutes(client: HttpClient) { @@ -283,6 +322,10 @@ fun Route.ebicsBankConnectionRoutes(client: HttpClient) { client, subscriberDetails, "HTD", EbicsStandardOrderParams() ) when (response) { + is EbicsDownloadEmptyResult -> { + // no-op + logger.warn("HTD response was empty.") + } is EbicsDownloadBankErrorResult -> { throw NexusError( HttpStatusCode.BadGateway, @@ -325,7 +368,7 @@ fun Route.ebicsBankConnectionRoutes(client: HttpClient) { if (orderType.length != 3) { throw NexusError(HttpStatusCode.BadRequest, "ebics order type must be three characters") } - val paramsJson = call.receiveNullable<EbicsStandardOrderParamsDateJson>() + val paramsJson = call.receiveNullable<EbicsStandardOrderParamsEmptyJson>() val orderParams = paramsJson?.toOrderParams() ?: EbicsStandardOrderParams() val subscriberDetails = transaction { val conn = requireBankConnection(call, "connid") @@ -341,6 +384,9 @@ fun Route.ebicsBankConnectionRoutes(client: HttpClient) { orderParams ) when (response) { + is EbicsDownloadEmptyResult -> { + logger.info(orderType + " response was empty.") // no op + } is EbicsDownloadSuccessResult -> { call.respondText( response.orderData.toString(Charsets.UTF_8), @@ -405,6 +451,34 @@ fun formatHex(ba: ByteArray): String { return out } +private fun getSubmissionTypeAfterDialect(dialect: String? = null): String { + return when (dialect) { + "pf" -> "XE2" + else -> "CCT" + } +} +private fun getReportTypeAfterDialect(dialect: String? = null): String { + return when (dialect) { + "pf" -> "Z52" + else -> "C52" + } +} +private fun getStatementTypeAfterDialect(dialect: String? = null): String { + return when (dialect) { + "pf" -> "Z53" + else -> "C53" + } +} + +private fun getNotificationTypeAfterDialect(dialect: String? = null): String { + return when (dialect) { + "pf" -> "Z54" + else -> throw NotImplementedError( + "Notifications not implemented in the 'default' EBICS dialect" + ) + } +} + /** * This function returns a possibly empty list of Exception. * That helps not to stop fetching if ONE operation fails. Notably, @@ -433,14 +507,17 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { fun addForLevel(l: FetchLevel, p: EbicsOrderParams) { when (l) { FetchLevel.ALL -> { - specs.add(EbicsFetchSpec("C52", p)) - specs.add(EbicsFetchSpec("C53", p)) + specs.add(EbicsFetchSpec(getReportTypeAfterDialect(subscriberDetails.dialect), p)) + specs.add(EbicsFetchSpec(getStatementTypeAfterDialect(subscriberDetails.dialect), p)) } FetchLevel.REPORT -> { - specs.add(EbicsFetchSpec("C52", p)) + specs.add(EbicsFetchSpec(getReportTypeAfterDialect(subscriberDetails.dialect), p)) } FetchLevel.STATEMENT -> { - specs.add(EbicsFetchSpec("C53", p)) + specs.add(EbicsFetchSpec(getStatementTypeAfterDialect(subscriberDetails.dialect), p)) + } + FetchLevel.NOTIFICATION -> { + specs.add(EbicsFetchSpec(getNotificationTypeAfterDialect(subscriberDetails.dialect), p)) } } } @@ -462,7 +539,9 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { /** * This branch differentiates the last date of reports and * statements and builds the fetch instructions for each of - * them. + * them. For this reason, it does not use the "addForLevel()" + * helper, since that uses the same date for all the messages + * falling in the ALL level. */ is FetchSpecSinceLastJson -> { val pRep = EbicsStandardOrderParams( @@ -481,21 +560,42 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { ), ZonedDateTime.now(ZoneOffset.UTC) ) ) + val pNtfn = EbicsStandardOrderParams( + EbicsDateRange( + lastTimes.lastNotification ?: ZonedDateTime.ofInstant( + Instant.EPOCH, + ZoneOffset.UTC + ), ZonedDateTime.now(ZoneOffset.UTC) + ) + ) when (fetchSpec.level) { - /** - * This branch doesn't call the "addForLevel()" helper because - * that takes only ONE time range and would use it for both - * statements and reports. - */ FetchLevel.ALL -> { - specs.add(EbicsFetchSpec("C52", pRep)) - specs.add(EbicsFetchSpec("C53", pStmt)) + specs.add(EbicsFetchSpec( + orderType = getReportTypeAfterDialect(dialect = subscriberDetails.dialect), + orderParams = pRep + )) + specs.add(EbicsFetchSpec( + orderType = getStatementTypeAfterDialect(dialect = subscriberDetails.dialect), + orderParams = pStmt + )) } FetchLevel.REPORT -> { - specs.add(EbicsFetchSpec("C52", pRep)) + specs.add(EbicsFetchSpec( + orderType = getReportTypeAfterDialect(dialect = subscriberDetails.dialect), + orderParams = pRep + )) } FetchLevel.STATEMENT -> { - specs.add(EbicsFetchSpec("C53", pStmt)) + specs.add(EbicsFetchSpec( + orderType = getStatementTypeAfterDialect(dialect = subscriberDetails.dialect), + orderParams = pStmt + )) + } + FetchLevel.NOTIFICATION -> { + specs.add(EbicsFetchSpec( + orderType = getNotificationTypeAfterDialect(dialect = subscriberDetails.dialect), + orderParams = pNtfn + )) } } } @@ -520,6 +620,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { return errors return null } + // Submit one Pain.001 for one payment initiations. override suspend fun submitPaymentInitiation(httpClient: HttpClient, paymentInitiationId: Long) { val dbData = transaction { @@ -545,7 +646,8 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { instructionId = preparedPayment.instructionId, endToEndId = preparedPayment.endToEndId, messageId = preparedPayment.messageId - ) + ), + dialect = subscriberDetails.dialect ) object { val painXml = painMessage @@ -568,7 +670,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { doEbicsUploadTransaction( httpClient, dbData.subscriberDetails, - "CCT", + getSubmissionTypeAfterDialect(dbData.subscriberDetails.dialect), dbData.painXml.toByteArray(Charsets.UTF_8), EbicsStandardOrderParams() ) @@ -648,6 +750,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { val subscriber = transaction { getEbicsSubscriberDetails(bankConnectionId) } val ret = EbicsKeysBackupJson( type = "ebics", + dialect = subscriber.dialect, userID = subscriber.userId, hostID = subscriber.hostId, partnerID = subscriber.partnerId, @@ -669,7 +772,19 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { subscriber.customerSignPriv.encoded, passphrase ) - ) + ), + bankAuthBlob = run { + val maybeBankAuthPub = subscriber.bankAuthPub + if (maybeBankAuthPub != null) + return@run bytesToBase64(maybeBankAuthPub.encoded) + null + }, + bankEncBlob = run { + val maybeBankEncPub = subscriber.bankEncPub + if (maybeBankEncPub != null) + return@run bytesToBase64(maybeBankEncPub.encoded) + null + } ) val mapper = ObjectMapper() return mapper.valueToTree(ret) @@ -683,7 +798,6 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { details.put("ebicsHostId", ebicsSubscriber.hostId) details.put("partnerId", ebicsSubscriber.partnerId) details.put("userId", ebicsSubscriber.userId) - details.put( "customerAuthKeyHash", CryptoUtil.getEbicsPublicKeyHash( @@ -717,16 +831,22 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { node.set<JsonNode>("details", details) return node } - override fun createConnection(connId: String, user: NexusUserEntity, data: JsonNode) { + override fun createConnection( + connId: String, + user: NexusUserEntity, + data: JsonNode + ) { + val newTransportData = jacksonObjectMapper() + .treeToValue(data, EbicsNewTransport::class.java) ?: throw NexusError( + HttpStatusCode.BadRequest, + "Ebics details not found in request" + ) val bankConn = NexusBankConnectionEntity.new { this.connectionId = connId owner = user type = "ebics" + this.dialect = newTransportData.dialect } - val newTransportData = jacksonObjectMapper( - ).treeToValue(data, EbicsNewTransport::class.java) ?: throw NexusError( - HttpStatusCode.BadRequest, "Ebics details not found in request" - ) val pairA = CryptoUtil.generateRsaKeyPair(2048) val pairB = CryptoUtil.generateRsaKeyPair(2048) val pairC = CryptoUtil.generateRsaKeyPair(2048) @@ -752,14 +872,18 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { backup: JsonNode ) { if (passphrase === null) { - throw NexusError(HttpStatusCode.BadRequest, "EBICS backup needs passphrase") + throw NexusError( + HttpStatusCode.BadRequest, + "EBICS backup needs passphrase" + ) } + val ebicsBackup = jacksonObjectMapper().treeToValue(backup, EbicsKeysBackupJson::class.java) val bankConn = NexusBankConnectionEntity.new { connectionId = connId owner = user type = "ebics" + this.dialect = ebicsBackup.dialect } - val ebicsBackup = jacksonObjectMapper().treeToValue(backup, EbicsKeysBackupJson::class.java) val (authKey, encKey, sigKey) = try { Triple( CryptoUtil.decryptKey( @@ -795,7 +919,31 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { nexusBankConnection = bankConn ebicsIniState = EbicsInitState.UNKNOWN ebicsHiaState = EbicsInitState.UNKNOWN - } + if (ebicsBackup.bankAuthBlob != null) { + val keyBlob = base64ToBytes(ebicsBackup.bankAuthBlob) + try { CryptoUtil.loadRsaPublicKey(keyBlob) } + catch (e: Exception) { + logger.error("Could not restore bank's auth public key") + throw NexusError( + HttpStatusCode.BadRequest, + "Bad bank's auth pub" + ) + } + bankAuthenticationPublicKey = ExposedBlob(keyBlob) + } + if (ebicsBackup.bankEncBlob != null) { + val keyBlob = base64ToBytes(ebicsBackup.bankEncBlob) + try { CryptoUtil.loadRsaPublicKey(keyBlob) } + catch (e: Exception) { + logger.error("Could not restore bank's enc public key") + throw NexusError( + HttpStatusCode.BadRequest, + "Bad bank's enc pub" + ) + } + bankEncryptionPublicKey = ExposedBlob(keyBlob) + } + } } catch (e: Exception) { throw NexusError( HttpStatusCode.BadRequest, @@ -811,6 +959,10 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { client, subscriberDetails, "HTD", EbicsStandardOrderParams() ) when (response) { + is EbicsDownloadEmptyResult -> { + // no-op + logger.warn("HTD response was empty.") + } is EbicsDownloadBankErrorResult -> { throw NexusError( HttpStatusCode.BadGateway, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt @@ -37,7 +37,6 @@ 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 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -49,16 +48,22 @@ 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.EbicsDialects +import tech.libeufin.nexus.server.FetchLevel +import tech.libeufin.nexus.server.PaymentUidQualifiers import tech.libeufin.util.* import toPlainString import java.time.Instant +import java.time.LocalDateTime import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter enum class CashManagementResponseType(@get:JsonValue val jsonName: String) { - Report("report"), Statement("statement"), Notification("notification") + Report("report"), + Statement("statement"), + Notification("notification") } @JsonInclude(JsonInclude.Include.NON_NULL) @@ -124,30 +129,40 @@ data class NexusPaymentInitiationData( val instructionId: String? = null ) +data class Pain001Namespaces( + val fullNamespace: String, + val xsdFilename: String +) + /** * Create a PAIN.001 XML document according to the input data. * Needs to be called within a transaction block. */ -fun createPain001document(paymentData: NexusPaymentInitiationData): String { - // Every PAIN.001 document contains at least three IDs: - // - // 1) MsgId: a unique id for the message itself - // 2) PmtInfId: the unique id for the payment's set of information - // 3) EndToEndId: a unique id to be shared between the debtor and - // creditor that uniquely identifies the transaction - // - // For now and for simplicity, since every PAIN entry in the database - // has a unique ID, and the three values aren't required to be mutually different, - // we'll assign the SAME id (= the row id) to all the three aforementioned - // PAIN id types. +fun createPain001document( + paymentData: NexusPaymentInitiationData, + dialect: String? = null +): String { + + val namespace: Pain001Namespaces = if (dialect == "pf") + Pain001Namespaces( + fullNamespace = "http://www.six-interbank-clearing.com/de/pain.001.001.03.ch.02.xsd", + xsdFilename = "pain.001.001.03.ch.02.xsd" + ) + else Pain001Namespaces( + fullNamespace = "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03", + xsdFilename = "pain.001.001.03.xsd" + ) + + val paymentMethod = if (dialect == "pf") + "SDVA" else "SEPA" val s = constructXml(indent = true) { root("Document") { - attribute("xmlns", "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03") + attribute("xmlns", namespace.fullNamespace) attribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") attribute( "xsi:schemaLocation", - "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03 pain.001.001.03.xsd" + "${namespace.fullNamespace} ${namespace.xsdFilename}" ) element("CstmrCdtTrfInitn") { element("GrpHdr") { @@ -188,7 +203,7 @@ fun createPain001document(paymentData: NexusPaymentInitiationData): String { text(paymentData.amount) } element("PmtTpInf/SvcLvl/Cd") { - text("SEPA") + text(paymentMethod) } element("ReqdExctnDt") { val dateMillis = paymentData.preparationTimestamp @@ -484,7 +499,6 @@ private fun XmlElementDestructor.extractTransactionDetails( maybeUniqueChildNamed("CntrValAmt") { extractCurrencyAmount() } }, currencyExchange = currencyExchange, - // FIXME: implement interBankSettlementAmount = null, endToEndId = maybeUniqueChildNamed("Refs") { maybeUniqueChildNamed("EndToEndId") { focusElement.textContent } @@ -492,6 +506,9 @@ private fun XmlElementDestructor.extractTransactionDetails( paymentInformationId = maybeUniqueChildNamed("Refs") { maybeUniqueChildNamed("PmtInfId") { focusElement.textContent } }, + accountServicerRef = maybeUniqueChildNamed("Refs") { + maybeUniqueChildNamed("AcctSvcrRef") { focusElement.textContent } + }, unstructuredRemittanceInformation = maybeUniqueChildNamed("RmtInf") { val chunks = mapEachChildNamed("Ustrd") { focusElement.textContent } if (chunks.isEmpty()) { @@ -499,7 +516,7 @@ private fun XmlElementDestructor.extractTransactionDetails( } else { chunks.joinToString(separator = "") } - } ?: "", + }, creditorAgent = maybeUniqueChildNamed("RltdAgts") { maybeUniqueChildNamed("CdtrAgt") { extractAgent() } }, debtorAgent = maybeUniqueChildNamed("RltdAgts") { maybeUniqueChildNamed("DbtrAgt") { extractAgent() } }, debtorAccount = maybeUniqueChildNamed("RltdPties") { maybeUniqueChildNamed("DbtrAcct") { extractAccount() } }, @@ -676,6 +693,11 @@ fun parseCamtMessage(doc: Document): CamtParseResult { extractInnerTransactions() } } + "BkToCstmrDbtCdtNtfctn" -> { + mapEachChildNamed("Ntfctn") { + extractInnerTransactions() + } + } else -> { throw CamtParsingError("expected statement or report") } @@ -695,6 +717,7 @@ fun parseCamtMessage(doc: Document): CamtParseResult { when (focusElement.localName) { "BkToCstmrAcctRpt" -> CashManagementResponseType.Report "BkToCstmrStmt" -> CashManagementResponseType.Statement + "BkToCstmrDbtCdtNtfctn" -> CashManagementResponseType.Notification else -> { throw CamtParsingError("expected statement or report") } @@ -710,6 +733,104 @@ fun parseCamtMessage(doc: Document): CamtParseResult { } } +// Get timestamp in milliseconds, according to the EBICS+camt dialect. +fun getTimestampInMillis( + dateTimeFromCamt: String, + dialect: String? = null +): Long { + return when(dialect) { + EbicsDialects.POSTFINANCE.dialectName -> { + val withoutTimezone = LocalDateTime.parse( + dateTimeFromCamt, + DateTimeFormatter.ISO_LOCAL_DATE_TIME + ) + ZonedDateTime.of( + withoutTimezone, + ZoneId.of("Europe/Zurich")).toInstant().toEpochMilli() + } + else -> { + ZonedDateTime.parse( + dateTimeFromCamt, + DateTimeFormatter.ISO_DATE_TIME + ).toInstant().toEpochMilli() + } + } +} + +/** + * Extracts the UID from the payment, according to dialect + * and direction. It returns the _qualified_ string from such + * ID. A qualified string has the format "$qualifier:$extracted_id". + * $qualifier is a constant that gives more context about the + * actual $extracted_id; for example, it may indicate that the + * ID was assigned by the bank, or by Nexus when it uploaded + * the payment initiation in the first place. + * + * NOTE: this version _still_ expect only singleton transactions + * in the input. That means _only one_ element is expected at the + * lowest level of the camt.05x report. This may/should change in + * future versions. + */ +fun extractPaymentUidFromSingleton( + ntry: CamtBankAccountEntry, + camtMessageId: String, // used to print errors. + dialect: String? + ): String { + // First check if the input is a singleton. + val batchTransactions: List<BatchTransaction>? = ntry.batches?.get(0)?.batchTransactions + val tx: BatchTransaction = if (ntry.batches?.size != 1 || batchTransactions?.size != 1) { + logger.error("camt message ${camtMessageId} has non singleton transactions.") + throw internalServerError("Dialect $dialect sent camt with non singleton transactions.") + } else + batchTransactions[0] + + when(dialect) { + EbicsDialects.POSTFINANCE.dialectName -> { + if (tx.creditDebitIndicator == CreditDebitIndicator.DBIT) { + val expectedEndToEndId = tx.details.endToEndId + /** + * Because this is an outgoing transaction, and because + * Nexus should have included the EndToEndId in the original + * pain.001, this transaction must have it (recall: EndToEndId + * is mandatory in the pain.001). A null value means therefore + * that the payment was done via another mean than pain.001. + */ + if (expectedEndToEndId == null) { + logger.error("Camt '$camtMessageId' shows outgoing payment _without_ the EndToEndId." + + " This likely wasn't initiated via pain.001" + ) + throw internalServerError("Internal reconciliation error (no EndToEndId)") + } + return "${PaymentUidQualifiers.NEXUS_GIVEN}:$expectedEndToEndId" + } + // Didn't return/throw before, it must be an incoming payment. + val maybeAcctSvcrRef = tx.details.accountServicerRef + // Expecting this value to be at the lowest level, as observed on the test platform. + val expectedAcctSvcrRef = tx.details.accountServicerRef + if (expectedAcctSvcrRef == null) { + logger.error("AcctSvcrRef was expected at the lowest tx level for dialect: $dialect, but wasn't found") + throw internalServerError("Internal reconciliation error (no AcctSvcrRef at lowest tx level)") + } + return "${PaymentUidQualifiers.BANK_GIVEN}:$expectedAcctSvcrRef" + } + // This is the default dialect, the one tested with GLS. + null -> { + /** + * This dialect has shown the AcctSvcrRef to be always given + * at the level that _contains_ the (singleton) transaction(s). + * This occurs _regardless_ of the payment direction. + */ + val expectedAcctSvcrRef = ntry.accountServicerRef + if (expectedAcctSvcrRef == null) { + logger.error("AcctSvcrRef was expected at the outer tx level for dialect: GLS, but wasn't found.") + throw internalServerError("Internal reconciliation error: AcctSvcrRef not found at outer level.") + } + return "${PaymentUidQualifiers.BANK_GIVEN}:$expectedAcctSvcrRef" + } + else -> throw internalServerError("Dialect $dialect is not supported.") + } +} + /** * Given that every CaMt is a collection of reports/statements * where each of them carries the bank account balance and a list @@ -720,7 +841,7 @@ fun parseCamtMessage(doc: Document): CamtParseResult { * report/statement. * - finds which transactions were already downloaded. * - stores a new NexusBankTransactionEntity for each new tx -accounted in the report/statement. + * accounted in the report/statement. * - tries to link the new transaction with a submitted one, in * case of DBIT transaction. * - returns a IngestedTransactionCount object. @@ -728,12 +849,16 @@ accounted in the report/statement. fun processCamtMessage( bankAccountId: String, camtDoc: Document, + fetchLevel: FetchLevel, + dialect: String? = null +): IngestedTransactionsCount { /** - * FIXME: should NOT be C52/C53 but "report" or "statement". - * The reason is that C52/C53 are NOT CaMt, they are EBICS names. + * Ensure that the level is not ALL, as the parser expects + * the exact type for the one message being parsed. */ - code: String -): IngestedTransactionsCount { + if (fetchLevel == FetchLevel.ALL) + throw internalServerError("Parser needs exact camt type (ALL not permitted).") + var newTransactions = 0 var downloadedTransactions = 0 transaction { @@ -742,7 +867,7 @@ fun processCamtMessage( throw NexusError(HttpStatusCode.NotFound, "user not found") } val res = try { parseCamtMessage(camtDoc) } catch (e: CamtParsingError) { - logger.warn("Invalid CAMT received from bank: $e") + logger.warn("Invalid CAMT received from bank: ${e.message}") newTransactions = -1 return@transaction } @@ -774,31 +899,28 @@ fun processCamtMessage( } } // Updating the local bank account state timestamps according to the current document. - val stamp = ZonedDateTime.parse( - res.creationDateTime, - DateTimeFormatter.ISO_DATE_TIME - ).toInstant().toEpochMilli() - when (code) { - "C52" -> { + val stamp = getTimestampInMillis(res.creationDateTime, dialect = dialect) + when (fetchLevel) { + FetchLevel.REPORT -> { val s = acct.lastReportCreationTimestamp - /** - * FIXME. - * The following check seems broken, as it ONLY sets the value when - * s is non-null BUT s gets never set; not even with a default value. - * That didn't break so far because the timestamp gets only used when - * the fetch specification has "since-last" for the time range. Never - * used. - */ - if (s != null && stamp > s) { + if (s == null || stamp > s) { acct.lastReportCreationTimestamp = stamp } } - "C53" -> { + FetchLevel.STATEMENT -> { val s = acct.lastStatementCreationTimestamp - if (s != null && stamp > s) { + if (s == null || stamp > s) { acct.lastStatementCreationTimestamp = stamp } } + FetchLevel.NOTIFICATION -> { + val s = acct.lastNotificationCreationTimestamp + if (s == null || stamp > s) { + acct.lastNotificationCreationTimestamp = stamp + } + } + // Silencing the compiler: the 'ALL' case was checked at the top of this function. + else -> {} } val entries: List<CamtBankAccountEntry> = res.reports.map { it.entries }.flatten() var newPaymentsLog = "" @@ -809,22 +931,26 @@ fun processCamtMessage( HttpStatusCode.InternalServerError, "Singleton money movements policy wasn't respected" ) - val acctSvcrRef = entry.accountServicerRef - if (acctSvcrRef == null) { - // FIXME(dold): Report this! - logger.error("missing account servicer reference in transaction") + if (entry.status != EntryStatus.BOOK) { + logger.info("camt message '${res.messageId}' has a " + + "non-BOOK transaction, ignoring it." + ) continue } - val duplicate = findDuplicate(bankAccountId, acctSvcrRef) + val paymentUid = extractPaymentUidFromSingleton( + ntry = entry, + camtMessageId = res.messageId, + dialect = dialect + ) + val duplicate = findDuplicate(bankAccountId, paymentUid) if (duplicate != null) { - logger.info("Found a duplicate (acctSvcrRef): $acctSvcrRef") - // FIXME(dold): See if an old transaction needs to be superseded by this one + logger.info("Found a duplicate, UID is $paymentUid") // https://bugs.gnunet.org/view.php?id=6381 continue@txloop } val rawEntity = NexusBankTransactionEntity.new { bankAccount = acct - accountTransactionId = acctSvcrRef + accountTransactionId = paymentUid amount = singletonBatchedTransaction.amount.value currency = singletonBatchedTransaction.amount.currency transactionJson = jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(entry) @@ -833,29 +959,35 @@ fun processCamtMessage( } rawEntity.flush() newTransactions++ - newPaymentsLog += "\n- " + entry.getSingletonSubject() + newPaymentsLog += "\n- ${entry.getSingletonSubject()}" + // This block tries to acknowledge a former outgoing payment as booked. if (singletonBatchedTransaction.creditDebitIndicator == CreditDebitIndicator.DBIT) { val t0 = singletonBatchedTransaction.details - val pmtInfId = t0.paymentInformationId - if (pmtInfId != null) { + val endToEndId = t0.endToEndId + if (endToEndId != null) { + logger.debug("Reconciling outgoing payment with EndToEndId: $endToEndId") val paymentInitiation = PaymentInitiationEntity.find { PaymentInitiationsTable.bankAccount eq acct.id and ( // pmtInfId is a value that the payment submitter // asked the bank to associate with the payment to be made. - PaymentInitiationsTable.paymentInformationId eq pmtInfId) + PaymentInitiationsTable.endToEndId eq endToEndId) }.firstOrNull() if (paymentInitiation != null) { - logger.info("Could confirm one initiated payment: $pmtInfId") + logger.info("Could confirm one initiated payment: $endToEndId") paymentInitiation.confirmationTransaction = rawEntity } } + // Every payment initiated by Nexus has EndToEndId. Warn if not found. + else + logger.warn("Camt ${res.messageId} has outgoing payment without EndToEndId..") } } if (newTransactions > 0) - logger.debug("Camt $code '${res.messageId}' has new payments:${newPaymentsLog}") + logger.debug("Camt $fetchLevel '${res.messageId}' has new payments:${newPaymentsLog}") } + return IngestedTransactionsCount( newTransactions = newTransactions, downloadedTransactions = downloadedTransactions diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt @@ -58,10 +58,35 @@ fun unknownBankAccount(bankAccountLabel: String): NexusError { * strings. */ +enum class EbicsDialects(val dialectName: String) { + POSTFINANCE("pf") +} + +/** + * Nexus needs to uniquely identify a payment, in order + * to spot the same payment to be ingested more than once. + * For example, payment X may have been already ingested + * (and possibly led to a Taler withdrawal) via a EBICS C52 + * order, and might be later again downloaded via another + * EBICS order (e.g. C53). The second time this payment + * reaches Nexus, it must NOT be considered new, therefore + * Nexus needs a UID to check its database for the presence + * of known payments. Every bank assigns UIDs in a different + * fashion, sometimes even differentiating between incoming and + * outgoing payments; Nexus therefore classifies those UIDs + * by assigning them one of the names defined in the following + * enum class. This way, Nexus has more control when it tries + * to locally reconcile payments. + */ +enum class PaymentUidQualifiers(qualifierName: String) { + BANK_GIVEN("bank_given"), + NEXUS_GIVEN("nexus_given") +} + // Valid connection types. enum class BankConnectionType(val typeName: String) { EBICS("ebics"), - X_LIBEUFIN_BANK("x-taler-bank"); + X_LIBEUFIN_BANK("x-libeufin-bank"); companion object { /** * This method takes legacy bank connection type names as input diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt @@ -20,7 +20,6 @@ package tech.libeufin.nexus.server import CamtBankAccountEntry -import CurrencyAmount import EntryStatus import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo @@ -143,7 +142,10 @@ data class EbicsKeysBackupJson( val ebicsURL: String, val authBlob: String, val encBlob: String, - val sigBlob: String + val sigBlob: String, + val bankAuthBlob: String?, + val bankEncBlob: String?, + val dialect: String? = null ) enum class PermissionChangeAction(@get:JsonValue val jsonName: String) { @@ -170,7 +172,10 @@ data class ChangePermissionsRequest( ) enum class FetchLevel(@get:JsonValue val jsonName: String) { - REPORT("report"), STATEMENT("statement"), ALL("all"); + REPORT("report"), + STATEMENT("statement"), + NOTIFICATION("notification"), + ALL("all"); } /** @@ -232,6 +237,7 @@ class CreateBankConnectionFromBackupRequestJson( class CreateBankConnectionFromNewRequestJson( name: String, val type: String, + val dialect: String? = null, val data: JsonNode ) : CreateBankConnectionRequestJson(name) @@ -240,7 +246,8 @@ data class EbicsNewTransport( val partnerID: String, val hostID: String, val ebicsURL: String, - val systemID: String? + val systemID: String?, + val dialect: String? = null ) /** @@ -386,7 +393,7 @@ data class Pain001Data( val sum: String, val currency: String, val subject: String, - val pmtInfId: String? = null + val endToEndId: String? = null ) data class AccountTask( diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt @@ -25,28 +25,20 @@ import io.ktor.server.plugins.contentnegotiation.* import com.fasterxml.jackson.core.util.DefaultIndenter import com.fasterxml.jackson.core.util.DefaultPrettyPrinter import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.fasterxml.jackson.module.kotlin.* import io.ktor.client.* import io.ktor.http.* -import io.ktor.network.sockets.* import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* import io.ktor.server.plugins.* import io.ktor.server.plugins.callloging.* import io.ktor.server.plugins.statuspages.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -import io.ktor.util.* -import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.sql.and -import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.event.Level import tech.libeufin.nexus.* @@ -54,9 +46,7 @@ import tech.libeufin.nexus.bankaccount.* import tech.libeufin.nexus.ebics.* import tech.libeufin.nexus.iso20022.processCamtMessage import tech.libeufin.util.* -import java.net.BindException import java.net.URLEncoder -import kotlin.system.exitProcess // Return facade state depending on the type. fun getFacadeState(type: String, facade: FacadeEntity): JsonNode { @@ -439,10 +429,24 @@ val nexusApp: Application.() -> Unit = { } post("/bank-accounts/{accountId}/test-camt-ingestion/{type}") { requireSuperuser(call.request) + val accountId = ensureNonNull(call.parameters["accountId"]) + val bankAccount = getBankAccount(accountId) + val connId = transaction { bankAccount.defaultBankConnection?.connectionId } + val dialect = if (connId != null) { + val defaultConn = getBankConnection(connId) + defaultConn.dialect + } else null + val msgType = ensureNonNull(call.parameters["type"]) processCamtMessage( - ensureNonNull(call.parameters["accountId"]), + ensureNonNull(accountId), XMLUtil.parseStringIntoDom(call.receiveText()), - ensureNonNull(call.parameters["type"]) + when(msgType) { + "C52", "Z52" -> { FetchLevel.REPORT } + "C53", "Z53" -> { FetchLevel.STATEMENT } + "C54", "Z54" -> { FetchLevel.NOTIFICATION } + else -> throw badRequest("Message type: '$msgType', not supported") + }, + dialect = dialect ) call.respond(object {}) return@post @@ -693,7 +697,7 @@ val nexusApp: Application.() -> Unit = { if (body.uid != null) { val maybeExists: PaymentInitiationEntity? = transaction { PaymentInitiationEntity.find { - PaymentInitiationsTable.paymentInformationId eq body.uid + PaymentInitiationsTable.endToEndId eq body.uid }.firstOrNull() } // If submitted payment looks exactly the same as the one @@ -733,7 +737,7 @@ val nexusApp: Application.() -> Unit = { sum = amount.amount, currency = amount.currency, subject = body.subject, - pmtInfId = body.uid + endToEndId = body.uid ), bankAccount ) @@ -850,14 +854,26 @@ val nexusApp: Application.() -> Unit = { is CreateBankConnectionFromBackupRequestJson -> { val type = body.data.get("type") if (type == null || !type.isTextual) { - throw NexusError(HttpStatusCode.BadRequest, "backup needs type") + throw NexusError( + HttpStatusCode.BadRequest, + "backup needs type" + ) } val plugin = getConnectionPlugin(type.textValue()) - plugin.createConnectionFromBackup(body.name, user, body.passphrase, body.data) + plugin.createConnectionFromBackup( + body.name, + user, + body.passphrase, + body.data + ) } is CreateBankConnectionFromNewRequestJson -> { val plugin = getConnectionPlugin(body.type) - plugin.createConnection(body.name, user, body.data) + plugin.createConnection( + body.name, + user, + body.data + ) } } } @@ -946,7 +962,7 @@ val nexusApp: Application.() -> Unit = { list.bankMessages.add( BankMessageInfo( messageId = it.messageId, - code = it.code, + code = it.fetchLevel.jsonName, length = it.message.bytes.size.toLong() ) ) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt @@ -303,6 +303,7 @@ class XlibeufinBankConnectionProtocol : BankConnectionProtocol { NexusBankMessageEntity.new { bankConnection = conn message = ExposedBlob(respBlob) + fetchLevel = fetchSpec.level } } return null diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt @@ -1,14 +1,33 @@ package tech.libeufin.nexus import CamtBankAccountEntry import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.server.testing.* +import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Ignore import org.junit.Test import org.w3c.dom.Document +import poFiCamt052 +import poFiCamt054 +import prepNexusDb +import tech.libeufin.nexus.bankaccount.getBankAccount import tech.libeufin.nexus.iso20022.* +import tech.libeufin.nexus.server.EbicsDialects +import tech.libeufin.nexus.server.FetchLevel +import tech.libeufin.nexus.server.getBankConnection +import tech.libeufin.nexus.server.nexusApp import tech.libeufin.util.DestructionError import tech.libeufin.util.XMLUtil import tech.libeufin.util.destructXml +import withTestDatabase import java.math.BigDecimal +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.TimeZone import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -85,4 +104,63 @@ class Iso20022Test { println(jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(r)) } + + /** + * PoFi timestamps aren't zoned, therefore the usual ZonedDateTime + * doesn't cover it. They must switch to (java.time.)LocalDateTime. + */ + @Test + fun parsePostFinanceDate() { + // 2011-12-03T10:15:30 from Java Doc as ISO_LOCAL_DATE_TIME. + // 2023-05-09T11:04:09 from PoFi + + getTimestampInMillis( + "2011-12-03T10:15:30", + EbicsDialects.POSTFINANCE.dialectName + ) + getTimestampInMillis( + "2011-12-03T10:15:30Z" // ! with timezone + ) + } + + @Test + fun parsePoFiCamt054() { + val doc = XMLUtil.parseStringIntoDom(poFiCamt054) + parseCamtMessage(doc) + } + + @Test + fun ingestPoFiCamt054() { + val doc = XMLUtil.parseStringIntoDom(poFiCamt054) + withTestDatabase { prepNexusDb() + processCamtMessage( + "foo", + doc, + FetchLevel.NOTIFICATION, + dialect = "pf" + ) + } + } + + @Test + fun parsePostFinanceCamt052() { + withTestDatabase { + prepNexusDb() + // Adjusting the MakeEnv.kt values to PoFi + val fooBankAccount = getBankAccount("foo") + val fooConnection = getBankConnection("foo") + transaction { + fooBankAccount.iban = "CH9789144829733648596" + fooConnection.dialect = "pf" + } + testApplication { + application(nexusApp) + client.post("/bank-accounts/foo/test-camt-ingestion/C52") { + basicAuth("foo", "foo") + contentType(ContentType.Application.Xml) + setBody(poFiCamt052) + } + } + } + } } diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt @@ -69,11 +69,13 @@ inline fun <reified ExceptionType> assertException( * Run a block after connecting to the test database. * Cleans up the DB file afterwards. */ -fun withTestDatabase(f: () -> Unit) { +fun withTestDatabase(keepData: Boolean = false, f: () -> Unit) { Database.connect(TEST_DB_CONN, user = currentUser) TransactionManager.manager.defaultIsolationLevel = java.sql.Connection.TRANSACTION_SERIALIZABLE - dbDropTables(TEST_DB_CONN) - tech.libeufin.sandbox.dbDropTables(TEST_DB_CONN) + if (!keepData) { + dbDropTables(TEST_DB_CONN) + tech.libeufin.sandbox.dbDropTables(TEST_DB_CONN) + } f() } @@ -195,11 +197,16 @@ fun prepNexusDb() { } } -fun prepSandboxDb(usersDebtLimit: Int = 1000, currency: String = "TESTKUDOS") { +fun prepSandboxDb( + usersDebtLimit: Int = 1000, + currency: String = "TESTKUDOS", + cashoutCurrency: String = "EUR" +) { tech.libeufin.sandbox.dbCreateTables(TEST_DB_CONN) transaction { val config = DemobankConfig( currency = currency, + cashoutCurrency = cashoutCurrency, bankDebtLimit = 10000, usersDebtLimit = usersDebtLimit, allowRegistrations = true, @@ -321,6 +328,7 @@ fun withSandboxTestDatabase(f: () -> Unit) { transaction { val config = DemobankConfig( currency = "TESTKUDOS", + cashoutCurrency = "NOTUSED", bankDebtLimit = 10000, usersDebtLimit = 1000, allowRegistrations = true, @@ -486,3 +494,184 @@ fun genNexusIncomingCamt( ) ) ) + +val poFiCamt054: String = """ + <?xml version="1.0" encoding="UTF-8"?> + <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.054.001.04" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.054.001.04 camt.054.001.04.xsd"> + <BkToCstmrDbtCdtNtfctn> + <GrpHdr> + <MsgId>286494ADFK/132157/448798</MsgId> + <CreDtTm>2023-05-10T13:21:57</CreDtTm> + <MsgPgntn> + <PgNb>1</PgNb> + <LastPgInd>true</LastPgInd> + </MsgPgntn> + <AddtlInf>SPS/1.7/TEST</AddtlInf> + </GrpHdr> + <Ntfctn> + <Id>286494ADFK/132157/448798</Id> + <CreDtTm>2023-05-10T13:21:57</CreDtTm> + <RptgSrc> + <Prtry>OTHR</Prtry> + </RptgSrc> + <Acct> + <Id> + <IBAN>${FOO_USER_IBAN}</IBAN> + </Id> + </Acct> + <Ntry> + <Amt Ccy="CHF">5.00</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <Sts>BOOK</Sts> + <BookgDt> + <Dt>2023-05-10</Dt> + </BookgDt> + <ValDt> + <Dt>2023-05-10</Dt> + </ValDt> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>ICDT</Cd> + <SubFmlyCd>AUTT</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <NtryDtls> + <TxDtls> + <Refs> + <MsgId>478b-9e7e-2a16b35ed69c</MsgId> + <PmtInfId>4f4-b65d-8aae7a2ded2f</PmtInfId> + <InstrId>InstructionId</InstrId> + <EndToEndId>4c3d-a74b-71cfbdaf901f</EndToEndId> + </Refs> + <Amt Ccy="CHF">5.00</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>ICDT</Cd> + <SubFmlyCd>BOOK</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <RltdPties> + <DbtrAcct> + <Id> + <IBAN>CH0889144371988976754</IBAN> + </Id> + </DbtrAcct> + <Cdtr> + <Nm>Sample Creditor Name</Nm> + </Cdtr> + <CdtrAcct> + <Id> + <IBAN>CH9789144829733648596</IBAN> + </Id> + </CdtrAcct> + </RltdPties> + <RmtInf> + <Ustrd>Unstructured remittance information</Ustrd> + </RmtInf> + </TxDtls> + </NtryDtls> + </Ntry> + </Ntfctn> + </BkToCstmrDbtCdtNtfctn> + </Document> +""".trimIndent() + +val poFiCamt052: String = """ + <?xml version="1.0"?> + <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.052.001.04" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.052.001.04 camt.052.001.04.xsd"> + <BkToCstmrAcctRpt> + <GrpHdr> + <MsgId>2827403ADFJ/110409/997113</MsgId> + <CreDtTm>2023-05-09T11:04:09</CreDtTm> + <MsgPgntn> + <PgNb>1</PgNb> + <LastPgInd>true</LastPgInd> + </MsgPgntn> + <AddtlInf>SPS/1.7/TEST</AddtlInf> + </GrpHdr> + <Rpt> + <Id>2827403ADFJ/110409/997113</Id> + <ElctrncSeqNb>129</ElctrncSeqNb> + <CreDtTm>2023-05-09T11:04:09</CreDtTm> + <FrToDt> + <FrDtTm>2023-05-09T00:00:00</FrDtTm> + <ToDtTm>2023-05-09T10:00:00</ToDtTm> + </FrToDt> + <Acct> + <Id> + <IBAN>CH9789144829733648596</IBAN> + </Id> + <Ownr> + <Nm>LibEuFin</Nm> + </Ownr> + </Acct> + <Bal> + <Tp> + <CdOrPrtry> + <Cd>OPBD</Cd> + </CdOrPrtry> + </Tp> + <Amt Ccy="CHF">500000.00</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <Dt> + <Dt>2023-05-09</Dt> + </Dt> + </Bal> + <Bal> + <Tp> + <CdOrPrtry> + <Cd>CLBD</Cd> + </CdOrPrtry> + </Tp> + <Amt Ccy="CHF">499998.00</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <Dt> + <Dt>2023-05-09</Dt> + </Dt> + </Bal> + <Ntry> + <Amt Ccy="CHF">2.00</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <RvslInd>false</RvslInd> + <Sts>BOOK</Sts> + <BookgDt> + <Dt>2023-05-09</Dt> + </BookgDt> + <ValDt> + <Dt>2023-05-09</Dt> + </ValDt> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>ICDT</Cd> + <SubFmlyCd>AUTT</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <NtryDtls> + <TxDtls> + <Refs> + <MsgId>leuf-mp1-187ffc0f021-1-1</MsgId> + <AcctSvcrRef>032663184998070600000003</AcctSvcrRef> + <PmtInfId>Zufall</PmtInfId> + <InstrId>leuf-i-187ffc0f021-1-1</InstrId> + <EndToEndId>leuf-e-187ffc0f021-1-1</EndToEndId> + </Refs> + <Amt Ccy="CHF">2.00</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + </TxDtls> + </NtryDtls> + <AddtlNtryInf>EZAG ISO 20022 SAMMELAUFTRAG E-FINANCE Zufall leuf-mp1-187ffc0f021-1-1</AddtlNtryInf> + </Ntry> + </Rpt> + </BkToCstmrAcctRpt> + </Document> +""".trimIndent() +\ No newline at end of file diff --git a/nexus/src/test/kotlin/PostFinance.kt b/nexus/src/test/kotlin/PostFinance.kt @@ -0,0 +1,100 @@ +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.ktor.client.* +import kotlinx.coroutines.runBlocking +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.ebics.doEbicsUploadTransaction +import tech.libeufin.nexus.ebics.getEbicsSubscriberDetails +import tech.libeufin.nexus.getConnectionPlugin +import tech.libeufin.nexus.getNexusUser +import tech.libeufin.nexus.server.* +import tech.libeufin.util.EbicsStandardOrderParams +import java.io.BufferedReader +import java.io.File + + +private fun downloadPayment() { + val httpClient = HttpClient() + runBlocking { + fetchBankAccountTransactions( + client = httpClient, + fetchSpec = FetchSpecLatestJson( + level = FetchLevel.NOTIFICATION, + bankConnection = null + ), + accountId = "foo" + ) + } +} + +// Causes one CRDT payment to show up in the camt.054. +private fun uploadQrrPayment() { + val httpClient = HttpClient() + val qrr = """ + Product;Channel;Account;Currency;Amount;Reference;Name;Street;Number;Postcode;City;Country;DebtorAddressLine;DebtorAddressLine;DebtorAccount;ReferenceType;UltimateDebtorName;UltimateDebtorStreet;UltimateDebtorNumber;UltimateDebtorPostcode;UltimateDebtorTownName;UltimateDebtorCountry;UltimateDebtorAddressLine;UltimateDebtorAddressLine;RemittanceInformationText + QRR;PO;CH9789144829733648596;CHF;33;;D009;Musterstrasse;1;1111;Musterstadt;CH;;;;NON;D009;Musterstrasse;1;1111;Musterstadt;CH;;;Taler-Demo + """.trimIndent() + runBlocking { + doEbicsUploadTransaction( + httpClient, + getEbicsSubscriberDetails("postfinance"), + "XTC", + qrr.toByteArray(Charsets.UTF_8), + EbicsStandardOrderParams() + ) + } +} + +/** + * Causes one DBIT payment to show up in the camt.054. This one + * however lacks the AcctSvcrRef, so other ways to pin it are needed. + * Notably, EndToEndId is mandatory in pain.001 _and_ is controlled + * by the sender. Hence, the sender can itself ensure the EndToEndId + * uniqueness. + */ +private fun uploadPain001Payment() { + transaction { + addPaymentInitiation( + Pain001Data( + creditorIban = "CH9300762011623852957", + creditorBic = "POFICHBEXXX", + creditorName = "Muster Frau", + sum = "2", + currency = "CHF", + subject = "Muster Zahlung 0", + endToEndId = "Zufall" + ), + getBankAccount("foo") + ) + } + val ebicsConn = getConnectionPlugin("ebics") + val httpClient = HttpClient() + runBlocking { + ebicsConn.submitPaymentInitiation(httpClient, 1L) + } +} +fun main() { + // Load EBICS subscriber's keys from disk. + val bufferedReader: BufferedReader = File("/tmp/pofi.json").bufferedReader() + val accessDataTxt = bufferedReader.use { it.readText() } + val ebicsConn = getConnectionPlugin("ebics") + val accessDataJson = jacksonObjectMapper().readTree(accessDataTxt) + withTestDatabase { + prepNexusDb() + transaction { + ebicsConn.createConnectionFromBackup( + connId = "postfinance", + user = getNexusUser("foo"), + passphrase = "foo", + accessDataJson + ) + val fooBankAccount = getBankAccount("foo") + fooBankAccount.defaultBankConnection = getBankConnection("postfinance") + fooBankAccount.iban = "CH9789144829733648596" + } + } + // uploadPayment() + downloadPayment() +} +\ No newline at end of file diff --git a/nexus/src/test/kotlin/TalerTest.kt b/nexus/src/test/kotlin/TalerTest.kt @@ -86,6 +86,7 @@ class TalerTest { contentType(ContentType.Application.Json) basicAuth(testedAccount, testedAccount) } + assert(r.status.value == HttpStatusCode.OK.value) val j = mapper.readTree(r.readBytes()) val wtidFromTwg = j.get("outgoing_transactions").get(0).get("wtid").asText() assert(wtidFromTwg == "T0") diff --git a/util/src/main/kotlin/CamtJsonMapping.kt b/util/src/main/kotlin/CamtJsonMapping.kt @@ -8,6 +8,7 @@ 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 org.jetbrains.exposed.sql.Transaction import tech.libeufin.util.internalServerError enum class CreditDebitIndicator { @@ -184,6 +185,7 @@ data class TransactionDetails( val endToEndId: String? = null, val paymentInformationId: String? = null, val messageId: String? = null, + val accountServicerRef: String? = null, val purpose: String?, val proprietaryPurpose: String?, @@ -214,11 +216,8 @@ data class TransactionDetails( */ val interBankSettlementAmount: CurrencyAmount?, - /** - * Unstructured remittance information (=subject line) of the transaction, - * or the empty string if missing. - */ - val unstructuredRemittanceInformation: String, + // PoFi shown entries lacking it. + val unstructuredRemittanceInformation: String?, val returnInfo: ReturnInfo? ) @@ -286,23 +285,15 @@ data class CamtBankAccountEntry( // 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] - } + // Checks that the given list contains only one element and returns it. + private 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] + } + private fun getSingletonTxDtls(): TransactionDetails { /** * Types breakdown until the meaningful payment information is reached. * @@ -329,6 +320,28 @@ data class CamtBankAccountEntry( val batchTransactions = batch.batchTransactions val tx: BatchTransaction = checkAndGetSingleton(batchTransactions) val details: TransactionDetails = tx.details - return details.unstructuredRemittanceInformation + return details + } + /** + * 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 { + val maybeSubject = getSingletonTxDtls().unstructuredRemittanceInformation + if (maybeSubject == null) { + throw internalServerError( + "The parser let in a transaction without subject" + + ", acctSvcrRef: ${this.getSingletonAcctSvcrRef()}." + ) + } + return maybeSubject + } + @JsonIgnore + fun getSingletonAcctSvcrRef(): String? { + return getSingletonTxDtls().accountServicerRef } } \ No newline at end of file diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt @@ -87,7 +87,8 @@ data class EbicsClientSubscriberDetails( val customerAuthPriv: RSAPrivateCrtKey, val customerSignPriv: RSAPrivateCrtKey, val ebicsIniState: EbicsInitState, - val ebicsHiaState: EbicsInitState + val ebicsHiaState: EbicsInitState, + var dialect: String? = null ) /** @@ -158,9 +159,12 @@ private fun signOrder( fun createEbicsRequestForDownloadReceipt( subscriberDetails: EbicsClientSubscriberDetails, - transactionID: String + transactionID: String? ): String { - val req = EbicsRequest.createForDownloadReceiptPhase(transactionID, subscriberDetails.hostId) + val req = EbicsRequest.createForDownloadReceiptPhase( + transactionID, + subscriberDetails.hostId + ) val doc = XMLUtil.convertJaxbToDocument(req) XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) return XMLUtil.convertDomToString(doc) @@ -300,7 +304,7 @@ fun createEbicsRequestForDownloadInitialization( fun createEbicsRequestForDownloadTransferPhase( subscriberDetails: EbicsClientSubscriberDetails, - transactionID: String, + transactionID: String?, segmentNumber: Int, numSegments: Int ): String { @@ -317,7 +321,7 @@ fun createEbicsRequestForDownloadTransferPhase( fun createEbicsRequestForUploadTransferPhase( subscriberDetails: EbicsClientSubscriberDetails, - transactionID: String, + transactionID: String?, preparedUploadData: PreparedUploadData, chunkIndex: Int ): String { @@ -363,10 +367,12 @@ enum class EbicsReturnCode(val errorCode: String) { EBICS_DOWNLOAD_POSTPROCESS_DONE("011000"), EBICS_DOWNLOAD_POSTPROCESS_SKIPPED("011001"), EBICS_TX_SEGMENT_NUMBER_UNDERRUN("011101"), + EBICS_AUTHENTICATION_FAILED ("061001"), EBICS_INVALID_USER_OR_USER_STATE("091002"), EBICS_PROCESSING_ERROR("091116"), EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"), EBICS_AMOUNT_CHECK_FAILED("091303"), + EBICS_EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"), EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"); companion object { diff --git a/util/src/main/kotlin/JSON.kt b/util/src/main/kotlin/JSON.kt @@ -93,7 +93,8 @@ data class XLibeufinBankTransaction( * along every API call using this object. */ val pmtInfId: String? = null, - val msgId: String? = null + val msgId: String? = null, + val endToEndId: String? = null ) data class IncomingPaymentInfo( val debtorIban: String, diff --git a/util/src/main/kotlin/XMLUtil.kt b/util/src/main/kotlin/XMLUtil.kt @@ -231,7 +231,8 @@ class XMLUtil private constructor() { "xsd/camt.052.001.02.xsd", "xsd/camt.053.001.02.xsd", "xsd/camt.054.001.02.xsd", - "xsd/pain.001.001.03.xsd" + "xsd/pain.001.001.03.xsd", + "xsd/pain.001.001.03.ch.02.xsd" ).map { val stream = classLoader.getResourceAsStream(it) ?: throw FileNotFoundException("Schema file $it not found.") diff --git a/util/src/main/kotlin/ebics_h004/EbicsRequest.kt b/util/src/main/kotlin/ebics_h004/EbicsRequest.kt @@ -289,7 +289,7 @@ class EbicsRequest { companion object { fun createForDownloadReceiptPhase( - transactionId: String, + transactionId: String?, hostId: String ): EbicsRequest { @@ -444,7 +444,7 @@ class EbicsRequest { fun createForUploadTransferPhase( hostId: String, - transactionId: String, + transactionId: String?, segNumber: BigInteger, encryptedData: String ): EbicsRequest { @@ -477,7 +477,7 @@ class EbicsRequest { fun createForDownloadTransferPhase( hostID: String, - transactionID: String, + transactionID: String?, segmentNumber: Int, numSegments: Int ): EbicsRequest { diff --git a/util/src/main/resources/xsd/pain.001.001.03.ch.02.xsd b/util/src/main/resources/xsd/pain.001.001.03.ch.02.xsd @@ -0,0 +1,1212 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- +(C) Copyright 2010, SKSF, www.sksf.ch +CH Version fuer pain.001 Credit Transfer: xmlns="http://www.iso-payments.ch" +.ch.: Identification for this CH version +Last part (.02): Version of this scheme + +Based on ISO pain.001.001.03 (urn:iso:std:iso:20022:tech:xsd:pain.001.001.03) + +Anregungen und Fragen zu diesem Dokument können an das jeweilige Finanzinstitut gerichtet werden. +Allgemeine Anregungen können auch bei der SIX Interbank Clearing AG unter folgender Adresse angebracht werden: +pm@six-group.com + +History +15.02.2010 V01 initial version, targetNamespace="http://www.six-interbank-clearing.com/de/pain.001.001.03.ch.01.xsd" File:pain.001.001.03.ch.01.xsd +30.04.2010 V02 added: element Initiating Party/Contact Details contains Software name and Version of producing application + changed: name in "PartyIdentification32-CH_Name" mandatory +--> +<!-- V01: changed: +--> +<xs:schema xmlns="http://www.six-interbank-clearing.com/de/pain.001.001.03.ch.02.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.six-interbank-clearing.com/de/pain.001.001.03.ch.02.xsd" elementFormDefault="qualified"> + <xs:element name="Document" type="Document"/> + <!-- V01: changed: CH version changes applied --> + <xs:complexType name="AccountIdentification4Choice-CH"> + <xs:sequence> + <xs:choice> + <xs:element name="IBAN" type="IBAN2007Identifier"/> + <xs:element name="Othr" type="GenericAccountIdentification1-CH"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <!-- V01: unused + <xs:complexType name="AccountSchemeName1Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="ExternalAccountIdentification1Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + --> + <xs:simpleType name="ActiveOrHistoricCurrencyAndAmount_SimpleType"> + <xs:restriction base="xs:decimal"> + <xs:minInclusive value="0"/> + <xs:fractionDigits value="5"/> + <xs:totalDigits value="18"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="ActiveOrHistoricCurrencyAndAmount"> + <xs:simpleContent> + <xs:extension base="ActiveOrHistoricCurrencyAndAmount_SimpleType"> + <xs:attribute name="Ccy" type="ActiveOrHistoricCurrencyCode" use="required"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + <xs:simpleType name="ActiveOrHistoricCurrencyCode"> + <xs:restriction base="xs:string"> + <xs:pattern value="[A-Z]{3,3}"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="AddressType2Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="ADDR"/> + <xs:enumeration value="PBOX"/> + <xs:enumeration value="HOME"/> + <xs:enumeration value="BIZZ"/> + <xs:enumeration value="MLTO"/> + <xs:enumeration value="DLVY"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="AmountType3Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="InstdAmt" type="ActiveOrHistoricCurrencyAndAmount"/> + <xs:element name="EqvtAmt" type="EquivalentAmount2"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="AnyBICIdentifier"> + <xs:restriction base="xs:string"> + <xs:pattern value="[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: unused: type Authorisation1Choice is not allowed or used in CH Version + <xs:complexType name="Authorisation1Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="Authorisation1Code"/> + <xs:element name="Prtry" type="Max128Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="Authorisation1Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="AUTH"/> + <xs:enumeration value="FDET"/> + <xs:enumeration value="FSUM"/> + <xs:enumeration value="ILEV"/> + </xs:restriction> + </xs:simpleType> + --> + <!-- V01: added: CH version supports only this character set. All text fields use this type --> + <xs:simpleType name="BasicText-CH"> + <xs:restriction base="xs:string"> + <xs:pattern value="([a-zA-Z0-9\.,;:'\+\-/\(\)?\*\[\]\{\}\\`´~ ]|[!&quot;#%&amp;&lt;&gt;÷=@_$£]|[àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ])*"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: added: This is the SWIFT character set --> + <xs:simpleType name="BasicText-Swift"> + <xs:restriction base="xs:string"> + <xs:pattern value="([A-Za-z0-9]|[+|\?|/|\-|:|\(|\)|\.|,|'|\p{Zs}])*"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="BICIdentifier"> + <xs:restriction base="xs:string"> + <xs:pattern value="[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="BaseOneRate"> + <xs:restriction base="xs:decimal"> + <xs:fractionDigits value="10"/> + <xs:totalDigits value="11"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="BatchBookingIndicator"> + <xs:restriction base="xs:boolean"/> + </xs:simpleType> + <xs:complexType name="BranchAndFinancialInstitutionIdentification4"> + <xs:sequence> + <xs:element name="FinInstnId" type="FinancialInstitutionIdentification7"/> + <xs:element name="BrnchId" type="BranchData2" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <!-- VO1: added: definition of FI where only BIC or Clearing Id is allowed, but no branch data --> + <xs:complexType name="BranchAndFinancialInstitutionIdentification4-CH_BicOrClrId"> + <xs:sequence> + <xs:element name="FinInstnId" type="FinancialInstitutionIdentification7-CH_BicOrClrId"/> + <!-- V01: unused + <xs:element name="BrnchId" type="BranchData2" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <!-- VO1: added: definition of FI where all elements are allowed, but no branch data --> + <xs:complexType name="BranchAndFinancialInstitutionIdentification4-CH"> + <xs:sequence> + <xs:element name="FinInstnId" type="FinancialInstitutionIdentification7-CH"/> + <!-- V01: unused + <xs:element name="BrnchId" type="BranchData2" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <xs:complexType name="BranchData2"> + <xs:sequence> + <xs:element name="Id" type="Max35Text" minOccurs="0"/> + <xs:element name="Nm" type="Max140Text" minOccurs="0"/> + <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <!-- V01: changed: CH version changes applied --> + <xs:complexType name="CashAccount16-CH_IdAndCurrency"> + <xs:sequence> + <xs:element name="Id" type="AccountIdentification4Choice-CH"/> + <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> + <!-- V01: unused + <xs:element name="Tp" type="CashAccountType2" minOccurs="0"/> + <xs:element name="Nm" type="Max70Text" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <!-- V01: added --> + <xs:complexType name="CashAccount16-CH_IdTpCcy"> + <xs:sequence> + <xs:element name="Id" type="AccountIdentification4Choice-CH"/> + <xs:element name="Tp" type="CashAccountType2" minOccurs="0"/> + <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> + <!-- V01: unused + <xs:element name="Nm" type="Max70Text" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <!-- V01: added --> + <xs:complexType name="CashAccount16-CH_Id"> + <xs:sequence> + <xs:element name="Id" type="AccountIdentification4Choice-CH"/> + <!-- V01: unused + <xs:element name="Tp" type="CashAccountType2" minOccurs="0"/> + <xs:element name="Ccy" type="ActiveOrHistoricCurrencyCode" minOccurs="0"/> + <xs:element name="Nm" type="Max70Text" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <xs:complexType name="CashAccountType2"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="CashAccountType4Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="CashAccountType4Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="CASH"/> + <xs:enumeration value="CHAR"/> + <xs:enumeration value="COMM"/> + <xs:enumeration value="TAXE"/> + <xs:enumeration value="CISH"/> + <xs:enumeration value="TRAS"/> + <xs:enumeration value="SACC"/> + <xs:enumeration value="CACC"/> + <xs:enumeration value="SVGS"/> + <xs:enumeration value="ONDP"/> + <xs:enumeration value="MGLD"/> + <xs:enumeration value="NREX"/> + <xs:enumeration value="MOMA"/> + <xs:enumeration value="LOAN"/> + <xs:enumeration value="SLRY"/> + <xs:enumeration value="ODFT"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: changed: Only element Code allowed in Ch version --> + <xs:complexType name="CategoryPurpose1-CH_Code"> + <xs:sequence> + <xs:element name="Cd" type="ExternalCategoryPurpose1Code"/> + <!-- V01: unused + <xs:element name="Prtry" type="Max35Text"/> + --> + </xs:sequence> + </xs:complexType> + <!-- --> + <xs:simpleType name="ChargeBearerType1Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="DEBT"/> + <xs:enumeration value="CRED"/> + <xs:enumeration value="SHAR"/> + <xs:enumeration value="SLEV"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: unused + <xs:complexType name="Cheque6"> + <xs:sequence> + <xs:element name="ChqTp" type="ChequeType2Code" minOccurs="0"/> + <xs:element name="ChqNb" type="Max35Text" minOccurs="0"/> + <xs:element name="ChqFr" type="NameAndAddress10" minOccurs="0"/> + <xs:element name="DlvryMtd" type="ChequeDeliveryMethod1Choice" minOccurs="0"/> + <xs:element name="DlvrTo" type="NameAndAddress10" minOccurs="0"/> + <xs:element name="InstrPrty" type="Priority2Code" minOccurs="0"/> + <xs:element name="ChqMtrtyDt" type="ISODate" minOccurs="0"/> + <xs:element name="FrmsCd" type="Max35Text" minOccurs="0"/> + <xs:element name="MemoFld" type="Max35Text" minOccurs="0" maxOccurs="2"/> + <xs:element name="RgnlClrZone" type="Max35Text" minOccurs="0"/> + <xs:element name="PrtLctn" type="Max35Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + --> + <!-- V01: added --> + <xs:complexType name="Cheque6-CH"> + <xs:sequence> + <xs:element name="ChqTp" type="ChequeType2Code" minOccurs="0"/> + <xs:element name="DlvryMtd" type="ChequeDeliveryMethod1Choice" minOccurs="0"/> + <!-- V01: unused + <xs:element name="ChqNb" type="Max35Text" minOccurs="0"/> + <xs:element name="ChqFr" type="NameAndAddress10" minOccurs="0"/> + <xs:element name="DlvrTo" type="NameAndAddress10" minOccurs="0"/> + <xs:element name="InstrPrty" type="Priority2Code" minOccurs="0"/> + <xs:element name="ChqMtrtyDt" type="ISODate" minOccurs="0"/> + <xs:element name="FrmsCd" type="Max35Text" minOccurs="0"/> + <xs:element name="MemoFld" type="Max35Text" minOccurs="0" maxOccurs="2"/> + <xs:element name="RgnlClrZone" type="Max35Text" minOccurs="0"/> + <xs:element name="PrtLctn" type="Max35Text" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="ChequeDelivery1Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="MLDB"/> + <xs:enumeration value="MLCD"/> + <xs:enumeration value="MLFA"/> + <xs:enumeration value="CRDB"/> + <xs:enumeration value="CRCD"/> + <xs:enumeration value="CRFA"/> + <xs:enumeration value="PUDB"/> + <xs:enumeration value="PUCD"/> + <xs:enumeration value="PUFA"/> + <xs:enumeration value="RGDB"/> + <xs:enumeration value="RGCD"/> + <xs:enumeration value="RGFA"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="ChequeDeliveryMethod1Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="ChequeDelivery1Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="ChequeType2Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="CCHQ"/> + <xs:enumeration value="CCCH"/> + <xs:enumeration value="BCHQ"/> + <xs:enumeration value="DRFT"/> + <xs:enumeration value="ELDR"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="ClearingSystemIdentification2Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="ExternalClearingSystemIdentification1Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:complexType name="ClearingSystemMemberIdentification2"> + <xs:sequence> + <xs:element name="ClrSysId" type="ClearingSystemIdentification2Choice" minOccurs="0"/> + <xs:element name="MmbId" type="Max35Text"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="ContactDetails2"> + <xs:sequence> + <xs:element name="NmPrfx" type="NamePrefix1Code" minOccurs="0"/> + <xs:element name="Nm" type="Max140Text" minOccurs="0"/> + <xs:element name="PhneNb" type="PhoneNumber" minOccurs="0"/> + <xs:element name="MobNb" type="PhoneNumber" minOccurs="0"/> + <xs:element name="FaxNb" type="PhoneNumber" minOccurs="0"/> + <xs:element name="EmailAdr" type="Max2048Text" minOccurs="0"/> + <xs:element name="Othr" type="Max35Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <!-- V02: changed: include Contact Details for Software name and version --> + <xs:complexType name="ContactDetails2-CH"> + <xs:sequence> + <xs:element name="Nm" type="Max70Text" minOccurs="0"/> + <xs:element name="Othr" type="Max35Text" minOccurs="0"/> + <!-- V02: unused + <xs:element name="NmPrfx" type="NamePrefix1Code" minOccurs="0"/> + <xs:element name="PhneNb" type="PhoneNumber" minOccurs="0"/> + <xs:element name="MobNb" type="PhoneNumber" minOccurs="0"/> + <xs:element name="FaxNb" type="PhoneNumber" minOccurs="0"/> + <xs:element name="EmailAdr" type="Max2048Text" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="CountryCode"> + <xs:restriction base="xs:string"> + <xs:pattern value="[A-Z]{2,2}"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="CreditDebitCode"> + <xs:restriction base="xs:string"> + <xs:enumeration value="CRDT"/> + <xs:enumeration value="DBIT"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="CreditTransferTransactionInformation10-CH"> + <xs:sequence> + <xs:element name="PmtId" type="PaymentIdentification1"/> + <xs:element name="PmtTpInf" type="PaymentTypeInformation19-CH" minOccurs="0"/> + <xs:element name="Amt" type="AmountType3Choice"/> + <xs:element name="XchgRateInf" type="ExchangeRateInformation1" minOccurs="0"/> + <xs:element name="ChrgBr" type="ChargeBearerType1Code" minOccurs="0"/> + <xs:element name="ChqInstr" type="Cheque6-CH" minOccurs="0"/> + <xs:element name="UltmtDbtr" type="PartyIdentification32-CH" minOccurs="0"/> + <xs:element name="IntrmyAgt1" type="BranchAndFinancialInstitutionIdentification4-CH" minOccurs="0"/> + <xs:element name="CdtrAgt" type="BranchAndFinancialInstitutionIdentification4-CH" minOccurs="0"/> + <!-- V02: changed: element Name mandatory --> + <xs:element name="Cdtr" type="PartyIdentification32-CH_Name" minOccurs="0"/> + <xs:element name="CdtrAcct" type="CashAccount16-CH_Id" minOccurs="0"/> + <!-- V02: changed: element Name mandatory --> + <xs:element name="UltmtCdtr" type="PartyIdentification32-CH_Name" minOccurs="0"/> + <xs:element name="InstrForCdtrAgt" type="InstructionForCreditorAgent1" minOccurs="0" maxOccurs="unbounded"/> + <xs:element name="InstrForDbtrAgt" type="Max140Text" minOccurs="0"/> + <xs:element name="Purp" type="Purpose2-CH_Code" minOccurs="0"/> + <xs:element name="RgltryRptg" type="RegulatoryReporting3" minOccurs="0" maxOccurs="10"/> + <xs:element name="RmtInf" type="RemittanceInformation5-CH" minOccurs="0"/> + <!-- V01: usused + <xs:element name="IntrmyAgt1Acct" type="CashAccount16" minOccurs="0"/> + <xs:element name="IntrmyAgt2" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> + <xs:element name="IntrmyAgt2Acct" type="CashAccount16" minOccurs="0"/> + <xs:element name="IntrmyAgt3" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> + <xs:element name="IntrmyAgt3Acct" type="CashAccount16" minOccurs="0"/> + <xs:element name="CdtrAgtAcct" type="CashAccount16-CH_Id" minOccurs="0"/> + <xs:element name="Tax" type="TaxInformation3" minOccurs="0"/> + <xs:element name="RltdRmtInf" type="RemittanceLocation2" minOccurs="0" maxOccurs="10"/> + --> + </xs:sequence> + </xs:complexType> + <xs:complexType name="CreditorReferenceInformation2"> + <xs:sequence> + <xs:element name="Tp" type="CreditorReferenceType2" minOccurs="0"/> + <xs:element name="Ref" type="Max35Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="CreditorReferenceType1Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="DocumentType3Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:complexType name="CreditorReferenceType2"> + <xs:sequence> + <xs:element name="CdOrPrtry" type="CreditorReferenceType1Choice"/> + <xs:element name="Issr" type="Max35Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="CustomerCreditTransferInitiationV03-CH"> + <xs:sequence> + <xs:element name="GrpHdr" type="GroupHeader32-CH"/> + <xs:element name="PmtInf" type="PaymentInstructionInformation3-CH" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="DateAndPlaceOfBirth"> + <xs:sequence> + <xs:element name="BirthDt" type="ISODate"/> + <xs:element name="PrvcOfBirth" type="Max35Text" minOccurs="0"/> + <xs:element name="CityOfBirth" type="Max35Text"/> + <xs:element name="CtryOfBirth" type="CountryCode"/> + </xs:sequence> + </xs:complexType> + <!-- V01: unused + <xs:complexType name="DatePeriodDetails"> + <xs:sequence> + <xs:element name="FrDt" type="ISODate"/> + <xs:element name="ToDt" type="ISODate"/> + </xs:sequence> + </xs:complexType> + --> + <xs:simpleType name="DecimalNumber"> + <xs:restriction base="xs:decimal"> + <xs:fractionDigits value="17"/> + <xs:totalDigits value="18"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="Document"> + <xs:sequence> + <xs:element name="CstmrCdtTrfInitn" type="CustomerCreditTransferInitiationV03-CH"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="DocumentAdjustment1"> + <xs:sequence> + <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> + <xs:element name="CdtDbtInd" type="CreditDebitCode" minOccurs="0"/> + <xs:element name="Rsn" type="Max4Text" minOccurs="0"/> + <xs:element name="AddtlInf" type="Max140Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="DocumentType3Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="RADM"/> + <xs:enumeration value="RPIN"/> + <xs:enumeration value="FXDR"/> + <xs:enumeration value="DISP"/> + <xs:enumeration value="PUOR"/> + <xs:enumeration value="SCOR"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="DocumentType5Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="MSIN"/> + <xs:enumeration value="CNFA"/> + <xs:enumeration value="DNFA"/> + <xs:enumeration value="CINV"/> + <xs:enumeration value="CREN"/> + <xs:enumeration value="DEBN"/> + <xs:enumeration value="HIRI"/> + <xs:enumeration value="SBIN"/> + <xs:enumeration value="CMCN"/> + <xs:enumeration value="SOAC"/> + <xs:enumeration value="DISP"/> + <xs:enumeration value="BOLD"/> + <xs:enumeration value="VCHR"/> + <xs:enumeration value="AROI"/> + <xs:enumeration value="TSUT"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="EquivalentAmount2"> + <xs:sequence> + <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> + <xs:element name="CcyOfTrf" type="ActiveOrHistoricCurrencyCode"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="ExchangeRateInformation1"> + <xs:sequence> + <xs:element name="XchgRate" type="BaseOneRate" minOccurs="0"/> + <xs:element name="RateTp" type="ExchangeRateType1Code" minOccurs="0"/> + <xs:element name="CtrctId" type="Max35Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="ExchangeRateType1Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="SPOT"/> + <xs:enumeration value="SALE"/> + <xs:enumeration value="AGRD"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: unused: + <xs:simpleType name="ExternalAccountIdentification1Code"> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="4"/> + </xs:restriction> + </xs:simpleType> + --> + <xs:simpleType name="ExternalCategoryPurpose1Code"> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="4"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="ExternalClearingSystemIdentification1Code"> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="5"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="ExternalFinancialInstitutionIdentification1Code"> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="4"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="ExternalLocalInstrument1Code"> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="35"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="ExternalOrganisationIdentification1Code"> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="4"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="ExternalPersonIdentification1Code"> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="4"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="ExternalPurpose1Code"> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="4"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="ExternalServiceLevel1Code"> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + <xs:maxLength value="4"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="FinancialIdentificationSchemeName1Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="ExternalFinancialInstitutionIdentification1Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:complexType name="FinancialInstitutionIdentification7"> + <xs:sequence> + <xs:element name="BIC" type="BICIdentifier" minOccurs="0"/> + <xs:element name="ClrSysMmbId" type="ClearingSystemMemberIdentification2" minOccurs="0"/> + <xs:element name="Nm" type="Max140Text" minOccurs="0"/> + <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> + <xs:element name="Othr" type="GenericFinancialIdentification1" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <!-- V01: added: definition of FI where only BIC or Clearing Id is allowed --> + <xs:complexType name="FinancialInstitutionIdentification7-CH_BicOrClrId"> + <xs:sequence> + <xs:element name="BIC" type="BICIdentifier" minOccurs="0"/> + <xs:element name="ClrSysMmbId" type="ClearingSystemMemberIdentification2" minOccurs="0"/> + <!-- V01: unused + <xs:element name="Nm" type="Max140Text" minOccurs="0"/> + <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> + <xs:element name="Othr" type="GenericFinancialIdentification1" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <!-- V01: added: definition of FI where all elements are allowed (in a CH version) --> + <xs:complexType name="FinancialInstitutionIdentification7-CH"> + <xs:sequence> + <xs:element name="BIC" type="BICIdentifier" minOccurs="0"/> + <xs:element name="ClrSysMmbId" type="ClearingSystemMemberIdentification2" minOccurs="0"/> + <xs:element name="Nm" type="Max70Text" minOccurs="0"/> + <xs:element name="PstlAdr" type="PostalAddress6-CH" minOccurs="0"/> + <xs:element name="Othr" type="GenericFinancialIdentification1-CH" minOccurs="0"/> + <!-- V01: unused + --> + </xs:sequence> + </xs:complexType> + <!-- V01: changed: only element ID allowed in CH version --> + <xs:complexType name="GenericAccountIdentification1-CH"> + <xs:sequence> + <xs:element name="Id" type="Max34Text"/> + <!-- V01: unused + <xs:element name="SchmeNm" type="AccountSchemeName1Choice" minOccurs="0"/> + <xs:element name="Issr" type="Max35Text" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <xs:complexType name="GenericFinancialIdentification1"> + <xs:sequence> + <xs:element name="Id" type="Max35Text"/> + <xs:element name="SchmeNm" type="FinancialIdentificationSchemeName1Choice" minOccurs="0"/> + <xs:element name="Issr" type="Max35Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <!-- V01: added: only element Id allowed in CH version --> + <xs:complexType name="GenericFinancialIdentification1-CH"> + <xs:sequence> + <xs:element name="Id" type="Max35Text"/> + <!-- V01: unused + <xs:element name="SchmeNm" type="FinancialIdentificationSchemeName1Choice" minOccurs="0"/> + <xs:element name="Issr" type="Max35Text" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <xs:complexType name="GenericOrganisationIdentification1"> + <xs:sequence> + <xs:element name="Id" type="Max35Text"/> + <xs:element name="SchmeNm" type="OrganisationIdentificationSchemeName1Choice" minOccurs="0"/> + <xs:element name="Issr" type="Max35Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="GenericPersonIdentification1"> + <xs:sequence> + <xs:element name="Id" type="Max35Text"/> + <xs:element name="SchmeNm" type="PersonIdentificationSchemeName1Choice" minOccurs="0"/> + <xs:element name="Issr" type="Max35Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="GroupHeader32-CH"> + <xs:sequence> + <xs:element name="MsgId" type="Max35Text-Swift"/> + <xs:element name="CreDtTm" type="ISODateTime"/> + <xs:element name="NbOfTxs" type="Max15NumericText"/> + <xs:element name="CtrlSum" type="DecimalNumber" minOccurs="0"/> + <!-- V02: changed: include Contact Details for Software name and version --> + <xs:element name="InitgPty" type="PartyIdentification32-CH_NameAndId"/> + <xs:element name="FwdgAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> + <!-- V01: unused: type Authorisation1Choice is not allowed or used in CH Version + <xs:element name="Authstn" type="Authorisation1Choice" minOccurs="0" maxOccurs="2"/> + --> + <!-- V01: changed: Initiating party only to contain name and id in CH version + <xs:element name="InitgPty" type="PartyIdentification32"/> + --> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="IBAN2007Identifier"> + <xs:restriction base="xs:string"> + <xs:pattern value="[A-Z]{2,2}[0-9]{2,2}[a-zA-Z0-9]{1,30}"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="ISODate"> + <xs:restriction base="xs:date"/> + </xs:simpleType> + <xs:simpleType name="ISODateTime"> + <xs:restriction base="xs:dateTime"/> + </xs:simpleType> + <xs:simpleType name="Instruction3Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="CHQB"/> + <xs:enumeration value="HOLD"/> + <xs:enumeration value="PHOB"/> + <xs:enumeration value="TELB"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="InstructionForCreditorAgent1"> + <xs:sequence> + <xs:element name="Cd" type="Instruction3Code" minOccurs="0"/> + <xs:element name="InstrInf" type="Max140Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="LocalInstrument2Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="ExternalLocalInstrument1Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="Max10Text"> + <xs:restriction base="BasicText-CH"> + <xs:minLength value="1"/> + <xs:maxLength value="10"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: unused + <xs:simpleType name="Max128Text"> + <xs:restriction base="BasicText-CH"> + <xs:minLength value="1"/> + <xs:maxLength value="128"/> + </xs:restriction> + </xs:simpleType> + --> + <xs:simpleType name="Max140Text"> + <xs:restriction base="BasicText-CH"> + <xs:minLength value="1"/> + <xs:maxLength value="140"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="Max15NumericText"> + <xs:restriction base="xs:string"> + <xs:pattern value="[0-9]{1,15}"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="Max16Text"> + <xs:restriction base="BasicText-CH"> + <xs:minLength value="1"/> + <xs:maxLength value="16"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="Max2048Text"> + <xs:restriction base="BasicText-CH"> + <xs:minLength value="1"/> + <xs:maxLength value="2048"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="Max34Text"> + <xs:restriction base="BasicText-CH"> + <xs:minLength value="1"/> + <xs:maxLength value="34"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="Max35Text"> + <xs:restriction base="BasicText-CH"> + <xs:minLength value="1"/> + <xs:maxLength value="35"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: added: replacement type for Max35Text where only the Swift character set is allowed --> + <xs:simpleType name="Max35Text-Swift"> + <xs:restriction base="BasicText-Swift"> + <xs:minLength value="1"/> + <xs:maxLength value="35"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="Max4Text"> + <xs:restriction base="BasicText-CH"> + <xs:minLength value="1"/> + <xs:maxLength value="4"/> + </xs:restriction> + </xs:simpleType> + <xs:simpleType name="Max70Text"> + <xs:restriction base="BasicText-CH"> + <xs:minLength value="1"/> + <xs:maxLength value="70"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: unused + <xs:complexType name="NameAndAddress10"> + <xs:sequence> + <xs:element name="Nm" type="Max140Text"/> + <xs:element name="Adr" type="PostalAddress6"/> + </xs:sequence> + </xs:complexType> + --> + <!-- V01: added: CH-Version: unused (prepared for later usage) + <xs:complexType name="NameAndAddress10-CH"> + <xs:sequence> + <xs:element name="Nm" type="Max70Text"/> + <xs:element name="Adr" type="PostalAddress6-CH"/> + </xs:sequence> + </xs:complexType> + --> + <xs:simpleType name="NamePrefix1Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="DOCT"/> + <xs:enumeration value="MIST"/> + <xs:enumeration value="MISS"/> + <xs:enumeration value="MADM"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: unused + <xs:simpleType name="Number"> + <xs:restriction base="xs:decimal"> + <xs:fractionDigits value="0"/> + <xs:totalDigits value="18"/> + </xs:restriction> + </xs:simpleType> + --> + <xs:complexType name="OrganisationIdentification4"> + <xs:sequence> + <xs:element name="BICOrBEI" type="AnyBICIdentifier" minOccurs="0"/> + <xs:element name="Othr" type="GenericOrganisationIdentification1" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + <!-- V01: added: only one occurance of element other allowed in CH version --> + <xs:complexType name="OrganisationIdentification4-CH"> + <xs:sequence> + <xs:element name="BICOrBEI" type="AnyBICIdentifier" minOccurs="0"/> + <xs:element name="Othr" type="GenericOrganisationIdentification1" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="OrganisationIdentificationSchemeName1Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="ExternalOrganisationIdentification1Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:complexType name="Party6Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="OrgId" type="OrganisationIdentification4"/> + <xs:element name="PrvtId" type="PersonIdentification5"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <!-- V01: added: --> + <xs:complexType name="Party6Choice-CH"> + <xs:sequence> + <xs:choice> + <xs:element name="OrgId" type="OrganisationIdentification4-CH"/> + <xs:element name="PrvtId" type="PersonIdentification5-CH"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:complexType name="PartyIdentification32"> + <xs:sequence> + <xs:element name="Nm" type="Max140Text" minOccurs="0"/> + <xs:element name="PstlAdr" type="PostalAddress6" minOccurs="0"/> + <xs:element name="Id" type="Party6Choice" minOccurs="0"/> + <xs:element name="CtryOfRes" type="CountryCode" minOccurs="0"/> + <xs:element name="CtctDtls" type="ContactDetails2" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <!-- V01: added: replacement type for PartyIdentification8 where only elements Name and Id may be used --> + <xs:complexType name="PartyIdentification32-CH_NameAndId"> + <xs:sequence> + <xs:element name="Nm" type="Max70Text" minOccurs="0"/> + <xs:element name="Id" type="Party6Choice-CH" minOccurs="0"/> + <!-- V02: added: Contact Details for Software name and version --> + <xs:element name="CtctDtls" type="ContactDetails2-CH" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <!-- V01: added --> + <xs:complexType name="PartyIdentification32-CH"> + <xs:sequence> + <xs:element name="Nm" type="Max70Text" minOccurs="0"/> + <xs:element name="PstlAdr" type="PostalAddress6-CH" minOccurs="0"/> + <xs:element name="Id" type="Party6Choice-CH" minOccurs="0"/> + <!-- changed --> + <!-- unused + <xs:element name="CtryOfRes" type="CountryCode" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <!-- V02: changed: element Name mandatory --> + <xs:complexType name="PartyIdentification32-CH_Name"> + <xs:sequence> + <xs:element name="Nm" type="Max70Text"/> + <xs:element name="PstlAdr" type="PostalAddress6-CH" minOccurs="0"/> + <xs:element name="Id" type="Party6Choice-CH" minOccurs="0"/> + <!-- changed --> + <!-- unused + <xs:element name="CtryOfRes" type="CountryCode" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <!-- + <xs:complexType name="PartyIdentification32-CH_Debtor"> + <xs:sequence> + <xs:element name="Nm" type="Max70Text" minOccurs="0"/> + <xs:element name="PstlAdr" type="PostalAddress6-CH" minOccurs="0"/> + <xs:element name="Id" type="Party6Choice-CH" minOccurs="0"/> + + <xs:element name="CtryOfRes" type="CountryCode" minOccurs="0"/> + <xs:element name="CtctDtls" type="ContactDetails2" minOccurs="0"/> + + </xs:sequence> + </xs:complexType> + + <xs:complexType name="PartyIdentification32-CH_Creditor"> + <xs:sequence> + <xs:element name="Nm" type="Max70Text" minOccurs="0"/> + <xs:element name="PstlAdr" type="PostalAddress6-CH" minOccurs="0"/> + <xs:element name="Id" type="Party6Choice-CH" minOccurs="0"/> + + <xs:element name="CtryOfRes" type="CountryCode" minOccurs="0"/> + <xs:element name="CtctDtls" type="ContactDetails2" minOccurs="0"/> + + </xs:sequence> + </xs:complexType> + --> + <xs:complexType name="PaymentIdentification1"> + <xs:sequence> + <xs:element name="InstrId" type="Max35Text-Swift" minOccurs="0"/> + <xs:element name="EndToEndId" type="Max35Text-Swift"/> + </xs:sequence> + </xs:complexType> + <!-- V01: changed: CH-version changes applied --> + <xs:complexType name="PaymentInstructionInformation3-CH"> + <xs:sequence> + <xs:element name="PmtInfId" type="Max35Text-Swift"/> + <xs:element name="PmtMtd" type="PaymentMethod3Code"/> + <xs:element name="BtchBookg" type="BatchBookingIndicator" minOccurs="0"/> + <xs:element name="NbOfTxs" type="Max15NumericText" minOccurs="0"/> + <xs:element name="CtrlSum" type="DecimalNumber" minOccurs="0"/> + <xs:element name="PmtTpInf" type="PaymentTypeInformation19-CH" minOccurs="0"/> + <xs:element name="ReqdExctnDt" type="ISODate"/> + <xs:element name="Dbtr" type="PartyIdentification32-CH"/> + <xs:element name="DbtrAcct" type="CashAccount16-CH_IdTpCcy"/> + <xs:element name="DbtrAgt" type="BranchAndFinancialInstitutionIdentification4-CH_BicOrClrId"/> + <xs:element name="UltmtDbtr" type="PartyIdentification32-CH" minOccurs="0"/> + <xs:element name="ChrgBr" type="ChargeBearerType1Code" minOccurs="0"/> + <xs:element name="ChrgsAcct" type="CashAccount16-CH_IdAndCurrency" minOccurs="0"/> + <xs:element name="CdtTrfTxInf" type="CreditTransferTransactionInformation10-CH" maxOccurs="unbounded"/> + <!-- V01: unused + <xs:element name="PoolgAdjstmntDt" type="ISODate" minOccurs="0"/> + <xs:element name="DbtrAgtAcct" type="CashAccount16" minOccurs="0"/> + <xs:element name="ChrgsAcctAgt" type="BranchAndFinancialInstitutionIdentification4" minOccurs="0"/> + --> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="PaymentMethod3Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="CHK"/> + <xs:enumeration value="TRF"/> + <xs:enumeration value="TRA"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: changed: CH version changes applied --> + <xs:complexType name="PaymentTypeInformation19-CH"> + <xs:sequence> + <xs:element name="InstrPrty" type="Priority2Code" minOccurs="0"/> + <xs:element name="SvcLvl" type="ServiceLevel8Choice" minOccurs="0"/> + <xs:element name="LclInstrm" type="LocalInstrument2Choice" minOccurs="0"/> + <xs:element name="CtgyPurp" type="CategoryPurpose1-CH_Code" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <!-- V01: unused + <xs:simpleType name="PercentageRate"> + <xs:restriction base="xs:decimal"> + <xs:fractionDigits value="10"/> + <xs:totalDigits value="11"/> + </xs:restriction> + </xs:simpleType> + --> + <xs:complexType name="PersonIdentification5"> + <xs:sequence> + <xs:element name="DtAndPlcOfBirth" type="DateAndPlaceOfBirth" minOccurs="0"/> + <xs:element name="Othr" type="GenericPersonIdentification1" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + <!-- V01: changed: only one occurance of element Othr allowed in CH version --> + <xs:complexType name="PersonIdentification5-CH"> + <xs:sequence> + <xs:element name="DtAndPlcOfBirth" type="DateAndPlaceOfBirth" minOccurs="0"/> + <xs:element name="Othr" type="GenericPersonIdentification1" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="PersonIdentificationSchemeName1Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="ExternalPersonIdentification1Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="PhoneNumber"> + <xs:restriction base="xs:string"> + <xs:pattern value="\+[0-9]{1,3}-[0-9()+\-]{1,30}"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="PostalAddress6"> + <xs:sequence> + <xs:element name="AdrTp" type="AddressType2Code" minOccurs="0"/> + <xs:element name="Dept" type="Max70Text" minOccurs="0"/> + <xs:element name="SubDept" type="Max70Text" minOccurs="0"/> + <xs:element name="StrtNm" type="Max70Text" minOccurs="0"/> + <xs:element name="BldgNb" type="Max16Text" minOccurs="0"/> + <xs:element name="PstCd" type="Max16Text" minOccurs="0"/> + <xs:element name="TwnNm" type="Max35Text" minOccurs="0"/> + <xs:element name="CtrySubDvsn" type="Max35Text" minOccurs="0"/> + <xs:element name="Ctry" type="CountryCode" minOccurs="0"/> + <xs:element name="AdrLine" type="Max70Text" minOccurs="0" maxOccurs="7"/> + </xs:sequence> + </xs:complexType> + <!-- V01: added: only 2 lines of address lines allowed in CH version --> + <xs:complexType name="PostalAddress6-CH"> + <xs:sequence> + <xs:element name="AdrTp" type="AddressType2Code" minOccurs="0"/> + <xs:element name="Dept" type="Max70Text" minOccurs="0"/> + <xs:element name="SubDept" type="Max70Text" minOccurs="0"/> + <xs:element name="StrtNm" type="Max70Text" minOccurs="0"/> + <xs:element name="BldgNb" type="Max16Text" minOccurs="0"/> + <xs:element name="PstCd" type="Max16Text" minOccurs="0"/> + <xs:element name="TwnNm" type="Max35Text" minOccurs="0"/> + <xs:element name="CtrySubDvsn" type="Max35Text" minOccurs="0"/> + <xs:element name="Ctry" type="CountryCode" minOccurs="0"/> + <xs:element name="AdrLine" type="Max70Text" minOccurs="0" maxOccurs="2"/> + <!-- V01: changed: max. 2 occurence --> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="Priority2Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="HIGH"/> + <xs:enumeration value="NORM"/> + </xs:restriction> + </xs:simpleType> + <!-- V01: changed: CH-version changes applied --> + <xs:complexType name="Purpose2-CH_Code"> + <xs:sequence> + <xs:element name="Cd" type="ExternalPurpose1Code"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="ReferredDocumentInformation3"> + <xs:sequence> + <xs:element name="Tp" type="ReferredDocumentType2" minOccurs="0"/> + <xs:element name="Nb" type="Max35Text" minOccurs="0"/> + <xs:element name="RltdDt" type="ISODate" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="ReferredDocumentType1Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="DocumentType5Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:complexType name="ReferredDocumentType2"> + <xs:sequence> + <xs:element name="CdOrPrtry" type="ReferredDocumentType1Choice"/> + <xs:element name="Issr" type="Max35Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="RegulatoryAuthority2"> + <xs:sequence> + <xs:element name="Nm" type="Max140Text" minOccurs="0"/> + <xs:element name="Ctry" type="CountryCode" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="RegulatoryReporting3"> + <xs:sequence> + <xs:element name="DbtCdtRptgInd" type="RegulatoryReportingType1Code" minOccurs="0"/> + <xs:element name="Authrty" type="RegulatoryAuthority2" minOccurs="0"/> + <xs:element name="Dtls" type="StructuredRegulatoryReporting3" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="RegulatoryReportingType1Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="CRED"/> + <xs:enumeration value="DEBT"/> + <xs:enumeration value="BOTH"/> + </xs:restriction> + </xs:simpleType> + <xs:complexType name="RemittanceAmount1"> + <xs:sequence> + <xs:element name="DuePyblAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> + <xs:element name="DscntApldAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> + <xs:element name="CdtNoteAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> + <xs:element name="TaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> + <xs:element name="AdjstmntAmtAndRsn" type="DocumentAdjustment1" minOccurs="0" maxOccurs="unbounded"/> + <xs:element name="RmtdAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="RemittanceInformation5-CH"> + <xs:sequence> + <xs:element name="Ustrd" type="Max140Text" minOccurs="0"/> + <xs:element name="Strd" type="StructuredRemittanceInformation7" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <!-- V01: unused + <xs:complexType name="RemittanceLocation2"> + <xs:sequence> + <xs:element name="RmtId" type="Max35Text" minOccurs="0"/> + <xs:element name="RmtLctnMtd" type="RemittanceLocationMethod2Code" minOccurs="0"/> + <xs:element name="RmtLctnElctrncAdr" type="Max2048Text" minOccurs="0"/> + <xs:element name="RmtLctnPstlAdr" type="NameAndAddress10" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="RemittanceLocationMethod2Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="FAXI"/> + <xs:enumeration value="EDIC"/> + <xs:enumeration value="URID"/> + <xs:enumeration value="EMAL"/> + <xs:enumeration value="POST"/> + <xs:enumeration value="SMSM"/> + </xs:restriction> + </xs:simpleType> + --> + <xs:complexType name="ServiceLevel8Choice"> + <xs:sequence> + <xs:choice> + <xs:element name="Cd" type="ExternalServiceLevel1Code"/> + <xs:element name="Prtry" type="Max35Text"/> + </xs:choice> + </xs:sequence> + </xs:complexType> + <xs:complexType name="StructuredRegulatoryReporting3"> + <xs:sequence> + <xs:element name="Tp" type="Max35Text" minOccurs="0"/> + <xs:element name="Dt" type="ISODate" minOccurs="0"/> + <xs:element name="Ctry" type="CountryCode" minOccurs="0"/> + <xs:element name="Cd" type="Max10Text" minOccurs="0"/> + <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> + <xs:element name="Inf" type="Max35Text" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="StructuredRemittanceInformation7"> + <xs:sequence> + <xs:element name="RfrdDocInf" type="ReferredDocumentInformation3" minOccurs="0" maxOccurs="unbounded"/> + <xs:element name="RfrdDocAmt" type="RemittanceAmount1" minOccurs="0"/> + <xs:element name="CdtrRefInf" type="CreditorReferenceInformation2" minOccurs="0"/> + <xs:element name="Invcr" type="PartyIdentification32" minOccurs="0"/> + <xs:element name="Invcee" type="PartyIdentification32" minOccurs="0"/> + <xs:element name="AddtlRmtInf" type="Max140Text" minOccurs="0" maxOccurs="3"/> + </xs:sequence> + </xs:complexType> + <!-- V01: unused + <xs:complexType name="TaxAmount1"> + <xs:sequence> + <xs:element name="Rate" type="PercentageRate" minOccurs="0"/> + <xs:element name="TaxblBaseAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> + <xs:element name="TtlAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> + <xs:element name="Dtls" type="TaxRecordDetails1" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="TaxAuthorisation1"> + <xs:sequence> + <xs:element name="Titl" type="Max35Text" minOccurs="0"/> + <xs:element name="Nm" type="Max140Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="TaxInformation3"> + <xs:sequence> + <xs:element name="Cdtr" type="TaxParty1" minOccurs="0"/> + <xs:element name="Dbtr" type="TaxParty2" minOccurs="0"/> + <xs:element name="AdmstnZn" type="Max35Text" minOccurs="0"/> + <xs:element name="RefNb" type="Max140Text" minOccurs="0"/> + <xs:element name="Mtd" type="Max35Text" minOccurs="0"/> + <xs:element name="TtlTaxblBaseAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> + <xs:element name="TtlTaxAmt" type="ActiveOrHistoricCurrencyAndAmount" minOccurs="0"/> + <xs:element name="Dt" type="ISODate" minOccurs="0"/> + <xs:element name="SeqNb" type="Number" minOccurs="0"/> + <xs:element name="Rcrd" type="TaxRecord1" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="TaxParty1"> + <xs:sequence> + <xs:element name="TaxId" type="Max35Text" minOccurs="0"/> + <xs:element name="RegnId" type="Max35Text" minOccurs="0"/> + <xs:element name="TaxTp" type="Max35Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="TaxParty2"> + <xs:sequence> + <xs:element name="TaxId" type="Max35Text" minOccurs="0"/> + <xs:element name="RegnId" type="Max35Text" minOccurs="0"/> + <xs:element name="TaxTp" type="Max35Text" minOccurs="0"/> + <xs:element name="Authstn" type="TaxAuthorisation1" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="TaxPeriod1"> + <xs:sequence> + <xs:element name="Yr" type="ISODate" minOccurs="0"/> + <xs:element name="Tp" type="TaxRecordPeriod1Code" minOccurs="0"/> + <xs:element name="FrToDt" type="DatePeriodDetails" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="TaxRecord1"> + <xs:sequence> + <xs:element name="Tp" type="Max35Text" minOccurs="0"/> + <xs:element name="Ctgy" type="Max35Text" minOccurs="0"/> + <xs:element name="CtgyDtls" type="Max35Text" minOccurs="0"/> + <xs:element name="DbtrSts" type="Max35Text" minOccurs="0"/> + <xs:element name="CertId" type="Max35Text" minOccurs="0"/> + <xs:element name="FrmsCd" type="Max35Text" minOccurs="0"/> + <xs:element name="Prd" type="TaxPeriod1" minOccurs="0"/> + <xs:element name="TaxAmt" type="TaxAmount1" minOccurs="0"/> + <xs:element name="AddtlInf" type="Max140Text" minOccurs="0"/> + </xs:sequence> + </xs:complexType> + <xs:complexType name="TaxRecordDetails1"> + <xs:sequence> + <xs:element name="Prd" type="TaxPeriod1" minOccurs="0"/> + <xs:element name="Amt" type="ActiveOrHistoricCurrencyAndAmount"/> + </xs:sequence> + </xs:complexType> + <xs:simpleType name="TaxRecordPeriod1Code"> + <xs:restriction base="xs:string"> + <xs:enumeration value="MM01"/> + <xs:enumeration value="MM02"/> + <xs:enumeration value="MM03"/> + <xs:enumeration value="MM04"/> + <xs:enumeration value="MM05"/> + <xs:enumeration value="MM06"/> + <xs:enumeration value="MM07"/> + <xs:enumeration value="MM08"/> + <xs:enumeration value="MM09"/> + <xs:enumeration value="MM10"/> + <xs:enumeration value="MM11"/> + <xs:enumeration value="MM12"/> + <xs:enumeration value="QTR1"/> + <xs:enumeration value="QTR2"/> + <xs:enumeration value="QTR3"/> + <xs:enumeration value="QTR4"/> + <xs:enumeration value="HLF1"/> + <xs:enumeration value="HLF2"/> + </xs:restriction> + </xs:simpleType> + --> +</xs:schema>