[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] 03/03: Introducing the "pf" dialect.
From: |
gnunet |
Subject: |
[libeufin] 03/03: Introducing the "pf" dialect. |
Date: |
Wed, 17 May 2023 14:23:52 +0200 |
This is an automated email from the git hooks/post-receive script.
ms pushed a commit to branch master
in repository libeufin.
commit 213256b16aab1b1e8ae6fac98f886a14d4537860
Author: MS <ms@taler.net>
AuthorDate: Wed May 17 14:16:11 2023 +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.
---
cli/bin/libeufin-cli | 8 +-
.../main/kotlin/tech/libeufin/nexus/Anastasis.kt | 7 +-
nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 7 +-
nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 47 +-
nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 4 +-
.../tech/libeufin/nexus/bankaccount/BankAccount.kt | 34 +-
.../tech/libeufin/nexus/ebics/EbicsClient.kt | 39 +-
.../kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 220 +++-
.../tech/libeufin/nexus/iso20022/Iso20022.kt | 244 +++-
.../kotlin/tech/libeufin/nexus/server/Helpers.kt | 27 +-
.../main/kotlin/tech/libeufin/nexus/server/JSON.kt | 17 +-
.../tech/libeufin/nexus/server/NexusServer.kt | 52 +-
.../nexus/xlibeufinbank/XLibeufinBankNexus.kt | 1 +
nexus/src/test/kotlin/Iso20022Test.kt | 78 ++
nexus/src/test/kotlin/MakeEnv.kt | 197 +++-
nexus/src/test/kotlin/PostFinance.kt | 100 ++
nexus/src/test/kotlin/TalerTest.kt | 1 +
util/src/main/kotlin/CamtJsonMapping.kt | 59 +-
util/src/main/kotlin/Ebics.kt | 16 +-
util/src/main/kotlin/JSON.kt | 3 +-
util/src/main/kotlin/XMLUtil.kt | 3 +-
util/src/main/kotlin/ebics_h004/EbicsRequest.kt | 6 +-
.../main/resources/xsd/pain.001.001.03.ch.02.xsd | 1212 ++++++++++++++++++++
23 files changed, 2197 insertions(+), 185 deletions(-)
diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli
index ff30d62a..04ac85fd 100755
--- 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
index b75755ef..684229d4 100644
--- 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
index 2f54626b..086d6fe6 100644
--- 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
index 5ba89ad4..a63e6503 100644
--- 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
index 35ff1819..36379094 100644
--- 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
index 9c1c8f9f..a1576a05 100644
--- 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
index 85006012..382aefc8 100644
--- 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
index 04150eaf..e84e9846 100644
--- 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
index 9d7ed3ea..fb65f0c5 100644
--- 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
index 52069270..5e386c84 100644
--- 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
index 312961c2..bb0ece5e 100644
--- 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
index 5b9a0bd7..fca81b51 100644
--- 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
index 22c07e38..a6193a14 100644
---
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
index 18502881..58777cba 100644
--- 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
index daad7447..c64d20e8 100644
--- 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
new file mode 100644
index 00000000..5b93f677
--- /dev/null
+++ 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
index ecaa7a0a..cd8b1776 100644
--- 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
index 06a042a6..fcf992c5 100644
--- 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
index 182a02bf..737039a6 100644
--- 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
index b2ad4a05..2be1f3d0 100644
--- 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
index c20a969a..e9074cb4 100644
--- 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
index c5d053ea..7041f5f2 100644
--- 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
new file mode 100644
index 00000000..b8ecce02
--- /dev/null
+++ 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\.,;:'\+\-/\(\)?\*\[\]\{\}\\`´~
]|[!"#%&<>÷=@_$£]|[àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ])*"/>
+ </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>
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.