diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-06-18 12:43:10 +0530 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-06-18 14:15:38 +0530 |
commit | 0cd1658bc6178a7154937268a66a34a58028492b (patch) | |
tree | a8d603a071b1f5d1daf7b2f50e4dd48a662b7d60 /nexus | |
parent | 12c1a45ce0bb96b874d27629a7ac0ead227ac773 (diff) | |
download | libeufin-0cd1658bc6178a7154937268a66a34a58028492b.tar.gz libeufin-0cd1658bc6178a7154937268a66a34a58028492b.tar.bz2 libeufin-0cd1658bc6178a7154937268a66a34a58028492b.zip |
cleanup
Diffstat (limited to 'nexus')
7 files changed, 305 insertions, 128 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt index 81cea39f..f6315a94 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -168,12 +168,16 @@ class RawBankTransactionEntity(id: EntityID<Long>) : LongEntity(id) { * Represents a prepared payment. */ object InitiatedPaymentsTable : LongIdTable() { + /** + * Bank account that wants to initiate the payment. + */ + val bankAccount = reference("bankAccount", NexusBankAccountsTable) val preparationDate = long("preparationDate") val submissionDate = long("submissionDate").nullable() val sum = amount("sum") val currency = varchar("currency", length = 3).default("EUR") val endToEndId = long("EndToEndId") - val subject = text("subject") + val subject = text("subject") val creditorIban = text("creditorIban") val creditorBic = text("creditorBic") val creditorName = text("creditorName") @@ -181,13 +185,18 @@ object InitiatedPaymentsTable : LongIdTable() { val debitorBic = text("debitorBic") val debitorName = text("debitorName").nullable() val submitted = bool("submitted").default(false) - // points at the raw transaction witnessing that this - // initiated payment was successfully performed. + + /** + * Points at the raw transaction witnessing that this + * initiated payment was successfully performed. + */ val rawConfirmation = reference("rawConfirmation", RawBankTransactionsTable).nullable() } class InitiatedPaymentEntity(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<InitiatedPaymentEntity>(InitiatedPaymentsTable) + + var bankAccount by NexusBankAccountEntity referencedOn InitiatedPaymentsTable.bankAccount var preparationDate by InitiatedPaymentsTable.preparationDate var submissionDate by InitiatedPaymentsTable.submissionDate var sum by InitiatedPaymentsTable.sum @@ -201,7 +210,7 @@ class InitiatedPaymentEntity(id: EntityID<Long>) : LongEntity(id) { var creditorBic by InitiatedPaymentsTable.creditorBic var creditorName by InitiatedPaymentsTable.creditorName var submitted by InitiatedPaymentsTable.submitted - var rawConfirmation by RawBankTransactionEntity optionalReferencedOn InitiatedPaymentsTable.rawConfirmation + var rawConfirmation by RawBankTransactionEntity optionalReferencedOn InitiatedPaymentsTable.rawConfirmation } /** diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt index b9851bb4..177369bc 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt @@ -26,60 +26,11 @@ import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction import org.w3c.dom.Document import tech.libeufin.util.* -import tech.libeufin.util.ebics_h004.EbicsTypes -import java.security.interfaces.RSAPublicKey import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter -import java.util.* -/** - * Skip national only-numeric bank account ids, and return the first IBAN in list - */ -fun extractFirstIban(bankAccounts: List<EbicsTypes.AbstractAccountNumber>?): String? { - if (bankAccounts == null) - return null - - for (item in bankAccounts) { - if (item is EbicsTypes.GeneralAccountNumber) { - if (item.international) - return item.value - } - } - return null -} - - -fun getEbicsSubscriberDetailsInternal(subscriber: EbicsSubscriberEntity): EbicsClientSubscriberDetails { - var bankAuthPubValue: RSAPublicKey? = null - if (subscriber.bankAuthenticationPublicKey != null) { - bankAuthPubValue = CryptoUtil.loadRsaPublicKey( - subscriber.bankAuthenticationPublicKey?.bytes!! - ) - } - var bankEncPubValue: RSAPublicKey? = null - if (subscriber.bankEncryptionPublicKey != null) { - bankEncPubValue = CryptoUtil.loadRsaPublicKey( - subscriber.bankEncryptionPublicKey?.bytes!! - ) - } - return EbicsClientSubscriberDetails( - bankAuthPub = bankAuthPubValue, - bankEncPub = bankEncPubValue, - - ebicsUrl = subscriber.ebicsURL, - hostId = subscriber.hostID, - userId = subscriber.userID, - partnerId = subscriber.partnerID, - - customerSignPriv = CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.bytes), - customerAuthPriv = CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.bytes), - customerEncPriv = CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.bytes), - ebicsIniState = subscriber.ebicsIniState, - ebicsHiaState = subscriber.ebicsHiaState - ) -} /** * Check if the transaction is already found in the database. @@ -355,6 +306,7 @@ fun getPreparedPayment(uuid: Long): InitiatedPaymentEntity { fun addPreparedPayment(paymentData: Pain001Data, debitorAccount: NexusBankAccountEntity): InitiatedPaymentEntity { return transaction { InitiatedPaymentEntity.new { + bankAccount = debitorAccount subject = paymentData.subject sum = paymentData.sum debitorIban = debitorAccount.iban diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt index 300cd34c..b199ddea 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -26,9 +26,15 @@ package tech.libeufin.nexus import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo +import io.ktor.http.HttpStatusCode +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.transactions.transaction import org.w3c.dom.Document -import tech.libeufin.util.XmlElementDestructor -import tech.libeufin.util.destructXml +import tech.libeufin.util.* +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter enum class CreditDebitIndicator { DBIT, CRDT @@ -211,6 +217,142 @@ data class RelatedParties( class CamtParsingError(msg: String) : Exception(msg) +/** + * Data that the LibEuFin nexus uses for payment initiation. + * Subset of what ISO 20022 allows. + */ +data class NexusPaymentInitiationData( + val debtorIban: String, + val debtorBic: String, + val messageId: String, + val paymentInformationId: String, + val amount: String, + val currency: String, + val subject: String, + val preparationTimestamp: Long, + val creditorName: String, + val creditorIban: 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. + */ + val debitorBankAccountLabel = run { + val debitorBankAcount = NexusBankAccountEntity.find { + NexusBankAccountsTable.iban eq paymentData.debtorIban and + (NexusBankAccountsTable.bankCode eq paymentData.debtorBic) + }.firstOrNull() ?: throw NexusError( + HttpStatusCode.NotFound, + "Please download bank accounts details first (HTD)" + ) + debitorBankAcount.id.value + } + + val s = constructXml(indent = true) { + root("Document") { + attribute("xmlns", "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03") + 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") + element("CstmrCdtTrfInitn") { + element("GrpHdr") { + element("MsgId") { + text(paymentData.messageId) + } + element("CreDtTm") { + val dateMillis = paymentData.preparationTimestamp + val dateFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME + val instant = Instant.ofEpochSecond(dateMillis / 1000) + val zoned = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()) + text(dateFormatter.format(zoned)) + } + element("NbOfTxs") { + text("1") + } + element("CtrlSum") { + text(paymentData.amount) + } + element("InitgPty/Nm") { + text(debitorBankAccountLabel) + } + } + element("PmtInf") { + element("PmtInfId") { + text(paymentData.paymentInformationId) + } + element("PmtMtd") { + text("TRF") + } + element("BtchBookg") { + text("true") + } + element("NbOfTxs") { + text("1") + } + element("CtrlSum") { + text(paymentData.amount) + } + element("PmtTpInf/SvcLvl/Cd") { + text("SEPA") + } + element("ReqdExctnDt") { + val dateMillis = paymentData.preparationTimestamp + text(importDateFromMillis(dateMillis).toDashedDate()) + } + element("Dbtr/Nm") { + text(debitorBankAccountLabel) + } + element("DbtrAcct/Id/IBAN") { + text(paymentData.debtorIban) + } + element("DbtrAgt/FinInstnId/BIC") { + text(paymentData.debtorBic) + } + element("ChrgBr") { + text("SLEV") + } + element("CdtTrfTxInf") { + element("PmtId") { + element("EndToEndId") { + // text(pain001Entity.id.value.toString()) + text("NOTPROVIDED") + } + } + element("Amt/InstdAmt") { + attribute("Ccy", paymentData.currency) + text(paymentData.amount) + } + element("Cdtr/Nm") { + text(paymentData.creditorName) + } + element("CdtrAcct/Id/IBAN") { + text(paymentData.creditorIban) + } + element("RmtInf/Ustrd") { + text(paymentData.subject) + } + } + } + } + } + } + return s +} + private fun XmlElementDestructor.extractDateOrDateTime(): DateOrDateTime { return requireOnlyChild { when (it.localName) { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt index af86c005..41123111 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -66,6 +66,7 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.Logger import org.slf4j.LoggerFactory import org.slf4j.event.Level +import tech.libeufin.nexus.bankaccount.submitPreparedPayments import tech.libeufin.nexus.ebics.* import tech.libeufin.util.* import tech.libeufin.util.CryptoUtil.hashpw @@ -245,7 +246,7 @@ fun moreFrequentBackgroundTasks(httpClient: HttpClient) { } // FIXME: should be done automatically after raw ingestion reportAndIgnoreErrors { ingestTalerTransactions() } - reportAndIgnoreErrors { submitPreparedPaymentsViaEbics(httpClient) } + reportAndIgnoreErrors { submitPreparedPayments(httpClient) } logger.debug("More frequent background jobs done") delay(Duration.ofSeconds(1)) } @@ -362,7 +363,6 @@ fun serverMain(dbName: String) { indentObjectsWith(DefaultIndenter(" ", "\n")) }) registerModule(KotlinModule(nullisSameAsDefault = true)) - //registerModule(JavaTimeModule()) } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt new file mode 100644 index 00000000..3c938289 --- /dev/null +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt @@ -0,0 +1,76 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2020 Taler Systems S.A. + * + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + * + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.nexus.bankaccount + +import io.ktor.client.HttpClient +import io.ktor.http.HttpStatusCode +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.not +import org.jetbrains.exposed.sql.transactions.transaction +import tech.libeufin.nexus.* +import tech.libeufin.nexus.ebics.doEbicsUploadTransaction +import tech.libeufin.nexus.ebics.submitEbicsPaymentInitiation +import tech.libeufin.util.EbicsClientSubscriberDetails +import tech.libeufin.util.EbicsStandardOrderParams + + +/** + * Submit all pending prepared payments. + */ +suspend fun submitPreparedPayments(httpClient: HttpClient) { + data class Submission( + val id: Long, + val type: String + ) + logger.debug("auto-submitter started") + val workQueue = mutableListOf<Submission>() + transaction { + NexusBankAccountEntity.all().forEach { + val defaultBankConnectionId = it.defaultBankConnection?.id ?: throw NexusError( + HttpStatusCode.BadRequest, + "needs default bank connection" + ) + val bankConnection = NexusBankConnectionEntity.findById(defaultBankConnectionId) ?: throw NexusError( + HttpStatusCode.InternalServerError, + "Bank account '${it.id.value}' doesn't map to any bank connection (named '${it.defaultBankConnection}')" + ) + if (bankConnection.type != "ebics") { + logger.info("Skipping non-implemented bank connection '${bankConnection.type}'") + return@forEach + } + val bankAccount: NexusBankAccountEntity = it + InitiatedPaymentEntity.find { + InitiatedPaymentsTable.debitorIban eq bankAccount.iban and + not(InitiatedPaymentsTable.submitted) + }.forEach { + workQueue.add(Submission(it.id.value, bankConnection.type)) + } + } + } + workQueue.forEach { + when (it.type) { + "ebics" -> { + submitEbicsPaymentInitiation(httpClient, it.id) + } + else -> throw NexusError(HttpStatusCode.NotImplemented, "submission for ${it.type }not supported") + } + + } +}
\ No newline at end of file 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 2c9741df..95847984 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -49,6 +49,7 @@ import tech.libeufin.util.ebics_h004.EbicsTypes import tech.libeufin.util.ebics_h004.HTDResponseOrderData import java.io.ByteArrayOutputStream import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -219,6 +220,36 @@ fun createEbicsBankConnectionFromBackup( return } +private fun getEbicsSubscriberDetailsInternal(subscriber: EbicsSubscriberEntity): EbicsClientSubscriberDetails { + var bankAuthPubValue: RSAPublicKey? = null + if (subscriber.bankAuthenticationPublicKey != null) { + bankAuthPubValue = CryptoUtil.loadRsaPublicKey( + subscriber.bankAuthenticationPublicKey?.bytes!! + ) + } + var bankEncPubValue: RSAPublicKey? = null + if (subscriber.bankEncryptionPublicKey != null) { + bankEncPubValue = CryptoUtil.loadRsaPublicKey( + subscriber.bankEncryptionPublicKey?.bytes!! + ) + } + return EbicsClientSubscriberDetails( + bankAuthPub = bankAuthPubValue, + bankEncPub = bankEncPubValue, + + ebicsUrl = subscriber.ebicsURL, + hostId = subscriber.hostID, + userId = subscriber.userID, + partnerId = subscriber.partnerID, + + customerSignPriv = CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.bytes), + customerAuthPriv = CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.bytes), + customerEncPriv = CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.bytes), + ebicsIniState = subscriber.ebicsIniState, + ebicsHiaState = subscriber.ebicsHiaState + ) +} + /** * Retrieve Ebics subscriber details given a bank connection. */ @@ -335,7 +366,8 @@ fun Route.ebicsBankConnectionRoutes(client: HttpClient) { payload.value.partnerInfo.accountInfoList?.forEach { NexusBankAccountEntity.new(id = it.id) { accountHolder = it.accountHolder ?: "NOT-GIVEN" - iban = extractFirstIban(it.accountNumberList) + iban = it.accountNumberList?.filterIsInstance<EbicsTypes.GeneralAccountNumber>() + ?.find { it.international }?.value ?: throw NexusError(HttpStatusCode.NotFound, reason = "bank gave no IBAN") bankCode = it.bankCodeList?.filterIsInstance<EbicsTypes.GeneralBankCode>() ?.find { it.international }?.value @@ -426,17 +458,6 @@ fun exportEbicsKeyBackup(bankConnectionId: String, passphrase: String): Any { ) } -suspend fun submitEbicsPaymentInitiation(client: HttpClient, connId: String, pain001Document: String) { - val ebicsSubscriberDetails = transaction { getEbicsSubscriberDetails(connId) } - logger.debug("Uploading PAIN.001: ${pain001Document}") - doEbicsUploadTransaction( - client, - ebicsSubscriberDetails, - "CCT", - pain001Document.toByteArray(Charsets.UTF_8), - EbicsStandardOrderParams() - ) -} fun getEbicsConnectionDetails(conn: NexusBankConnectionEntity): Any { val ebicsSubscriber = transaction { getEbicsSubscriberDetails(conn.id.value) } @@ -610,4 +631,40 @@ fun getEbicsKeyLetterPdf(conn: NexusBankConnectionEntity): ByteArray { } pdfWriter.flush() return po.toByteArray() +} + +suspend fun submitEbicsPaymentInitiation(httpClient: HttpClient, paymentInitiationId: Long) { + val r = transaction { + val paymentInitiation = InitiatedPaymentEntity.findById(paymentInitiationId) + ?: throw NexusError(HttpStatusCode.NotFound, "payment initiation not found") + val connId = paymentInitiation.bankAccount.defaultBankConnection?.id + ?: throw NexusError(HttpStatusCode.NotFound, "no default bank connection available for submission") + val subscriberDetails = getEbicsSubscriberDetails(connId.value) + val painMessage = createPain001document( + NexusPaymentInitiationData( + debtorIban = paymentInitiation.debitorIban, + currency = paymentInitiation.currency, + amount = paymentInitiation.sum.toString(), + creditorIban = paymentInitiation.creditorIban, + creditorName = paymentInitiation.creditorName, + debtorBic = paymentInitiation.creditorBic, + // FIXME(dold): Put date in here as well + messageId = paymentInitiation.id.toString(), + // FIXME(dold): Put date in here as well + paymentInformationId = paymentInitiation.id.toString(), + preparationTimestamp = paymentInitiation.preparationDate, + subject = paymentInitiation.subject + )) + object { + val subscriberDetails = subscriberDetails + val painMessage = painMessage + } + } + doEbicsUploadTransaction( + httpClient, + r.subscriberDetails, + "CCT", + r.painMessage.toByteArray(Charsets.UTF_8), + EbicsStandardOrderParams() + ) }
\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt index 3d7c056d..3f2470fe 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt @@ -379,65 +379,6 @@ private suspend fun talerAddIncoming(call: ApplicationCall, httpClient: HttpClie ) } -/** - * submits ALL the prepared payments from ALL the Taler facades. - * FIXME(dold): This should not be done here. - * -> why? It crawls the *taler* facade to find payment to submit. - */ -suspend fun submitPreparedPaymentsViaEbics(httpClient: HttpClient) { - data class EbicsSubmission( - val subscriberDetails: EbicsClientSubscriberDetails, - val pain001document: String - ) - logger.debug("auto-submitter started") - val workQueue = mutableListOf<EbicsSubmission>() - transaction { - TalerFacadeStateEntity.all().forEach { - val bankConnection = NexusBankConnectionEntity.findById(it.bankConnection) ?: throw NexusError( - HttpStatusCode.InternalServerError, - "Such facade '${it.facade.id.value}' doesn't map to any bank connection (named '${it.bankConnection}')" - ) - if (bankConnection.type != "ebics") { - logger.info("Skipping non-implemented bank connection '${bankConnection.type}'") - return@forEach - } - - val subscriberEntity = EbicsSubscriberEntity.find { - EbicsSubscribersTable.nexusBankConnection eq it.bankConnection - }.firstOrNull() ?: throw NexusError( - HttpStatusCode.InternalServerError, - "Such facade '${it.facade.id.value}' doesn't map to any Ebics subscriber" - ) - val bankAccount: NexusBankAccountEntity = - NexusBankAccountEntity.findById(it.bankAccount) ?: throw NexusError( - HttpStatusCode.InternalServerError, - "Bank account '${it.bankAccount}' not found for facade '${it.id.value}'" - ) - InitiatedPaymentEntity.find { - InitiatedPaymentsTable.debitorIban eq bankAccount.iban and - not(InitiatedPaymentsTable.submitted) - }.forEach { - val pain001document = createPain001document(it) - logger.debug("Preparing payment: ${pain001document}") - val subscriberDetails = getEbicsSubscriberDetailsInternal(subscriberEntity) - workQueue.add(EbicsSubmission(subscriberDetails, pain001document)) - // FIXME: the payment must be flagged AFTER the submission happens. - // -> this is an open question: see #6367. - it.submitted = true - } - } - } - workQueue.forEach { - println("submitting prepared payment via EBICS") - doEbicsUploadTransaction( - httpClient, - it.subscriberDetails, - "CCT", - it.pain001document.toByteArray(Charsets.UTF_8), - EbicsStandardOrderParams() - ) - } -} private fun ingestIncoming(payment: RawBankTransactionEntity, txDtls: TransactionDetails) { val subject = txDtls.unstructuredRemittanceInformation |