Skip to content

various improvements #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ Create `Spayd` instance. The only mandatory parameter is `account`. Example:

```kotlin
val spayd = Spayd(
account = Account(iban = "CZ7603000000000076327632"),
amount = 500.00,
currency = "CZK",
bankAccount = BankAccount(iban = "CZ7603000000000076327632"),
amount = "500.00".toBigDecimal(),
currencyCode = "CZK",
message = "Clovek v tisni",
)
```
Expand All @@ -46,9 +46,9 @@ This will validate data and possibly throw `ValidationException` with a short me
### Alternative constructors
```kotlin
val spayd = Spayd(
Key.ACCOUNT to Account(iban = "CZ7603000000000076327632"),
Key.AMOUNT to 500.00,
Key.CURRENCY to "CZK",
Key.BANK_ACCOUNT to BankAccount(iban = "CZ7603000000000076327632"),
Key.AMOUNT to "500.00".toBigDecimal(),
Key.CURRENCY_CODE to "CZK",
Key.MESSAGE to "Clovek v tisni",
)
```
Expand All @@ -57,9 +57,9 @@ or

```kotlin
val parameters: Map<Key, Any> = mapOf(
Key.ACCOUNT to Account(iban = "CZ7603000000000076327632"),
Key.AMOUNT to 500.00,
Key.CURRENCY to "CZK",
Key.BANK_ACCOUNT to BankAccount(iban = "CZ7603000000000076327632"),
Key.AMOUNT to "500.00".toBigDecimal(),
Key.CURRENCY_CODE to "CZK",
Key.MESSAGE to "Clovek v tisni",
)

Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
agp = "8.2.2"
android-compileSdk = "34"
android-minSdk = "23"
bignum = "0.3.10"
compose-plugin = "1.6.11"
kotlin = "2.0.21"
kotlinx-datetime = "0.6.1"
Expand All @@ -21,6 +22,7 @@ skie = { id = "co.touchlab.skie", version.ref = "skie" }
mavenDeployer = { id = "io.deepmedia.tools.deployer", version.ref = "maven-deployer"}

[libraries]
bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "bignum" }
kotlin_junit = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin_test = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" }
kotlin_test_annotations = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" }
Expand Down
3 changes: 2 additions & 1 deletion shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ kotlin {

sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.datetime)
api(libs.bignum)
api(libs.kotlinx.datetime)
implementation(libs.ktor.http)
implementation(libs.okio)
implementation(libs.urlencoder)
Expand Down
88 changes: 33 additions & 55 deletions shared/src/commonMain/kotlin/io/stepuplabs/spaydkmp/Spayd.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package io.stepuplabs.spaydkmp

import io.stepuplabs.spaydkmp.common.Account
import io.stepuplabs.spaydkmp.common.AccountList
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import io.stepuplabs.spaydkmp.common.BankAccount
import io.stepuplabs.spaydkmp.common.BankAccountList
import io.stepuplabs.spaydkmp.common.Key
import io.stepuplabs.spaydkmp.common.NotificationType
import io.stepuplabs.spaydkmp.common.PaymentType
import io.stepuplabs.spaydkmp.common.Validator
import io.stepuplabs.spaydkmp.exception.*
import kotlin.math.log10
import kotlinx.datetime.LocalDate
import kotlinx.datetime.format
import net.thauvin.erik.urlencoder.UrlEncoderUtil
Expand All @@ -25,42 +26,41 @@ class Spayd(

// Convenience constructor that accepts all values in form of named parameters
constructor(
account: Account,
alternateAccounts: AccountList? = null,
currency: String? = null,
amount: Double? = null,
date: LocalDate? = null,
senderReference: Int? = null,
bankAccount: BankAccount,
alternativeBankAccounts: BankAccountList? = null,
currencyCode: String? = null,
amount: BigDecimal? = null,
dueDate: LocalDate? = null,
referenceForRecipient: Int? = null,
recipientName: String? = null,
paymentType: String? = null,
paymentType: PaymentType? = null,
message: String? = null,
notificationType: NotificationType? = null,
notificationAddress: String? = null,
repeat: Int? = null,
daysToRepeatIfUnsuccessfull: Int? = null,
variableSymbol: Long? = null,
specificSymbol: Long? = null,
constantSymbol: Long? = null,
identifier: String? = null,
referenceForSender: String? = null,
url: String? = null,
): this(
parameters = arrayOf(
Key.ACCOUNT to account,
alternateAccounts?.let { Key.ALTERNATE_ACCOUNTS to it },
alternateAccounts?.let { Key.ALTERNATE_ACCOUNTS to it },
currency?.let { Key.CURRENCY to it },
Key.BANK_ACCOUNT to bankAccount,
alternativeBankAccounts?.let { Key.ALTERNATIVE_BANK_ACCOUNTS to it },
currencyCode?.let { Key.CURRENCY_CODE to it },
amount?.let { Key.AMOUNT to it },
date?.let { Key.DATE to it },
senderReference?.let { Key.SENDER_REFERENCE to it },
dueDate?.let { Key.DUE_DATE to it },
referenceForRecipient?.let { Key.REFERENCE_FOR_RECIPIENT to it },
recipientName?.let { Key.RECIPIENT_NAME to it },
paymentType?.let { Key.PAYMENT_TYPE to it },
message?.let { Key.MESSAGE to it },
notificationType?.let { Key.NOTIFY_TYPE to it },
notificationAddress?.let { Key.NOTIFY_ADDRESS to it },
repeat?.let { Key.REPEAT to it },
daysToRepeatIfUnsuccessfull?.let { Key.DAYS_TO_REPEAT_IF_UNSUCCESSFUL to it },
variableSymbol?.let { Key.VARIABLE_SYMBOL to it },
specificSymbol?.let { Key.SPECIFIC_SYMBOL to it },
constantSymbol?.let { Key.CONSTANT_SYMBOL to it },
identifier?.let { Key.IDENTIFIER to it },
referenceForSender?.let { Key.REFERENCE_FOR_SENDER to it },
url?.let { Key.URL to it },
)
)
Expand All @@ -77,7 +77,7 @@ class Spayd(

// payment parameters
for (parameter in parameters.filterNotNull()) {
getEntry(parameter.first.key, parameter.second)?.let { parts.add(it) }
getEntry(parameter.first, parameter.second)?.let { parts.add(it) }
}

// merge into one string
Expand Down Expand Up @@ -109,7 +109,7 @@ class Spayd(
validator.validate(key = parameter.first, value = parameter.second)

when (parameter.first) {
Key.ACCOUNT -> hasAccount = true
Key.BANK_ACCOUNT -> hasAccount = true
Key.NOTIFY_TYPE -> hasNotificationType = true
Key.NOTIFY_ADDRESS -> hasNotificationAddress = true
else -> continue
Expand All @@ -126,12 +126,18 @@ class Spayd(
}

// Get parameter:value key for SPAYD
private fun getEntry(parameter: String, value: Any?): String? {
private fun getEntry(parameter: Key, value: Any?): String? {
if (value == null) {
return null
}

return "$parameter:$value"
val valStr = when (parameter.type) {
LocalDate::class -> (value as LocalDate).format(LocalDate.Formats.ISO_BASIC)
BigDecimal::class -> (value as BigDecimal).toStringExpanded()
else -> sanitize("$value")
}

return "${parameter.key}:$valStr"
}

// Get parameter:value key for SPAYD
Expand All @@ -147,44 +153,16 @@ class Spayd(
}

entries.append(
escape(value.toString()),
sanitize(value.toString()),
)
}

return "$parameter:$entries"
}

// Get parameter:value key for SPAYD
private fun getEntry(parameter: String, date: LocalDate?): String? {
if (date == null) {
return null
}

return "$parameter:${date.format(LocalDate.Formats.ISO_BASIC)}"
}

// Sanitize values for SPAYD
private fun escape(value: String): String {
val escapedValue = StringBuilder()

for (char in value) {
if (char.code > 127) {
escapedValue.append(UrlEncoderUtil.encode(char.toString()))
} else {
if (char.compareTo('*') == 0) { // spayd value separator
escapedValue.append("%2A")
} else if (char.compareTo('+') == 0) {
escapedValue.append("%2B")
} else if (char.compareTo('%') == 0) {
escapedValue.append("%25")
} else {
escapedValue.append(char)
}
}
}

return escapedValue.toString()
}
private fun sanitize(value: String): String = Regex("[^A-Za-z0-9 @$%+\\-/:.,]")
.replace(value, "")

companion object {
const val MIME_TYPE: String = "application/x-shortpaymentdescriptor"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.stepuplabs.spaydkmp.common
/*
Account representation
*/
data class Account(
data class BankAccount(
val iban: String,
val bic: String? = null,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package io.stepuplabs.spaydkmp.common
/*
Representation of multiple Accounts
*/
data class AccountList(
val accounts: List<Account>
data class BankAccountList(
val bankAccounts: List<BankAccount>
) {
override fun toString(): String {
val builder = StringBuilder()
for (account in accounts) {
for (account in bankAccounts) {
if (builder.isNotEmpty()) {
builder.append(",")
}
Expand Down
24 changes: 15 additions & 9 deletions shared/src/commonMain/kotlin/io/stepuplabs/spaydkmp/common/Key.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.stepuplabs.spaydkmp.common

import com.ionspin.kotlin.bignum.decimal.BigDecimal
import kotlinx.datetime.LocalDate
import kotlin.reflect.KClass

Expand All @@ -15,21 +16,26 @@ enum class Key(
val minLength: Int? = null,
val maxLength: Int? = null,
) {
DATE(key = "DT", type = LocalDate::class),
CURRENCY(key = "CC", type = String::class, minLength = 3, maxLength = 3),
AMOUNT(key = "AM", type = Double::class, minValue = 0.00, maxValue = 9_999_999.99),
ACCOUNT(key = "ACC", type = Account::class),
ALTERNATE_ACCOUNTS(key = "ALT-ACC", type = AccountList::class, maxLength = 2),
SENDER_REFERENCE(key = "RF", type = Int::class, maxLength = 16),
DUE_DATE(key = "DT", type = LocalDate::class),
CURRENCY_CODE(key = "CC", type = String::class, minLength = 3, maxLength = 3),
AMOUNT(key = "AM", type = BigDecimal::class, minValue = 0.00, maxValue = 9_999_999.99),
BANK_ACCOUNT(key = "ACC", type = BankAccount::class),
ALTERNATIVE_BANK_ACCOUNTS(key = "ALT-ACC", type = BankAccountList::class, maxLength = 2),
REFERENCE_FOR_RECIPIENT(key = "RF", type = Int::class, maxLength = 16),
RECIPIENT_NAME(key = "RN", type = String::class, maxLength = 35),
PAYMENT_TYPE(key = "PT", type = String::class, maxLength = 3),
PAYMENT_TYPE(key = "PT", type = PaymentType::class, maxLength = 3),
MESSAGE(key = "MSG", type = String::class, maxLength = 60),
NOTIFY_TYPE(key = "NT", type = NotificationType::class),
NOTIFY_ADDRESS(key = "NTA", type = String::class, maxLength = 320),
REPEAT(key = "X-PER", type = Int::class, minValue = 0.0, maxValue = 30.0),
DAYS_TO_REPEAT_IF_UNSUCCESSFUL(
key = "X-PER",
type = Int::class,
minValue = 0.0,
maxValue = 30.0,
),
VARIABLE_SYMBOL(key = "X-VS", type = Long::class, maxLength = 10),
SPECIFIC_SYMBOL(key = "X-SS", type = Long::class, maxLength = 10),
CONSTANT_SYMBOL(key = "X-KS", type = Long::class, maxLength = 10),
IDENTIFIER(key = "X-ID", type = String::class, maxLength = 20),
REFERENCE_FOR_SENDER(key = "X-ID", type = String::class, maxLength = 20),
URL(key = "X-URL", type = String::class, maxLength = 40),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.stepuplabs.spaydkmp.common

/*
Payment type representation
*/
@Suppress("UNUSED")
enum class PaymentType(val key: String) {
IMMEDIATE_PAYMENT(key = "IP");

override fun toString(): String = key
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.stepuplabs.spaydkmp.common

import com.ionspin.kotlin.bignum.decimal.BigDecimal
import io.stepuplabs.spaydkmp.exception.ValidationException
import kotlinx.datetime.LocalDate
import kotlin.math.log10
Expand All @@ -19,8 +20,9 @@ internal class Validator {

when (key.type) {
LocalDate::class -> return true
Account::class -> return true
BankAccount::class -> return true
NotificationType::class -> return true
PaymentType::class -> return true

Int::class -> {
val typedValue = value as Int
Expand Down Expand Up @@ -89,6 +91,23 @@ internal class Validator {
// length for double doesn't make much sense
}

BigDecimal::class -> {
val typedValue = value as BigDecimal

key.minValue?.let {
if (typedValue < it) {
throw ValidationException("$key is lower than allowed minimum value ($it)")
}
}
key.maxValue?.let {
if (typedValue > it) {
throw ValidationException("$key is higher than allowed maximum value ($it)")
}
}

// length for big decimal doesn't make much sense
}

String::class -> {
val typedValue = value as String

Expand All @@ -106,18 +125,18 @@ internal class Validator {
}
}

AccountList::class -> {
val typedValue = value as AccountList
BankAccountList::class -> {
val typedValue = value as BankAccountList

// min/max value for list doesn't make much sense

key.minLength?.let {
if (typedValue.accounts.count() < it) {
if (typedValue.bankAccounts.count() < it) {
throw ValidationException("$key is shorter than allowed minimum length ($it)")
}
}
key.maxLength?.let {
if (typedValue.accounts.count() > it) {
if (typedValue.bankAccounts.count() > it) {
throw ValidationException("$key is longer than allowed maximum length ($it)")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ Exception that represents failed parameter validation effort
*/
class ValidationException(
override val message: String?,
): Exception()
): Throwable()
Loading
Loading