Add PayPal decline code errors.

main
Alex Hart 2023-01-23 16:25:04 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 88da382a6f
commit 0303467c91
4 zmienionych plików z 212 dodań i 7 usunięć

Wyświetl plik

@ -62,6 +62,16 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
* Payment failed by the credit card processor, with a specific reason told to us by Stripe.
*/
class StripeDeclinedError(source: DonationErrorSource, cause: Throwable, val declineCode: StripeDeclineCode, val method: PaymentSourceType.Stripe) : PaymentSetupError(source, cause)
/**
* Payment setup failed in some way, which we are told about by PayPal.
*/
class PayPalCodedError(source: DonationErrorSource, cause: Throwable, val errorCode: Int) : PaymentSetupError(source, cause)
/**
* Payment failed by the credit card processor, with a specific reason told to us by PayPal.
*/
class PayPalDeclinedError(source: DonationErrorSource, cause: Throwable, val code: PayPalDeclineCode.KnownCode) : PaymentSetupError(source, cause)
}
/**

Wyświetl plik

@ -0,0 +1,128 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.errors
/**
* From: https://developer.paypal.com/braintree/docs/reference/general/processor-responses/authorization-responses#decline-codes
*/
data class PayPalDeclineCode(
val code: Int
) {
val knownCode: KnownCode? = KnownCode.fromCode(code)
enum class KnownCode(val code: Int) {
DO_NOT_HONOR(2000),
INSUFFICIENT_FUNDS(2001),
LIMIT_EXCEEDED(2002),
CARDHOLDER_ACTIVITY_LIMIT_EXCEEDED(2003),
EXPIRED_CARD(2004),
INVALID_CREDIT_CARD(2005),
INVALID_EXPIRATION_DATE(2006),
NO_ACCOUNT(2007),
CARD_ACCOUNT_LENGTH_ERROR(2008),
NO_SUCH_ISSUER(2009),
CARD_ISSUER_DECLINED_CVV(2010),
VOICE_AUTHORIZATION_REQUIRED(2011),
PROCESSOR_DECLINED_POSSIBLE_LOST_CARD(2012),
PROCESSOR_DECLINED_POSSIBLE_STOLEN_CARD(2013),
PROCESSOR_DECLINED_FRAUD_SUSPECTED(2014),
TRANSACTION_NOT_ALLOWED(2015),
DUPLICATE_TRANSACTION(2016),
CARDHOLDER_STOPPED_BILLING(2017),
CARDHOLDER_STOPPED_ALL_BILLING(2018),
INVALID_TRANSACTION(2019),
VIOLATION(2020),
SECURITY_VIOLATION(2021),
DECLINED_UPDATED_CARDHOLDER_AVAILABLE(2022),
PROCESSOR_DOES_NOT_SUPPORT_THIS_FEATURE(2023),
CARD_TYPE_NOT_ENABLED(2024),
SET_UP_ERROR_MERCHANT(2025),
INVALID_MERCHANT_ID(2026),
SET_UP_ERROR_AMOUNT(2027),
SET_UP_ERROR_HIERARCHY(2028),
SET_UP_ERROR_CARD(2029),
SET_UP_ERROR_TERMINAL(2030),
ENCRYPTION_ERROR(2031),
SURCHARGE_NOT_PERMITTED(2032),
INCONSISTENT_DATA(2033),
NO_ACTION_TAKEN(2034),
PARTIAL_APPROVAL_FOR_AMOUNT_IN_GROUP_3_VERSION(2035),
AUTHORIZATION_COULD_NOT_BE_FOUND(2036),
ALREADY_REVERSED(2037),
PROCESSOR_DECLINED(2038),
INVALID_AUTHORIZATION_CODE(2039),
INVALID_STORE(2040),
DECLINED_CALL_FOR_APPROVAL(2041),
INVALID_CLIENT_ID(2042),
ERROR_DO_NOT_RETRY_CALL_ISSUER(2043),
DECLINED_CALL_ISSUER(2044),
INVALID_MERCHANT_NUMBER(2045),
DECLINED(2046),
CALL_ISSUER_PICK_UP_CARD(2047),
INVALID_AMOUNT(2048),
INVALID_SKU_NUMBER(2049),
INVALID_CREDIT_PLAN(2050),
CREDIT_CARD_NUMBER_DOES_NOT_MATCH_METHOD_OF_PAYMENT(2051),
INVALID_LEVEL_3_PURCHASE(2052),
CARD_REPORTED_AS_LOST_OR_STOLEN(2053),
REVERSAL_AMOUNT_DOES_NOT_MATCH_AUTHORIZATION_AMOUNT(2054),
INVALID_TRANSACTION_DIVISION_NUMBER(2055),
TRANSACTION_AMOUNT_EXCEEDS_THE_TRANSACTION_DIVISION_LIMIT(2056),
ISSUER_OR_CARDHOLDER_HAS_PUT_A_RESTRICTION_ON_THE_CARD(2057),
MERCHANT_NOT_MASTERCARD_SECURECODE_ENABLED(2058),
ADDRESS_VERIFICATION_FAILED(2059),
ADDRESS_VERIFICATION_AND_CARD_SECURITY_CODE_FAILED(2060),
INVALID_TRANSACTION_DATA(2061),
INVALID_TAX_AMOUNT(2062),
PAYPAL_BUSINESS_ACCOUNT_PREFERENCE_RESULTED_IN_THE_TRANSACTION_FAILING(2063),
INVALID_CURRENCY_CODE(2064),
REFUND_TIME_LIMIT_EXCEEDED(2065),
PAYPAL_BUSINESS_ACCOUNT_RESTRICTED(2066),
AUTHORIZATION_EXPIRED(2067),
PAYPAL_BUSINESS_ACCOUNT_LOCKED_OR_CLOSED(2068),
PAYPAL_BLOCKING_DUPLICATE_ORDER_IDS(2069),
PAYPAL_BUYER_REVOKED_PRE_APPROVED_PAYMENT_AUTHORIZATION(2070),
PAYPAL_PAYEE_ACCOUNT_INVALID_OR_DOES_NOT_HAVE_A_VERIFIED_EMAIL(2071),
PAYPAL_PAYEE_EMAIL_INCORRECTLY_FORMATTED(2072),
PAYPAL_VALIDATION_ERROR(2073),
FUNDING_INSTRUMENT_IN_THE_PAYPAL_ACCOUNT_WAS_DECLINED_BY_THE_PROCESSR_OR_BANK_OR_IT_CANT_BE_USED_FOR_THIS_PAYMENT(2074),
PAYER_ACCOUNT_IS_LOCKED_OR_CLOSED(2075),
PAYER_CANNOT_PAY_FOR_THIS_TRANSACTION_WITH_PAYPAL(2076),
TRANSACTION_REFUSED_DUE_TO_PAYPAL_RISK_MODEL(2077),
INVALID_SECURE_PAYMENT_DATA(2078),
PAYPAL_MERCHANT_ACCOUNT_CONFIGURATION_ERROR(2079),
INVALID_USER_CREDENTIALS(2080),
PAYPAL_PENDING_PAYMENTS_ARE_NOT_SUPPORTED(2081),
PAYPAL_DOMESTIC_TRANSACTION_REQUIRED(2082),
PAYPAL_PHONE_NUMBER_REQUIRED(2083),
PAYPAL_TAX_INFO_REQUIRED(2084),
PAYPAL_PAYEE_BLOCKED_TRANSACTION(2085),
PAYPAL_TRANSACTION_LIMIT_EXCEEDED(2086),
PAYPAL_REFERENCE_TRANSACTIONS_ARE_NOT_ENABLED_FOR_YOUR_ACCOUNT(2087),
CURRENCY_NOT_ENABLED_FOR_YOUR_PAYPAL_SELLER_ACCOUNT(2088),
PAYPAL_PAYEE_EMAIL_PERMISSION_DENIED_FOR_THIS_REQUEST(2089),
PAYPAL_OR_VENMO_ACCOUNT_NOT_CONFIGURED_TO_REFUND_MORE_THAN_SETTLED_AMOUNT(2090),
CURRENCY_OF_THIS_TRANSACTION_MUST_MATCH_CURRENCY_OF_YOUR_PAYPAL_ACCOUNT(2091),
NO_DATA_FOUND_TRY_ANOTHER_VERIFICATION_METHOD(2092),
PAYPAL_PAYMENT_METHOD_IS_INVALID(2093),
PAYPAL_PAYMENT_HAS_ALREADY_BEEN_COMPLETED(2094),
PAYPAL_REFUND_IS_NOT_ALLOWED_AFTER_PARTIAL_REFUND(2095),
PAYPAL_BUYER_ACCOUNT_CANT_BE_THE_SAME_AS_THE_SELLER_ACCOUNT(2096),
PAYPAL_AUTHORIZATION_AMOUNT_LIMIT_EXCEEDED(2097),
PAYPAL_AUTHORIZATION_COUNT_LIMIT_EXCEEDED(2098),
CARDHOLDER_AUTHORIZATION_REQUIRED(2099),
PAYPAL_CHANNEL_INITIATED_BILLING_NOT_ENABLED_FOR_YOUR_ACCOUNT(2100),
ADDITIONAL_AUTHORIZATION_REQUIRED(2101),
INCORRECT_PIN(2102),
PIN_TRY_EXCEEDED(2103),
OFFLINE_ISSUER_DECLINED(2104),
CANNOT_AUTHORIZE_AT_THIS_TIME_LIFE_CYCLE(2105),
CANNOT_AUTHORIZE_AT_THIS_TIME_POLICY(2106),
CARD_NOT_ACTIVATED(2107),
CLOSED_CARD(2108),
PROCESSOR_NETWORK_UNAVAILABLE_TRY_AGAIN(3000);
companion object {
fun fromCode(code: Int): KnownCode? = values().firstOrNull { it.code == code }
}
}
}

Wyświetl plik

@ -16,6 +16,7 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError;
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource;
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.PayPalDeclineCode;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -139,11 +140,11 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
if (isForKeepAlive) {
Log.w(TAG, "Subscription payment failure in active subscription response (status = " + subscription.getStatus() + ").", true);
onPaymentFailure(subscription.getStatus(), chargeFailure, subscription.getEndOfCurrentPeriod(), true);
onPaymentFailure(subscription.getStatus(), subscription.getProcessor(), chargeFailure, subscription.getEndOfCurrentPeriod(), true);
throw new Exception("Active subscription hit a payment failure: " + subscription.getStatus());
} else {
Log.w(TAG, "New subscription has hit a payment failure. (status = " + subscription.getStatus() + ").", true);
onPaymentFailure(subscription.getStatus(), chargeFailure, subscription.getEndOfCurrentPeriod(), false);
onPaymentFailure(subscription.getStatus(), subscription.getProcessor(), chargeFailure, subscription.getEndOfCurrentPeriod(), false);
throw new Exception("New subscription has hit a payment failure: " + subscription.getStatus());
}
} else if (!subscription.isActive()) {
@ -153,7 +154,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
if (!isForKeepAlive) {
Log.w(TAG, "Initial subscription payment failed, treating as a permanent failure.");
onPaymentFailure(subscription.getStatus(), chargeFailure, subscription.getEndOfCurrentPeriod(), false);
onPaymentFailure(subscription.getStatus(), subscription.getProcessor(), chargeFailure, subscription.getEndOfCurrentPeriod(), false);
throw new Exception("New subscription has hit a payment failure.");
}
}
@ -283,7 +284,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
* 1. In the case of a keep-alive event, we want to book-keep the error to show the user on a subsequent launch, and we want to sync our failure state to
* linked devices.
*/
private void onPaymentFailure(@NonNull String status, @Nullable ActiveSubscription.ChargeFailure chargeFailure, long timestamp, boolean isForKeepAlive) {
private void onPaymentFailure(@NonNull String status, @NonNull ActiveSubscription.Processor processor, @Nullable ActiveSubscription.ChargeFailure chargeFailure, long timestamp, boolean isForKeepAlive) {
SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true);
if (isForKeepAlive) {
Log.d(TAG, "Is for a keep-alive and we have a status. Setting UnexpectedSubscriptionCancelation state...", true);
@ -291,8 +292,8 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationReason(status);
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationTimestamp(timestamp);
MultiDeviceSubscriptionSyncRequestJob.enqueue();
} else if (chargeFailure != null) {
Log.d(TAG, "Charge failure detected: " + chargeFailure, true);
} else if (chargeFailure != null && processor == ActiveSubscription.Processor.STRIPE) {
Log.d(TAG, "Stripe charge failure detected: " + chargeFailure, true);
StripeDeclineCode declineCode = StripeDeclineCode.Companion.getFromCode(chargeFailure.getOutcomeNetworkReason());
DonationError.PaymentSetupError paymentSetupError;
@ -319,6 +320,44 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
);
}
Log.w(TAG, "Not for a keep-alive and we have a charge failure. Routing a payment setup error...", true);
DonationError.routeDonationError(context, paymentSetupError);
} else if (chargeFailure != null && processor == ActiveSubscription.Processor.BRAINTREE) {
Log.d(TAG, "PayPal charge failure detected: " + chargeFailure, true);
int code;
try {
code = Integer.parseInt(chargeFailure.getCode());
} catch (NumberFormatException e) {
Log.w(TAG, "PayPal charge failure code had unexpected type.");
code = -1;
}
PayPalDeclineCode declineCode = new PayPalDeclineCode(code);
DonationError.PaymentSetupError paymentSetupError;
PaymentSourceType paymentSourceType = SignalStore.donationsValues().getSubscriptionPaymentSourceType();
boolean isPayPalSource = paymentSourceType instanceof PaymentSourceType.PayPal;
if (declineCode.getKnownCode() != null && isPayPalSource) {
paymentSetupError = new DonationError.PaymentSetupError.PayPalDeclinedError(
getErrorSource(),
new Exception(chargeFailure.getMessage()),
declineCode.getKnownCode()
);
} else if (isPayPalSource) {
paymentSetupError = new DonationError.PaymentSetupError.PayPalCodedError(
getErrorSource(),
new Exception("Card was declined. " + chargeFailure.getCode()),
code
);
} else {
paymentSetupError = new DonationError.PaymentSetupError.GenericError(
getErrorSource(),
new Exception("Payment Failed for " + paymentSourceType.getCode())
);
}
Log.w(TAG, "Not for a keep-alive and we have a charge failure. Routing a payment setup error...", true);
DonationError.routeDonationError(context, paymentSetupError);
} else {

Wyświetl plik

@ -15,6 +15,27 @@ public final class ActiveSubscription {
public static final ActiveSubscription EMPTY = new ActiveSubscription(null, null);
public enum Processor {
STRIPE("STRIPE"),
BRAINTREE("BRAINTREE");
private final String code;
Processor(String code) {
this.code = code;
}
static Processor fromCode(String code) {
for (Processor value : Processor.values()) {
if (value.code.equals(code)) {
return value;
}
}
return STRIPE;
}
}
private enum Status {
/**
* The subscription is currently in a trial period and it's safe to provision your product for your customer.
@ -121,6 +142,7 @@ public final class ActiveSubscription {
private final long billingCycleAnchor;
private final boolean willCancelAtPeriodEnd;
private final String status;
private final Processor processor;
@JsonCreator
public Subscription(@JsonProperty("level") int level,
@ -130,7 +152,8 @@ public final class ActiveSubscription {
@JsonProperty("active") boolean isActive,
@JsonProperty("billingCycleAnchor") long billingCycleAnchor,
@JsonProperty("cancelAtPeriodEnd") boolean willCancelAtPeriodEnd,
@JsonProperty("status") String status)
@JsonProperty("status") String status,
@JsonProperty("processor") String processor)
{
this.level = level;
this.currency = currency;
@ -140,6 +163,7 @@ public final class ActiveSubscription {
this.billingCycleAnchor = billingCycleAnchor;
this.willCancelAtPeriodEnd = willCancelAtPeriodEnd;
this.status = status;
this.processor = Processor.fromCode(processor);
}
public int getLevel() {
@ -190,6 +214,10 @@ public final class ActiveSubscription {
return status;
}
public Processor getProcessor() {
return processor;
}
public boolean isInProgress() {
return !isActive() && !Status.isPaymentFailed(getStatus());
}