Skip to content

Commit 8ea699c

Browse files
c4t-dr34mradovan paška
and
radovan paška
authored
various improvements (#4)
* 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]>
1 parent 6558827 commit 8ea699c

File tree

12 files changed

+172
-149
lines changed

12 files changed

+172
-149
lines changed

README.md

+9-9
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ Create `Spayd` instance. The only mandatory parameter is `account`. Example:
2828

2929
```kotlin
3030
val spayd = Spayd(
31-
account = Account(iban = "CZ7603000000000076327632"),
32-
amount = 500.00,
33-
currency = "CZK",
31+
bankAccount = BankAccount(iban = "CZ7603000000000076327632"),
32+
amount = "500.00".toBigDecimal(),
33+
currencyCode = "CZK",
3434
message = "Clovek v tisni",
3535
)
3636
```
@@ -46,9 +46,9 @@ This will validate data and possibly throw `ValidationException` with a short me
4646
### Alternative constructors
4747
```kotlin
4848
val spayd = Spayd(
49-
Key.ACCOUNT to Account(iban = "CZ7603000000000076327632"),
50-
Key.AMOUNT to 500.00,
51-
Key.CURRENCY to "CZK",
49+
Key.BANK_ACCOUNT to BankAccount(iban = "CZ7603000000000076327632"),
50+
Key.AMOUNT to "500.00".toBigDecimal(),
51+
Key.CURRENCY_CODE to "CZK",
5252
Key.MESSAGE to "Clovek v tisni",
5353
)
5454
```
@@ -57,9 +57,9 @@ or
5757

