Skip to content

Commit

Permalink
various improvements (#4)
Browse files Browse the repository at this point in the history
* add bignum

* rename params, fix issues

* update readme

* fix big decimal, fix test

* fix value sanitization

* fix readme

---------

Co-authored-by: radovan paška <[email protected]>
  • Loading branch information
c4t-dr34m and radovan paška authored Jan 15, 2025
1 parent 6558827 commit 8ea699c
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 149 deletions.
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

0 comments on commit 8ea699c

Please sign in to comment.