Implemented writing account transactions to CSV files (with a very primitive CSV file writer)

This commit is contained in:
dankito 2022-02-24 02:42:23 +01:00
parent ed66168c0b
commit bed96199c8
5 changed files with 111 additions and 9 deletions

View File

@ -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"
}
}

View File

@ -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")
}

View File

@ -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<OutputFormat>().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<TanMethodType>().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)
}
}

View File

@ -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 ?: ""
}
}

View File

@ -0,0 +1,12 @@
package util
enum class OutputFormat {
Json,
CommaSeparated,
SemicolonSeparated
}