From bed96199c8652ce66a6d9cde54ee9d179bfb45df Mon Sep 17 00:00:00 2001 From: dankito Date: Thu, 24 Feb 2022 02:42:23 +0100 Subject: [PATCH] Implemented writing account transactions to CSV files (with a very primitive CSV file writer) --- fints4k/build.gradle | 8 ++- fints4k/src/nativeMain/kotlin/NativeApp.kt | 26 ++++++-- .../commands/fints4kCommandLineInterface.kt | 10 ++- .../src/nativeMain/kotlin/util/CsvWriter.kt | 64 +++++++++++++++++++ .../nativeMain/kotlin/util/OutputFormat.kt | 12 ++++ 5 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 fints4k/src/nativeMain/kotlin/util/CsvWriter.kt create mode 100644 fints4k/src/nativeMain/kotlin/util/OutputFormat.kt diff --git a/fints4k/build.gradle b/fints4k/build.gradle index 8a63676b..5e225a86 100644 --- a/fints4k/build.gradle +++ b/fints4k/build.gradle @@ -1,5 +1,6 @@ plugins { id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" version "$kotlinVersion" id "maven-publish" } @@ -75,10 +76,12 @@ kotlin { dependencies { api project(":multiplatform-utils") - implementation "co.touchlab:stately-concurrency:1.2.0" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" + implementation "co.touchlab:stately-concurrency:1.2.0" + implementation "io.ktor:ktor-client-core:$ktorVersion" } } @@ -144,6 +147,9 @@ kotlin { implementation "io.ktor:ktor-client-curl:$ktorVersion" implementation "com.github.ajalt.clikt:clikt:3.4.0" + + // only needed for writing files to output + implementation "com.soywiz.korlibs.korio:korio:2.5.0" } } diff --git a/fints4k/src/nativeMain/kotlin/NativeApp.kt b/fints4k/src/nativeMain/kotlin/NativeApp.kt index 78fb122d..cec9f515 100644 --- a/fints4k/src/nativeMain/kotlin/NativeApp.kt +++ b/fints4k/src/nativeMain/kotlin/NativeApp.kt @@ -1,4 +1,9 @@ +import com.soywiz.korio.file.PathInfo +import com.soywiz.korio.file.isAbsolute import com.soywiz.korio.file.std.localCurrentDirVfs +import com.soywiz.korio.file.std.rootLocalVfs +import com.soywiz.korio.file.std.userHomeVfs +import com.soywiz.korio.lang.substr import kotlinx.coroutines.runBlocking import kotlinx.datetime.LocalDate import kotlinx.serialization.encodeToString @@ -13,6 +18,8 @@ import net.dankito.banking.fints.getAccountData import net.dankito.banking.fints.model.TanChallenge import net.dankito.banking.fints.transferMoney import net.dankito.utils.multiplatform.extensions.* +import util.CsvWriter +import util.OutputFormat class NativeApp { @@ -24,7 +31,7 @@ class NativeApp { getAccountData(GetAccountDataParameter(bankCode, loginName, password)) } - fun getAccountData(param: GetAccountDataParameter, outputFilePath: String? = null) { + fun getAccountData(param: GetAccountDataParameter, outputFilePath: String? = null, outputFormat: OutputFormat = OutputFormat.Json) { val response = client.getAccountData(param) if (response.error != null) { @@ -33,7 +40,7 @@ class NativeApp { response.customerAccount?.let { account -> if (outputFilePath != null) { - writeResponseToFile(outputFilePath, account) + writeResponseToFile(outputFilePath, outputFormat, account) } else { println("Retrieved response from ${account.bankName} for ${account.customerName}") @@ -109,14 +116,21 @@ class NativeApp { } - private fun writeResponseToFile(outputFilePath: String, customer: CustomerAccount) { + private fun writeResponseToFile(outputFilePath: String, outputFormat: OutputFormat, customer: CustomerAccount) { try { - val outputFile = localCurrentDirVfs.get(outputFilePath) + val outputFileInfo = PathInfo(outputFilePath) + val outputFile = if (outputFileInfo.isAbsolute()) rootLocalVfs.get(outputFilePath) + else if (outputFilePath.startsWith("~/")) userHomeVfs.get(outputFilePath.substr(2)) + else localCurrentDirVfs.get(outputFilePath) println("Writing file to ${outputFile.absolutePath}") - val json = Json.encodeToString(customer) + if (outputFormat == OutputFormat.Json) { + val json = Json.encodeToString(customer) - runBlocking { outputFile.writeString(json) } + runBlocking { outputFile.writeString(json) } + } else { + CsvWriter().writeToFile(outputFile, if (outputFormat == OutputFormat.SemicolonSeparated) ";" else ",", customer) + } } catch (e: Exception) { println("Could not write file to $outputFilePath: $e") } diff --git a/fints4k/src/nativeMain/kotlin/commands/fints4kCommandLineInterface.kt b/fints4k/src/nativeMain/kotlin/commands/fints4kCommandLineInterface.kt index e261e404..4197d4bb 100644 --- a/fints4k/src/nativeMain/kotlin/commands/fints4kCommandLineInterface.kt +++ b/fints4k/src/nativeMain/kotlin/commands/fints4kCommandLineInterface.kt @@ -14,6 +14,7 @@ import net.dankito.banking.client.model.parameter.GetAccountDataParameter import net.dankito.banking.client.model.parameter.RetrieveTransactions import net.dankito.banking.fints.model.TanMethodType import net.dankito.utils.multiplatform.extensions.todayAtEuropeBerlin +import util.OutputFormat class fints4kCommandLineInterface : CliktCommand(name = "fints", printHelpOnEmptyArgs = true, invokeWithoutSubcommand = true) { @@ -40,6 +41,8 @@ class fints4kCommandLineInterface : CliktCommand(name = "fints", printHelpOnEmpt val outputFile by option("-o", help = "Write retrieved account transactions to file instead of stdout. Supported formats: JSON") + val outputFormat by option("-f", "--format").enum().default(OutputFormat.Json) + val preferredTanMethods by option("-m", "--tan-method", help = "Your preferred TAN methods to use if action affords a TAN. Can be repeated like '-m AppTan -m SmsTan'").enum().multiple() val abortIfRequiresTan by option("-a", "--abort-if-requires-tan", help = "If actions should be aborted if it affords a TAN. Defaults to false").flag(default = false) @@ -69,11 +72,14 @@ class fints4kCommandLineInterface : CliktCommand(name = "fints", printHelpOnEmpt val retrieveTransactionsToDate = if (retrieveTransactionsTo.isNullOrBlank()) null else LocalDate.parse(retrieveTransactionsTo!!) val effectiveRetrieveTransactions = if (retrieveTransactionsFromDate != null || retrieveTransactionsToDate != null) RetrieveTransactions.AccordingToRetrieveFromAndTo - else retrieveTransactions + else retrieveTransactions + + val effectiveOutputFormat = if (outputFile != null && outputFile?.endsWith(".csv", true) == true && outputFormat == OutputFormat.Json) OutputFormat.CommaSeparated + else outputFormat app.getAccountData(GetAccountDataParameter(bankCode, loginName, password, null, retrieveBalance, effectiveRetrieveTransactions, - retrieveTransactionsFromDate, retrieveTransactionsToDate, preferredTanMethods, abortIfTanIsRequired = abortIfRequiresTan), outputFile) + retrieveTransactionsFromDate, retrieveTransactionsToDate, preferredTanMethods, abortIfTanIsRequired = abortIfRequiresTan), outputFile, effectiveOutputFormat) } } \ No newline at end of file diff --git a/fints4k/src/nativeMain/kotlin/util/CsvWriter.kt b/fints4k/src/nativeMain/kotlin/util/CsvWriter.kt new file mode 100644 index 00000000..c0b43693 --- /dev/null +++ b/fints4k/src/nativeMain/kotlin/util/CsvWriter.kt @@ -0,0 +1,64 @@ +package util + +import com.soywiz.korio.file.VfsFile +import com.soywiz.korio.file.VfsOpenMode +import com.soywiz.korio.stream.AsyncStream +import com.soywiz.korio.stream.writeString +import kotlinx.coroutines.runBlocking +import net.dankito.banking.client.model.AccountTransaction +import net.dankito.banking.client.model.BankAccount +import net.dankito.banking.client.model.CustomerAccount + + +/** + * A very basic implementation of a CSV writer. Do not use in production + */ +open class CsvWriter { + + companion object { + const val NewLine = "\r\n" + } + + + open fun writeToFile(outputFile: VfsFile, valueSeparator: String, customer: CustomerAccount) { + runBlocking { + val stream = outputFile.open(VfsOpenMode.CREATE_OR_TRUNCATE) + + // print header + stream.writeString(listOf("Bank", "Account", "Date", "Amount", "Currency", "Booking text", "Reference", "Other party name", "Other party bank id", "Other party account id").joinToString(valueSeparator)) + stream.writeString(NewLine) + + customer.accounts.forEach { writeToFile(stream, valueSeparator, customer, it) } + + stream.close() + } + } + + protected open suspend fun writeToFile(stream: AsyncStream, valueSeparator: String, customer: CustomerAccount, account: BankAccount) { + account.bookedTransactions.forEach { writeToFile(stream, valueSeparator, customer, account, it) } + } + + protected open suspend fun writeToFile(stream: AsyncStream, valueSeparator: String, customer: CustomerAccount, account: BankAccount, transaction: AccountTransaction) { + val amount = if (valueSeparator == ";") transaction.amount.amount.string.replace('.', ',') else transaction.amount.amount.string.replace(',', '.') + + stream.writeString(listOf(customer.bankName, account.identifier, transaction.valueDate, amount, transaction.amount.currency, ensureNotNull(transaction.bookingText), wrap(transaction.reference), + ensureNotNull(transaction.otherPartyName), ensureNotNull(transaction.otherPartyBankCode), ensureNotNull(transaction.otherPartyAccountId)).joinToString(valueSeparator)) + + stream.writeString(NewLine) + } + + /** + * Wraps values that potentially contain the value separator + */ + protected open fun wrap(value: String): String { + return "\"$value\"" + } + + /** + * Ensures that 'null' doesn't get written to output + */ + protected open fun ensureNotNull(value: Any?): Any { + return value ?: "" + } + +} \ No newline at end of file diff --git a/fints4k/src/nativeMain/kotlin/util/OutputFormat.kt b/fints4k/src/nativeMain/kotlin/util/OutputFormat.kt new file mode 100644 index 00000000..dba02c87 --- /dev/null +++ b/fints4k/src/nativeMain/kotlin/util/OutputFormat.kt @@ -0,0 +1,12 @@ +package util + + +enum class OutputFormat { + + Json, + + CommaSeparated, + + SemicolonSeparated + +} \ No newline at end of file