Implemented writing account transactions to CSV files (with a very primitive CSV file writer)
This commit is contained in:
parent
ed66168c0b
commit
bed96199c8
|
@ -1,5 +1,6 @@
|
||||||
plugins {
|
plugins {
|
||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization" version "$kotlinVersion"
|
||||||
id "maven-publish"
|
id "maven-publish"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,10 +76,12 @@ kotlin {
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(":multiplatform-utils")
|
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 "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
|
||||||
|
|
||||||
|
implementation "co.touchlab:stately-concurrency:1.2.0"
|
||||||
|
|
||||||
implementation "io.ktor:ktor-client-core:$ktorVersion"
|
implementation "io.ktor:ktor-client-core:$ktorVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,6 +147,9 @@ kotlin {
|
||||||
implementation "io.ktor:ktor-client-curl:$ktorVersion"
|
implementation "io.ktor:ktor-client-curl:$ktorVersion"
|
||||||
|
|
||||||
implementation "com.github.ajalt.clikt:clikt:3.4.0"
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.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.coroutines.runBlocking
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.encodeToString
|
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.model.TanChallenge
|
||||||
import net.dankito.banking.fints.transferMoney
|
import net.dankito.banking.fints.transferMoney
|
||||||
import net.dankito.utils.multiplatform.extensions.*
|
import net.dankito.utils.multiplatform.extensions.*
|
||||||
|
import util.CsvWriter
|
||||||
|
import util.OutputFormat
|
||||||
|
|
||||||
|
|
||||||
class NativeApp {
|
class NativeApp {
|
||||||
|
@ -24,7 +31,7 @@ class NativeApp {
|
||||||
getAccountData(GetAccountDataParameter(bankCode, loginName, password))
|
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)
|
val response = client.getAccountData(param)
|
||||||
|
|
||||||
if (response.error != null) {
|
if (response.error != null) {
|
||||||
|
@ -33,7 +40,7 @@ class NativeApp {
|
||||||
|
|
||||||
response.customerAccount?.let { account ->
|
response.customerAccount?.let { account ->
|
||||||
if (outputFilePath != null) {
|
if (outputFilePath != null) {
|
||||||
writeResponseToFile(outputFilePath, account)
|
writeResponseToFile(outputFilePath, outputFormat, account)
|
||||||
} else {
|
} else {
|
||||||
println("Retrieved response from ${account.bankName} for ${account.customerName}")
|
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 {
|
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}")
|
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) {
|
} catch (e: Exception) {
|
||||||
println("Could not write file to $outputFilePath: $e")
|
println("Could not write file to $outputFilePath: $e")
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
||||||
import net.dankito.banking.client.model.parameter.RetrieveTransactions
|
import net.dankito.banking.client.model.parameter.RetrieveTransactions
|
||||||
import net.dankito.banking.fints.model.TanMethodType
|
import net.dankito.banking.fints.model.TanMethodType
|
||||||
import net.dankito.utils.multiplatform.extensions.todayAtEuropeBerlin
|
import net.dankito.utils.multiplatform.extensions.todayAtEuropeBerlin
|
||||||
|
import util.OutputFormat
|
||||||
|
|
||||||
|
|
||||||
class fints4kCommandLineInterface : CliktCommand(name = "fints", printHelpOnEmptyArgs = true, invokeWithoutSubcommand = true) {
|
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 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 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)
|
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 retrieveTransactionsToDate = if (retrieveTransactionsTo.isNullOrBlank()) null else LocalDate.parse(retrieveTransactionsTo!!)
|
||||||
val effectiveRetrieveTransactions = if (retrieveTransactionsFromDate != null || retrieveTransactionsToDate != null) RetrieveTransactions.AccordingToRetrieveFromAndTo
|
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,
|
app.getAccountData(GetAccountDataParameter(bankCode, loginName, password, null, retrieveBalance, effectiveRetrieveTransactions,
|
||||||
retrieveTransactionsFromDate, retrieveTransactionsToDate, preferredTanMethods, abortIfTanIsRequired = abortIfRequiresTan), outputFile)
|
retrieveTransactionsFromDate, retrieveTransactionsToDate, preferredTanMethods, abortIfTanIsRequired = abortIfRequiresTan), outputFile, effectiveOutputFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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 ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
|
||||||
|
enum class OutputFormat {
|
||||||
|
|
||||||
|
Json,
|
||||||
|
|
||||||
|
CommaSeparated,
|
||||||
|
|
||||||
|
SemicolonSeparated
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue