Donation CreatePaymentMethod 409 error recovery.

This commit is contained in:
Alex Hart
2023-09-27 15:24:45 -04:00
committed by Cody Henthorne
parent c409d49f14
commit 4d640ec467
3 changed files with 72 additions and 8 deletions

View File

@@ -79,9 +79,29 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
}.subscribeOn(Schedulers.io())
}
fun ensureSubscriberId(): Completable {
Log.d(TAG, "Ensuring SubscriberId exists on Signal service...", true)
val subscriberId = SignalStore.donationsValues().getSubscriber()?.subscriberId ?: SubscriberId.generate()
/**
* Since PayPal and Stripe can't interoperate, we need to be able to rotate the subscriber ID
* in case of failures.
*/
fun rotateSubscriberId(): Completable {
Log.d(TAG, "Rotating SubscriberId due to alternate payment processor...", true)
val cancelCompletable: Completable = if (SignalStore.donationsValues().getSubscriber() != null) {
cancelActiveSubscription().andThen(updateLocalSubscriptionStateAndScheduleDataSync())
} else {
Completable.complete()
}
return cancelCompletable.andThen(ensureSubscriberId(isRotation = true))
}
fun ensureSubscriberId(isRotation: Boolean = false): Completable {
Log.d(TAG, "Ensuring SubscriberId exists on Signal service {isRotation?$isRotation}...", true)
val subscriberId: SubscriberId = if (isRotation) {
SubscriberId.generate()
} else {
SignalStore.donationsValues().getSubscriber()?.subscriberId ?: SubscriberId.generate()
}
return Single
.fromCallable {
donationsService.putSubscription(subscriberId)
@@ -222,4 +242,18 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
levelUpdateOperation
}
}
/**
* Update local state information and schedule a storage sync for the change. This method
* assumes you've already properly called the DELETE method for the stored ID on the server.
*/
private fun updateLocalSubscriptionStateAndScheduleDataSync(): Completable {
return Completable.fromAction {
Log.d(TAG, "Marking subscription cancelled...", true)
SignalStore.donationsValues().updateLocalStateForManualCancellation()
MultiDeviceSubscriptionSyncRequestJob.enqueue()
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
}

View File

@@ -29,6 +29,8 @@ class PayPalRepository(private val donationsService: DonationsService) {
private val TAG = Log.tag(PayPalRepository::class.java)
}
private val monthlyDonationRepository = MonthlyDonationRepository(donationsService)
fun createOneTimePaymentIntent(
amount: FiatMoney,
badgeRecipient: RecipientId,
@@ -69,7 +71,12 @@ class PayPalRepository(private val donationsService: DonationsService) {
}.flatMap { it.flattenResult() }.subscribeOn(Schedulers.io())
}
fun createPaymentMethod(): Single<PayPalCreatePaymentMethodResponse> {
/**
* Creates the PaymentMethod via the Signal Service. Note that if the operation fails with a 409,
* it means that the PaymentMethod is already tied to a Stripe account. We can retry in this
* situation by simply deleting the old subscriber id on the service and replacing it.
*/
fun createPaymentMethod(retryOn409: Boolean = true): Single<PayPalCreatePaymentMethodResponse> {
return Single.fromCallable {
donationsService.createPayPalPaymentMethod(
Locale.getDefault(),
@@ -77,7 +84,13 @@ class PayPalRepository(private val donationsService: DonationsService) {
MONTHLY_RETURN_URL,
CANCEL_URL
)
}.flatMap { it.flattenResult() }.subscribeOn(Schedulers.io())
}.flatMap { serviceResponse ->
if (retryOn409 && serviceResponse.status == 409) {
monthlyDonationRepository.rotateSubscriberId().andThen(createPaymentMethod(retryOn409 = false))
} else {
serviceResponse.flattenResult()
}
}.subscribeOn(Schedulers.io())
}
fun setDefaultPaymentMethod(paymentMethodId: String): Completable {

View File

@@ -47,6 +47,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION)
private val stripeApi = StripeApi(Environment.Donations.STRIPE_CONFIGURATION, this, this, ApplicationDependencies.getOkHttpClient())
private val monthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService())
fun isGooglePayAvailable(): Completable {
return googlePayApi.queryIsReadyToPay()
@@ -153,8 +154,12 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
}
}
override fun fetchSetupIntent(): Single<StripeIntentAccessor> {
Log.d(TAG, "Fetching setup intent from Signal service...")
/**
* Creates the PaymentMethod via the Signal Service. Note that if the operation fails with a 409,
* it means that the PaymentMethod is already tied to a PayPal account. We can retry in this
* situation by simply deleting the old subscriber id on the service and replacing it.
*/
private fun createPaymentMethod(retryOn409: Boolean = true): Single<StripeClientSecret> {
return Single.fromCallable { SignalStore.donationsValues().requireSubscriber() }
.flatMap {
Single.fromCallable {
@@ -163,7 +168,18 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
.createStripeSubscriptionPaymentMethod(it.subscriberId)
}
}
.flatMap(ServiceResponse<StripeClientSecret>::flattenResult)
.flatMap { serviceResponse ->
if (retryOn409 && serviceResponse.status == 409) {
monthlyDonationRepository.rotateSubscriberId().andThen(createPaymentMethod(retryOn409 = false))
} else {
serviceResponse.flattenResult()
}
}
}
override fun fetchSetupIntent(): Single<StripeIntentAccessor> {
Log.d(TAG, "Fetching setup intent from Signal service...")
return createPaymentMethod()
.map {
StripeIntentAccessor(
objectType = StripeIntentAccessor.ObjectType.SETUP_INTENT,
@@ -191,6 +207,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
}
StatusAndPaymentMethodId(it.status ?: StripeIntentStatus.SUCCEEDED, it.paymentMethod)
}
StripeIntentAccessor.ObjectType.SETUP_INTENT -> stripeApi.getSetupIntent(stripeIntentAccessor).let {
StatusAndPaymentMethodId(it.status, it.paymentMethod)
}