Fix bad one-time-payment receipt creation for cancelled iDEAL.
This commit is contained in:
parent
fa72a1788b
commit
3eea331e83
9 changed files with 245 additions and 11 deletions
|
@ -228,6 +228,7 @@ android {
|
||||||
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"")
|
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"")
|
||||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"unset\"")
|
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"unset\"")
|
||||||
buildConfigField("String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\"")
|
buildConfigField("String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\"")
|
||||||
|
buildConfigField("String", "STRIPE_BASE_URL", "\"https://api.stripe.com/v1\"")
|
||||||
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
|
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
|
||||||
buildConfigField("boolean", "TRACING_ENABLED", "false")
|
buildConfigField("boolean", "TRACING_ENABLED", "false")
|
||||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "false")
|
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "false")
|
||||||
|
@ -301,6 +302,7 @@ android {
|
||||||
applicationIdSuffix = ".instrumentation"
|
applicationIdSuffix = ".instrumentation"
|
||||||
|
|
||||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Instrumentation\"")
|
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Instrumentation\"")
|
||||||
|
buildConfigField("String", "STRIPE_BASE_URL", "\"http://127.0.0.1:8080/stripe\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
create("spinner") {
|
create("spinner") {
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import okhttp3.mockwebserver.MockResponse
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.core.util.deleteAll
|
||||||
|
import org.signal.core.util.money.FiatMoney
|
||||||
|
import org.signal.donations.InAppPaymentType
|
||||||
|
import org.signal.donations.json.StripeIntentStatus
|
||||||
|
import org.signal.donations.json.StripePaymentIntent
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
|
||||||
|
import org.thoughtcrime.securesms.database.DonationReceiptTable
|
||||||
|
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||||
|
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||||
|
import org.thoughtcrime.securesms.testing.Get
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import org.thoughtcrime.securesms.testing.assert
|
||||||
|
import org.thoughtcrime.securesms.testing.success
|
||||||
|
import org.thoughtcrime.securesms.util.TestStripePaths
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.util.Currency
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class InAppPaymentAuthCheckJobTest {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TEST_INTENT_ID = "test-intent-id"
|
||||||
|
private const val TEST_CLIENT_SECRET = "test-client-secret"
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val harness = SignalActivityRule()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
SignalDatabase.inAppPayments.writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME)
|
||||||
|
SignalDatabase.donationReceipts.writableDatabase.deleteAll(DonationReceiptTable.TABLE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenCanceledOneTimeAuthRequiredPayment_whenICheck_thenIDoNotExpectAReceipt() {
|
||||||
|
initializeMockGetPaymentIntent(status = StripeIntentStatus.CANCELED)
|
||||||
|
|
||||||
|
SignalDatabase.inAppPayments.insert(
|
||||||
|
type = InAppPaymentType.ONE_TIME_DONATION,
|
||||||
|
state = InAppPaymentTable.State.WAITING_FOR_AUTHORIZATION,
|
||||||
|
subscriberId = null,
|
||||||
|
endOfPeriod = null,
|
||||||
|
inAppPaymentData = InAppPaymentData(
|
||||||
|
amount = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")).toFiatValue(),
|
||||||
|
waitForAuth = InAppPaymentData.WaitingForAuthorizationState(
|
||||||
|
stripeIntentId = TEST_INTENT_ID,
|
||||||
|
stripeClientSecret = TEST_CLIENT_SECRET
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
InAppPaymentAuthCheckJob().run()
|
||||||
|
|
||||||
|
val receipts = SignalDatabase.donationReceipts.getReceipts(InAppPaymentReceiptRecord.Type.ONE_TIME_DONATION)
|
||||||
|
receipts assert Matchers.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenSuccessfulOneTimeAuthRequiredPayment_whenICheck_thenIExpectAReceipt() {
|
||||||
|
initializeMockGetPaymentIntent(status = StripeIntentStatus.SUCCEEDED)
|
||||||
|
|
||||||
|
SignalDatabase.inAppPayments.insert(
|
||||||
|
type = InAppPaymentType.ONE_TIME_DONATION,
|
||||||
|
state = InAppPaymentTable.State.WAITING_FOR_AUTHORIZATION,
|
||||||
|
subscriberId = null,
|
||||||
|
endOfPeriod = null,
|
||||||
|
inAppPaymentData = InAppPaymentData(
|
||||||
|
amount = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")).toFiatValue(),
|
||||||
|
waitForAuth = InAppPaymentData.WaitingForAuthorizationState(
|
||||||
|
stripeIntentId = TEST_INTENT_ID,
|
||||||
|
stripeClientSecret = TEST_CLIENT_SECRET
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
InAppPaymentAuthCheckJob().run()
|
||||||
|
|
||||||
|
val receipts = SignalDatabase.donationReceipts.getReceipts(InAppPaymentReceiptRecord.Type.ONE_TIME_DONATION)
|
||||||
|
receipts assert Matchers.hasSize(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeMockGetPaymentIntent(status: StripeIntentStatus) {
|
||||||
|
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||||
|
Get(TestStripePaths.getPaymentIntentPath(TEST_INTENT_ID, TEST_CLIENT_SECRET)) {
|
||||||
|
MockResponse().success(
|
||||||
|
StripePaymentIntent(
|
||||||
|
id = TEST_INTENT_ID,
|
||||||
|
clientSecret = TEST_CLIENT_SECRET,
|
||||||
|
status = status,
|
||||||
|
paymentMethod = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.util
|
||||||
|
|
||||||
|
import org.signal.donations.StripePaths
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stripe paths should be prefixed with 'stripe/' in order to access the proper namespacing in
|
||||||
|
* the mock server. This object serves as a convenience delegate to StripePaths.
|
||||||
|
*/
|
||||||
|
object TestStripePaths {
|
||||||
|
/**
|
||||||
|
* @see StripePaths.getPaymentIntentPath
|
||||||
|
*/
|
||||||
|
fun getPaymentIntentPath(paymentIntentId: String, clientSecret: String): String {
|
||||||
|
return withNamespace(StripePaths.getPaymentIntentPath(paymentIntentId, clientSecret))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see StripePaths.getPaymentIntentConfirmationPath
|
||||||
|
*/
|
||||||
|
fun getPaymentIntentConfirmationPath(paymentIntentId: String): String {
|
||||||
|
return withNamespace(StripePaths.getPaymentIntentConfirmationPath(paymentIntentId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see StripePaths.getSetupIntentPath
|
||||||
|
*/
|
||||||
|
fun getSetupIntentPath(setupIntentId: String, clientSecret: String): String {
|
||||||
|
return withNamespace(StripePaths.getSetupIntentPath(setupIntentId, clientSecret))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see StripePaths.getSetupIntentConfirmationPath
|
||||||
|
*/
|
||||||
|
fun getSetupIntentConfirmationPath(setupIntentId: String): String {
|
||||||
|
return withNamespace(StripePaths.getSetupIntentConfirmationPath(setupIntentId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see StripePaths.getPaymentIntentPath
|
||||||
|
*/
|
||||||
|
fun getPaymentMethodsPath(): String {
|
||||||
|
return withNamespace(StripePaths.getPaymentMethodsPath())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see StripePaths.getTokensPath
|
||||||
|
*/
|
||||||
|
fun getTokensPath(): String {
|
||||||
|
return withNamespace(StripePaths.getTokensPath())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun withNamespace(path: String) = "stripe/$path"
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.core.content.contentValuesOf
|
import androidx.core.content.contentValuesOf
|
||||||
import org.signal.core.util.CursorUtil
|
import org.signal.core.util.CursorUtil
|
||||||
import org.signal.core.util.SqlUtil
|
import org.signal.core.util.SqlUtil
|
||||||
|
@ -12,7 +13,8 @@ import java.util.Currency
|
||||||
|
|
||||||
class DonationReceiptTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
|
class DonationReceiptTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TABLE_NAME = "donation_receipt"
|
@VisibleForTesting
|
||||||
|
const val TABLE_NAME = "donation_receipt"
|
||||||
|
|
||||||
private const val ID = "_id"
|
private const val ID = "_id"
|
||||||
private const val TYPE = "receipt_type"
|
private const val TYPE = "receipt_type"
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.jobs
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.money.FiatMoney
|
import org.signal.core.util.money.FiatMoney
|
||||||
|
@ -40,7 +41,8 @@ import kotlin.time.Duration.Companion.days
|
||||||
*/
|
*/
|
||||||
class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : BaseJob(parameters), StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper {
|
class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : BaseJob(parameters), StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper {
|
||||||
|
|
||||||
private constructor() : this(
|
@VisibleForTesting
|
||||||
|
constructor() : this(
|
||||||
Parameters.Builder()
|
Parameters.Builder()
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.setMaxAttempts(Parameters.UNLIMITED)
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
|
@ -138,7 +140,10 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
checkIntentStatus(stripeIntentData.status)
|
val checkIntentStatusResult = checkIntentStatus(stripeIntentData.status)
|
||||||
|
if (checkIntentStatusResult !is CheckResult.Success) {
|
||||||
|
return checkIntentStatusResult
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Creating and inserting receipt.", true)
|
Log.i(TAG, "Creating and inserting receipt.", true)
|
||||||
val receipt = when (inAppPayment.type) {
|
val receipt = when (inAppPayment.type) {
|
||||||
|
|
|
@ -29,6 +29,7 @@ object Environment {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@get:JvmName("getStripeConfiguration")
|
@get:JvmName("getStripeConfiguration")
|
||||||
val STRIPE_CONFIGURATION = StripeApi.Configuration(
|
val STRIPE_CONFIGURATION = StripeApi.Configuration(
|
||||||
|
baseUrl = BuildConfig.STRIPE_BASE_URL,
|
||||||
publishableKey = BuildConfig.STRIPE_PUBLISHABLE_KEY
|
publishableKey = BuildConfig.STRIPE_PUBLISHABLE_KEY
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ class StripeApi(
|
||||||
parameters["mandate_data[customer_acceptance][online][infer_from_client]"] = "true"
|
parameters["mandate_data[customer_acceptance][online][infer_from_client]"] = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
val (nextActionUri, returnUri) = postForm("setup_intents/${setupIntent.intentId}/confirm", parameters).use { response ->
|
val (nextActionUri, returnUri) = postForm(StripePaths.getSetupIntentConfirmationPath(setupIntent.intentId), parameters).use { response ->
|
||||||
getNextAction(response)
|
getNextAction(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ class StripeApi(
|
||||||
parameters["mandate_data[customer_acceptance][online][infer_from_client]"] = "true"
|
parameters["mandate_data[customer_acceptance][online][infer_from_client]"] = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
val (nextActionUri, returnUri) = postForm("payment_intents/${paymentIntent.intentId}/confirm", parameters).use { response ->
|
val (nextActionUri, returnUri) = postForm(StripePaths.getPaymentIntentConfirmationPath(paymentIntent.intentId), parameters).use { response ->
|
||||||
getNextAction(response)
|
getNextAction(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ class StripeApi(
|
||||||
*/
|
*/
|
||||||
fun getSetupIntent(stripeIntentAccessor: StripeIntentAccessor): StripeSetupIntent {
|
fun getSetupIntent(stripeIntentAccessor: StripeIntentAccessor): StripeSetupIntent {
|
||||||
return when (stripeIntentAccessor.objectType) {
|
return when (stripeIntentAccessor.objectType) {
|
||||||
StripeIntentAccessor.ObjectType.SETUP_INTENT -> get("setup_intents/${stripeIntentAccessor.intentId}?client_secret=${stripeIntentAccessor.intentClientSecret}&expand[0]=latest_attempt").use {
|
StripeIntentAccessor.ObjectType.SETUP_INTENT -> get(StripePaths.getSetupIntentPath(stripeIntentAccessor.intentId, stripeIntentAccessor.intentClientSecret)).use {
|
||||||
val body = it.body?.string()
|
val body = it.body?.string()
|
||||||
try {
|
try {
|
||||||
objectMapper.readValue(body!!)
|
objectMapper.readValue(body!!)
|
||||||
|
@ -167,7 +167,7 @@ class StripeApi(
|
||||||
*/
|
*/
|
||||||
fun getPaymentIntent(stripeIntentAccessor: StripeIntentAccessor): StripePaymentIntent {
|
fun getPaymentIntent(stripeIntentAccessor: StripeIntentAccessor): StripePaymentIntent {
|
||||||
return when (stripeIntentAccessor.objectType) {
|
return when (stripeIntentAccessor.objectType) {
|
||||||
StripeIntentAccessor.ObjectType.PAYMENT_INTENT -> get("payment_intents/${stripeIntentAccessor.intentId}?client_secret=${stripeIntentAccessor.intentClientSecret}").use {
|
StripeIntentAccessor.ObjectType.PAYMENT_INTENT -> get(StripePaths.getPaymentIntentPath(stripeIntentAccessor.intentId, stripeIntentAccessor.intentClientSecret)).use {
|
||||||
val body = it.body?.string()
|
val body = it.body?.string()
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Reading StripePaymentIntent from JSON")
|
Log.d(TAG, "Reading StripePaymentIntent from JSON")
|
||||||
|
@ -229,7 +229,7 @@ class StripeApi(
|
||||||
CARD_CVC_KEY to cardData.cvc
|
CARD_CVC_KEY to cardData.cvc
|
||||||
)
|
)
|
||||||
|
|
||||||
postForm("tokens", parameters).use { response ->
|
postForm(StripePaths.getTokensPath(), parameters).use { response ->
|
||||||
val body = response.body ?: throw StripeError.FailedToCreatePaymentSourceFromCardData
|
val body = response.body ?: throw StripeError.FailedToCreatePaymentSourceFromCardData
|
||||||
return CreditCardPaymentSource(JSONObject(body.string()))
|
return CreditCardPaymentSource(JSONObject(body.string()))
|
||||||
}
|
}
|
||||||
|
@ -257,7 +257,7 @@ class StripeApi(
|
||||||
"billing_details[name]" to paymentSource.sepaDebitData.name
|
"billing_details[name]" to paymentSource.sepaDebitData.name
|
||||||
)
|
)
|
||||||
|
|
||||||
return postForm("payment_methods", parameters)
|
return postForm(StripePaths.getPaymentMethodsPath(), parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createPaymentMethodForIDEAL(paymentSource: IDEALPaymentSource): Response {
|
private fun createPaymentMethodForIDEAL(paymentSource: IDEALPaymentSource): Response {
|
||||||
|
@ -268,7 +268,7 @@ class StripeApi(
|
||||||
"billing_details[name]" to paymentSource.idealData.name
|
"billing_details[name]" to paymentSource.idealData.name
|
||||||
)
|
)
|
||||||
|
|
||||||
return postForm("payment_methods", parameters)
|
return postForm(StripePaths.getPaymentMethodsPath(), parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createPaymentMethodForToken(paymentSource: PaymentSource): Response {
|
private fun createPaymentMethodForToken(paymentSource: PaymentSource): Response {
|
||||||
|
@ -278,7 +278,7 @@ class StripeApi(
|
||||||
"type" to "card"
|
"type" to "card"
|
||||||
)
|
)
|
||||||
|
|
||||||
return postForm("payment_methods", parameters)
|
return postForm(StripePaths.getPaymentMethodsPath(), parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun get(endpoint: String): Response {
|
private fun get(endpoint: String): Response {
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.donations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint generation class that assists in ensuring test code utilizes the same
|
||||||
|
* paths for data access as production code.
|
||||||
|
*/
|
||||||
|
object StripePaths {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint to retrieve data on the given payment intent
|
||||||
|
*/
|
||||||
|
fun getPaymentIntentPath(paymentIntentId: String, clientSecret: String): String {
|
||||||
|
return "payment_intents/$paymentIntentId?client_secret=$clientSecret"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint to confirm the given payment intent
|
||||||
|
*/
|
||||||
|
fun getPaymentIntentConfirmationPath(paymentIntentId: String): String {
|
||||||
|
return "payment_intents/$paymentIntentId/confirm"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint to retrieve data on the given setup intent
|
||||||
|
*/
|
||||||
|
fun getSetupIntentPath(setupIntentId: String, clientSecret: String): String {
|
||||||
|
return "setup_intents/$setupIntentId?client_secret=$clientSecret&expand[0]=latest_attempt"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint to confirm the given setup intent
|
||||||
|
*/
|
||||||
|
fun getSetupIntentConfirmationPath(setupIntentId: String): String {
|
||||||
|
return "setup_intents/$setupIntentId/confirm"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint to interact with payment methods
|
||||||
|
*/
|
||||||
|
fun getPaymentMethodsPath() = "payment_methods"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint to interact with tokens
|
||||||
|
*/
|
||||||
|
fun getTokensPath() = "tokens"
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package org.signal.donations.json
|
package org.signal.donations.json
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator
|
import com.fasterxml.jackson.annotation.JsonCreator
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stripe intent status, from:
|
* Stripe intent status, from:
|
||||||
|
@ -24,4 +25,9 @@ enum class StripeIntentStatus(private val code: String) {
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
fun fromCode(code: String): StripeIntentStatus = entries.first { it.code == code }
|
fun fromCode(code: String): StripeIntentStatus = entries.first { it.code == code }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
fun toValue(): String {
|
||||||
|
return code
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue