Signal-Android/donations/lib/src/main/java/org/signal/donations/StripeApi.kt

306 wiersze
7.1 KiB
Kotlin

package org.signal.donations
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.ByteString
import org.json.JSONObject
import org.signal.core.util.money.FiatMoney
import java.io.IOException
import java.math.BigDecimal
import java.util.Locale
class StripeApi(private val configuration: Configuration, private val paymentIntentFetcher: PaymentIntentFetcher, private val okHttpClient: OkHttpClient) {
sealed class CreatePaymentIntentResult {
data class AmountIsTooSmall(val amount: FiatMoney) : CreatePaymentIntentResult()
data class AmountIsTooLarge(val amount: FiatMoney) : CreatePaymentIntentResult()
data class CurrencyIsNotSupported(val currencyCode: String) : CreatePaymentIntentResult()
data class Success(val paymentIntent: PaymentIntent) : CreatePaymentIntentResult()
}
fun createPaymentIntent(price: FiatMoney, description: String? = null): Single<CreatePaymentIntentResult> {
@Suppress("CascadeIf")
return if (Validation.isAmountTooSmall(price)) {
Single.just(CreatePaymentIntentResult.AmountIsTooSmall(price))
} else if (Validation.isAmountTooLarge(price)) {
Single.just(CreatePaymentIntentResult.AmountIsTooLarge(price))
} else if (!Validation.supportedCurrencyCodes.contains(price.currency.currencyCode.toUpperCase(Locale.ROOT))) {
Single.just<CreatePaymentIntentResult>(CreatePaymentIntentResult.CurrencyIsNotSupported(price.currency.currencyCode))
} else {
paymentIntentFetcher
.fetchPaymentIntent(price, description)
.map<CreatePaymentIntentResult> { CreatePaymentIntentResult.Success(it) }
}.subscribeOn(Schedulers.io())
}
fun confirmPaymentIntent(paymentSource: PaymentSource, paymentIntent: PaymentIntent): Completable = Completable.fromAction {
val paymentMethodId = createPaymentMethod(paymentSource).use { response ->
val body = response.body()
if (body != null) {
val paymentMethodObject = body.string().replace("\n", "").let { JSONObject(it) }
paymentMethodObject.getString("id")
} else {
throw IOException("Failed to parse payment method response")
}
}
val parameters = mapOf(
"client_secret" to paymentIntent.clientSecret,
"payment_method" to paymentMethodId
)
postForm("payment_intents/${paymentIntent.id}/confirm", parameters)
}.subscribeOn(Schedulers.io())
private fun createPaymentMethod(paymentSource: PaymentSource): Response {
val tokenizationData = paymentSource.parameterize()
val parameters = mapOf(
"card[token]" to JSONObject((tokenizationData.get("token") as String).replace("\n", "")).getString("id"),
"type" to "card"
)
return postForm("payment_methods", parameters)
}
private fun postForm(endpoint: String, parameters: Map<String, String>): Response {
val formBodyBuilder = FormBody.Builder()
parameters.forEach { (k, v) ->
formBodyBuilder.add(k, v)
}
val request = Request.Builder()
.url("${configuration.baseUrl}/$endpoint")
.addHeader("Authorization", "Basic ${ByteString.encodeUtf8("${configuration.publishableKey}:").base64()}")
.post(formBodyBuilder.build())
.build()
val response = okHttpClient.newCall(request).execute()
if (response.isSuccessful) {
return response
} else {
throw IOException("postForm failure: ${response.code()}")
}
}
object Validation {
private val MAX_AMOUNT = BigDecimal(99_999_999)
fun isAmountTooLarge(fiatMoney: FiatMoney): Boolean {
return fiatMoney.amount > MAX_AMOUNT
}
fun isAmountTooSmall(fiatMoney: FiatMoney): Boolean {
return fiatMoney.amount < BigDecimal(minimumIntegralChargePerCurrencyCode[fiatMoney.currency.currencyCode] ?: 50)
}
private val minimumIntegralChargePerCurrencyCode: Map<String, Int> = mapOf(
"USD" to 50,
"AED" to 200,
"AUD" to 50,
"BGN" to 100,
"BRL" to 50,
"CAD" to 50,
"CHF" to 50,
"CZK" to 1500,
"DKK" to 250,
"EUR" to 50,
"GBP" to 30,
"HKD" to 400,
"HUF" to 17500,
"INR" to 50,
"JPY" to 50,
"MXN" to 10,
"MYR" to 2,
"NOK" to 300,
"NZD" to 50,
"PLN" to 200,
"RON" to 200,
"SEK" to 300,
"SGD" to 50
)
val supportedCurrencyCodes: List<String> = listOf(
"USD",
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BWP",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"ISK",
"JMD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KRW",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"STD",
"SZL",
"THB",
"TJS",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"UYU",
"UZS",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMW"
)
}
class Gateway(private val configuration: Configuration) : GooglePayApi.Gateway {
override fun getTokenizationSpecificationParameters(): Map<String, String> {
return mapOf(
"gateway" to "stripe",
"stripe:version" to configuration.version,
"stripe:publishableKey" to configuration.publishableKey
)
}
override val allowedCardNetworks: List<String> = listOf(
"AMEX",
"DISCOVER",
"JCB",
"MASTERCARD",
"VISA"
)
}
data class Configuration(
val publishableKey: String,
val baseUrl: String = "https://api.stripe.com/v1",
val version: String = "2018-10-31"
)
interface PaymentIntentFetcher {
fun fetchPaymentIntent(
price: FiatMoney,
description: String? = null
): Single<PaymentIntent>
}
data class PaymentIntent(
val id: String,
val clientSecret: String
)
interface PaymentSource {
fun parameterize(): JSONObject
}
}