5858
```kotlin
5959
val parameters: Map<Key, Any> = mapOf(
60-
Key.ACCOUNT to Account(iban = "CZ7603000000000076327632"),
61-
Key.AMOUNT to 500.00,
62-
Key.CURRENCY to "CZK",
60+
Key.BANK_ACCOUNT to BankAccount(iban = "CZ7603000000000076327632"),
61+
Key.AMOUNT to "500.00".toBigDecimal(),
62+
Key.CURRENCY_CODE to "CZK",
6363
Key.MESSAGE to "Clovek v tisni",
6464
)
6565

gradle/libs.versions.toml

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
agp = "8.2.2"
33
android-compileSdk = "34"
44
android-minSdk = "23"
5+
bignum = "0.3.10"
56
compose-plugin = "1.6.11"
67
kotlin = "2.0.21"
78
kotlinx-datetime = "0.6.1"
@@ -21,6 +22,7 @@ skie = { id = "co.touchlab.skie", version.ref = "skie" }
2122
mavenDeployer = { id = "io.deepmedia.tools.deployer", version.ref = "maven-deployer"}
2223

2324
[libraries]
25+
bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "bignum" }
2426
kotlin_junit = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
2527
kotlin_test = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" }
2628
kotlin_test_annotations = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" }

shared/build.gradle.kts

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ kotlin {
3535

3636
sourceSets {
3737
commonMain.dependencies {
38-
implementation(libs.kotlinx.datetime)
38+
api(libs.bignum)
39+
api(libs.kotlinx.datetime)
3940
implementation(libs.ktor.http)
4041
implementation(libs.okio)
4142
implementation(libs.urlencoder)

shared/src/commonMain/kotlin/io/stepuplabs/spaydkmp/Spayd.kt

+33-55
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package io.stepuplabs.spaydkmp
22

3-
import io.stepuplabs.spaydkmp.common.Account
4-
import io.stepuplabs.spaydkmp.common.AccountList
3+
import com.ionspin.kotlin.bignum.decimal.BigDecimal
4+
import io.stepuplabs.spaydkmp.common.BankAccount
5+
import io.stepuplabs.spaydkmp.common.BankAccountList
56
import io.stepuplabs.spaydkmp.common.Key
67
import io.stepuplabs.spaydkmp.common.NotificationType
8+
import io.stepuplabs.spaydkmp.common.PaymentType
79
import io.stepuplabs.spaydkmp.common.Validator
810
import io.stepuplabs.spaydkmp.exception.*
9-
import kotlin.math.log10
1011
import kotlinx.datetime.LocalDate
1112
import kotlinx.datetime.format
1213
import net.thauvin.erik.urlencoder.UrlEncoderUtil
@@ -25,42 +26,41 @@ class Spayd(
2526

2627
// Convenience constructor that accepts all values in form of named parameters
2728
constructor(
28-
account: Account,
29-
alternateAccounts: AccountList? = null,
30-
currency: String? = null,
31-
amount: Double? = null,
32-
date: LocalDate? = null,
33-
senderReference: Int? = null,
29+
bankAccount: BankAccount,
30+
alternativeBankAccounts: BankAccountList? = null,
31+
currencyCode: String? = null,
32+
amount: BigDecimal? = null,
33+
dueDate: LocalDate? = null,
34+
referenceForRecipient: Int? = null,
3435
recipientName: String? = null,
35-
paymentType: String? = null,
36+
paymentType: PaymentType? = null,
3637
message: String? = null,
3738
notificationType: NotificationType? = null,
3839
notificationAddress: String? = null,
39-
repeat: Int? = null,
40+
daysToRepeatIfUnsuccessfull: Int? = null,
4041
variableSymbol: Long? = null,
4142
specificSymbol: Long? = null,
4243
constantSymbol: Long? = null,
43-
identifier: String? = null,
44+
referenceForSender: String? = null,
4445
url: String? = null,
4546
): this(
4647
parameters = arrayOf(
47-
Key.ACCOUNT to account,
48-
alternateAccounts?.let { Key.ALTERNATE_ACCOUNTS to it },
49-
alternateAccounts?.let { Key.ALTERNATE_ACCOUNTS to it },
50-
currency?.let { Key.CURRENCY to it },
48+
Key.BANK_ACCOUNT to bankAccount,
49+
alternativeBankAccounts?.let { Key.ALTERNATIVE_BANK_ACCOUNTS to it },
50+
currencyCode?.let { Key.CURRENCY_CODE to it },
5151
amount?.let { Key.AMOUNT to it },
52-
date?.let { Key.DATE to it },
53-
senderReference?.let { Key.SENDER_REFERENCE to it },
52+
dueDate?.let { Key.DUE_DATE to it },
53+
referenceForRecipient?.let { Key.REFERENCE_FOR_RECIPIENT to it },
5454
recipientName?.let { Key.RECIPIENT_NAME to it },
5555
paymentType?.let { Key.PAYMENT_TYPE to it },
5656
message?.let { Key.MESSAGE to it },
5757
notificationType?.let { Key.NOTIFY_TYPE to it },
5858
notificationAddress?.let { Key.NOTIFY_ADDRESS to it },
59-
repeat?.let { Key.REPEAT to it },
59+
daysToRepeatIfUnsuccessfull?.let { Key.DAYS_TO_REPEAT_IF_UNSUCCESSFUL to it },
6060
variableSymbol?.let { Key.VARIABLE_SYMBOL to it },
6161
specificSymbol?.let { Key.SPECIFIC_SYMBOL to it },
6262
constantSymbol?.let { Key.CONSTANT_SYMBOL to it },
63-
identifier?.let { Key.IDENTIFIER to it },
63+
referenceForSender?.let { Key.REFERENCE_FOR_SENDER to it },
6464
url?.let { Key.URL to it },
6565
)
6666
)
@@ -77,7 +77,7 @@ class Spayd(
7777

7878
// payment parameters
7979
for (parameter in parameters.filterNotNull()) {
80-
getEntry(parameter.first.key, parameter.second)?.let { parts.add(it) }
80+
getEntry(parameter.first, parameter.second)?.let { parts.add(it) }
8181
}
8282

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

111111
when (parameter.first) {
112-
Key.ACCOUNT -> hasAccount = true
112+
Key.BANK_ACCOUNT -> hasAccount = true
113113
Key.NOTIFY_TYPE -> hasNotificationType = true
114114
Key.NOTIFY_ADDRESS -> hasNotificationAddress = true
115115
else -> continue
@@ -126,12 +126,18 @@ class Spayd(
126126
}
127127

128128
// Get parameter:value key for SPAYD
129-
private fun getEntry(parameter: String, value: Any?): String? {
129+
private fun getEntry(parameter: Key, value: Any?): String? {
130130
if (value == null) {
131131
return null
132132
}
133133

134-
return "$parameter:$value"
134+
val valStr = when (parameter.type) {
135+
LocalDate::class -> (value as LocalDate).format(LocalDate.Formats.ISO_BASIC)
136+
BigDecimal::class -> (value as BigDecimal).toStringExpanded()
137+
else -> sanitize("$value")
138+
}
139+
140+
return "${parameter.key}:$valStr"
135141
}
136142

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

149155
entries.append(
150-
escape(value.toString()),
156+
sanitize(value.toString()),
151157
)
152158
}
153159

154160
return "$parameter:$entries"
155161
}
156162

157-
// Get parameter:value key for SPAYD
158-
private fun getEntry(parameter: String, date: LocalDate?): String? {
159-
if (date == null) {
160-
return null
161-
}
162-
163-
return "$parameter:${date.format(LocalDate.Formats.ISO_BASIC)}"
164-
}
165-
166163
// Sanitize values for SPAYD
167-
private fun escape(value: String): String {
168-
val escapedValue = StringBuilder()
169-
170-
for (char in value) {
171-
if (char.code > 127) {
172-
escapedValue.append(UrlEncoderUtil.encode(char.toString()))
173-
} else {
174-
if (char.compareTo('*') == 0) { // spayd value separator
175-
escapedValue.append("%2A")
176-
} else if (char.compareTo('+') == 0) {
177-
escapedValue.append("%2B")
178-
} else if (char.compareTo('%') == 0) {
179-
escapedValue.append("%25")
180-
} else {
181-
escapedValue.append(char)
182-
}
183-
}
184-
}
185-
186-
return escapedValue.toString()
187-
}
164+
private fun sanitize(value: String): String = Regex("[^A-Za-z0-9 @$%+\\-/:.,]")
165+
.replace(value, "")
188166

189167
companion object {
190168
const val MIME_TYPE: String = "application/x-shortpaymentdescriptor"

shared/src/commonMain/kotlin/io/stepuplabs/spaydkmp/common/Account.kt renamed to shared/src/commonMain/kotlin/io/stepuplabs/spaydkmp/common/BankAccount.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package io.stepuplabs.spaydkmp.common
33
/*
44
Account representation
55
*/
6-
data class Account(
6+
data class BankAccount(
77
val iban: String,
88
val bic: String? = null,
99
) {

shared/src/commonMain/kotlin/io/stepuplabs/spaydkmp/common/AccountList.kt renamed to shared/src/commonMain/kotlin/io/stepuplabs/spaydkmp/common/BankAccountList.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package io.stepuplabs.spaydkmp.common
33
/*
44
Representation of multiple Accounts
55
*/
6-
data class AccountList(
7-
val accounts: List<Account>
6+
data class BankAccountList(
7+
val bankAccounts: List<BankAccount>
88
) {
99
override fun toString(): String {
1010
val builder = StringBuilder()
11-
for (account in accounts) {
11+
for (account in bankAccounts) {
1212
if (builder.isNotEmpty()) {
1313
builder.append(",")
1414
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.stepuplabs.spaydkmp.common
22

3+
import com.ionspin.kotlin.bignum.decimal.BigDecimal
34
import kotlinx.datetime.LocalDate
45
import kotlin.reflect.KClass
56

@@ -15,21 +16,26 @@ enum class Key(
1516
val minLength: Int? = null,
1617
val maxLength: Int? = null,
1718
) {
18-
DATE(key = "DT", type = LocalDate::class),
19-
CURRENCY(key = "CC", type = String::class, minLength = 3, maxLength = 3),
20-
AMOUNT(key = "AM", type = Double::class, minValue = 0.00, maxValue = 9_999_999.99),
21-
ACCOUNT(key = "ACC", type = Account::class),
22-
ALTERNATE_ACCOUNTS(key = "ALT-ACC", type = AccountList::class, maxLength = 2),
23-
SENDER_REFERENCE(key = "RF", type = Int::class, maxLength = 16),
19+
DUE_DATE(key = "DT", type = LocalDate::class),
20+
CURRENCY_CODE(key = "CC", type = String::class, minLength = 3, maxLength = 3),
21+
AMOUNT(key = "AM", type = BigDecimal::class, minValue = 0.00, maxValue = 9_999_999.99),
22+
BANK_ACCOUNT(key = "ACC", type = BankAccount::class),
23+
ALTERNATIVE_BANK_ACCOUNTS(key = "ALT-ACC", type = BankAccountList::class, maxLength = 2),
24+
REFERENCE_FOR_RECIPIENT(key = "RF", type = Int::class, maxLength = 16),
2425
RECIPIENT_NAME(key = "RN", type = String::class, maxLength = 35),
25-
PAYMENT_TYPE(key = "PT", type = String::class, maxLength = 3),
26+
PAYMENT_TYPE(key = "PT", type = PaymentType::class, maxLength = 3),
2627
MESSAGE(key = "MSG", type = String::class, maxLength = 60),
2728
NOTIFY_TYPE(key = "NT", type = NotificationType::class),
2829
NOTIFY_ADDRESS(key = "NTA", type = String::class, maxLength = 320),
29-
REPEAT(key = "X-PER", type = Int::class, minValue = 0.0, maxValue = 30.0),
30+
DAYS_TO_REPEAT_IF_UNSUCCESSFUL(
31+
key = "X-PER",
32+
type = Int::class,
33+
minValue = 0.0,
34+
maxValue = 30.0,
35+
),
3036
VARIABLE_SYMBOL(key = "X-VS", type = Long::class, maxLength = 10),
3137
SPECIFIC_SYMBOL(key = "X-SS", type = Long::class, maxLength = 10),
3238
CONSTANT_SYMBOL(key = "X-KS", type = Long::class, maxLength = 10),
33-
IDENTIFIER(key = "X-ID", type = String::class, maxLength = 20),
39+
REFERENCE_FOR_SENDER(key = "X-ID", type = String::class, maxLength = 20),
3440
URL(key = "X-URL", type = String::class, maxLength = 40),
3541
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.stepuplabs.spaydkmp.common
2+
3+
/*
4+
Payment type representation
5+
*/
6+
@Suppress("UNUSED")
7+
enum class PaymentType(val key: String) {
8+
IMMEDIATE_PAYMENT(key = "IP");
9+
10+
override fun toString(): String = key
11+
}

shared/src/commonMain/kotlin/io/stepuplabs/spaydkmp/common/Validator.kt

+24-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.stepuplabs.spaydkmp.common
22

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

2021
when (key.type) {
2122
LocalDate::class -> return true
22-
Account::class -> return true
23+
BankAccount::class -> return true
2324
NotificationType::class -> return true
25+
PaymentType::class -> return true
2426

2527
Int::class -> {
2628
val typedValue = value as Int
@@ -89,6 +91,23 @@ internal class Validator {
8991
// length for double doesn't make much sense
9092
}
9193

94+
BigDecimal::class -> {
95+
val typedValue = value as BigDecimal
96+
97+
key.minValue?.let {
98+
if (typedValue < it) {
99+
throw ValidationException("$key is lower than allowed minimum value ($it)")
100+
}
101+
}
102+
key.maxValue?.let {
103+
if (typedValue > it) {
104+
throw ValidationException("$key is higher than allowed maximum value ($it)")
105+
}
106+
}
107+
108+
// length for big decimal doesn't make much sense
109+
}
110+
92111
String::class -> {
93112
val typedValue = value as String
94113

@@ -106,18 +125,18 @@ internal class Validator {
106125
}
107126
}
108127

109-
AccountList::class -> {
110-
val typedValue = value as AccountList
128+
BankAccountList::class -> {
129+
val typedValue = value as BankAccountList
111130

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

114133
key.minLength?.let {
115-
if (typedValue.accounts.count() < it) {
134+
if (typedValue.bankAccounts.count() < it) {
116135
throw ValidationException("$key is shorter than allowed minimum length ($it)")
117136
}
118137
}
119138
key.maxLength?.let {
120-
if (typedValue.accounts.count() > it) {
139+
if (typedValue.bankAccounts.count() > it) {
121140
throw ValidationException("$key is longer than allowed maximum length ($it)")
122141
}
123142
}

shared/src/commonMain/kotlin/io/stepuplabs/spaydkmp/exception/ValidationException.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ Exception that represents failed parameter validation effort
55
*/
66
class ValidationException(
77
override val message: String?,
8-
): Exception()
8+
): Throwable()

0 commit comments

Comments
 (0)