mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 05:08:03 +01:00
Propagate certain subscription processor errors to client responses
This commit is contained in:
@@ -49,6 +49,8 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BraintreeManager.class);
|
||||
|
||||
private static final String GENERIC_DECLINED_PROCESSOR_CODE = "2046";
|
||||
private static final String PAYPAL_FUNDING_INSTRUMENT_DECLINED_PROCESSOR_CODE = "2074";
|
||||
private static final String PAYPAL_PAYMENT_ALREADY_COMPLETED_PROCESSOR_CODE = "2094";
|
||||
private final BraintreeGateway braintreeGateway;
|
||||
private final BraintreeGraphqlClient braintreeGraphqlClient;
|
||||
@@ -184,11 +186,18 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
new PayPalChargeSuccessDetails(successfulTx.getGraphQLId()));
|
||||
}
|
||||
|
||||
logger.info("PayPal charge unexpectedly failed: {}", unsuccessfulTx.getProcessorResponseCode());
|
||||
return switch (unsuccessfulTx.getProcessorResponseCode()) {
|
||||
case GENERIC_DECLINED_PROCESSOR_CODE, PAYPAL_FUNDING_INSTRUMENT_DECLINED_PROCESSOR_CODE ->
|
||||
CompletableFuture.failedFuture(
|
||||
new SubscriptionProcessorException(getProcessor(), createChargeFailure(unsuccessfulTx)));
|
||||
|
||||
return CompletableFuture.failedFuture(
|
||||
new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR));
|
||||
default -> {
|
||||
logger.info("PayPal charge unexpectedly failed: {}", unsuccessfulTx.getProcessorResponseCode());
|
||||
|
||||
yield CompletableFuture.failedFuture(
|
||||
new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
};
|
||||
}, executor));
|
||||
}
|
||||
|
||||
@@ -240,12 +249,6 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
|
||||
}
|
||||
|
||||
private void assertResultSuccess(Result<?> result) throws CompletionException {
|
||||
if (!result.isSuccess()) {
|
||||
throw new CompletionException(new BraintreeException(result.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ProcessorCustomer> createCustomer(final byte[] subscriberUser) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
@@ -258,7 +261,9 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
}
|
||||
}, executor)
|
||||
.thenApply(result -> {
|
||||
assertResultSuccess(result);
|
||||
if (!result.isSuccess()) {
|
||||
throw new CompletionException(new BraintreeException(result.getMessage()));
|
||||
}
|
||||
|
||||
return new ProcessorCustomer(result.getTarget().getId(), SubscriptionProcessor.BRAINTREE);
|
||||
});
|
||||
@@ -336,7 +341,19 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
.done()
|
||||
);
|
||||
|
||||
assertResultSuccess(result);
|
||||
if (!result.isSuccess()) {
|
||||
final CompletionException completionException;
|
||||
if (result.getTarget() != null) {
|
||||
completionException = result.getTarget().getTransactions().stream().findFirst()
|
||||
.map(transaction -> new CompletionException(
|
||||
new SubscriptionProcessorException(getProcessor(), createChargeFailure(transaction))))
|
||||
.orElseGet(() -> new CompletionException(new BraintreeException(result.getMessage())));
|
||||
} else {
|
||||
completionException = new CompletionException(new BraintreeException(result.getMessage()));
|
||||
}
|
||||
|
||||
throw completionException;
|
||||
}
|
||||
|
||||
return result.getTarget();
|
||||
}));
|
||||
@@ -358,7 +375,7 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
}
|
||||
|
||||
// since badge redemption is untrackable by design and unrevokable, subscription changes must be immediate and
|
||||
// and not prorated. Braintree subscriptions cannot change their next billing date,
|
||||
// not prorated. Braintree subscriptions cannot change their next billing date,
|
||||
// so we must end the existing one and create a new one
|
||||
return cancelSubscriptionAtEndOfCurrentPeriod(subscription)
|
||||
.thenCompose(ignored -> {
|
||||
@@ -413,37 +430,13 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
final Instant anchor = subscription.getFirstBillingDate().toInstant();
|
||||
final Instant endOfCurrentPeriod = subscription.getBillingPeriodEndDate().toInstant();
|
||||
|
||||
final Optional<Transaction> maybeTransaction = getLatestTransactionForSubscription(subscription);
|
||||
|
||||
final ChargeFailure chargeFailure = maybeTransaction.map(transaction -> {
|
||||
|
||||
final ChargeFailure chargeFailure = getLatestTransactionForSubscription(subscription).map(transaction -> {
|
||||
if (getPaymentStatus(transaction.getStatus()).equals(PaymentStatus.SUCCEEDED)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String code;
|
||||
final String message;
|
||||
if (transaction.getProcessorResponseCode() != null) {
|
||||
code = transaction.getProcessorResponseCode();
|
||||
message = transaction.getProcessorResponseText();
|
||||
} else if (transaction.getGatewayRejectionReason() != null) {
|
||||
code = "gateway";
|
||||
message = transaction.getGatewayRejectionReason().toString();
|
||||
} else {
|
||||
code = "unknown";
|
||||
message = "unknown";
|
||||
}
|
||||
|
||||
return new ChargeFailure(
|
||||
code,
|
||||
message,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
return createChargeFailure(transaction);
|
||||
}).orElse(null);
|
||||
|
||||
|
||||
return new SubscriptionInformation(
|
||||
new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT),
|
||||
SubscriptionCurrencyUtil.convertBraintreeAmountToApiAmount(plan.getCurrencyIsoCode(), plan.getPrice())),
|
||||
@@ -458,6 +451,29 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
}, executor);
|
||||
}
|
||||
|
||||
private ChargeFailure createChargeFailure(Transaction transaction) {
|
||||
|
||||
final String code;
|
||||
final String message;
|
||||
if (transaction.getProcessorResponseCode() != null) {
|
||||
code = transaction.getProcessorResponseCode();
|
||||
message = transaction.getProcessorResponseText();
|
||||
} else if (transaction.getGatewayRejectionReason() != null) {
|
||||
code = "gateway";
|
||||
message = transaction.getGatewayRejectionReason().toString();
|
||||
} else {
|
||||
code = "unknown";
|
||||
message = "unknown";
|
||||
}
|
||||
|
||||
return new ChargeFailure(
|
||||
code,
|
||||
message,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> cancelAllActiveSubscriptions(String customerId) {
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public record ChargeFailure(String code, String message, @Nullable String outcomeNetworkStatus,
|
||||
@Nullable String outcomeReason, @Nullable String outcomeType) {
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.subscriptions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.stripe.StripeClient;
|
||||
import com.stripe.exception.CardException;
|
||||
import com.stripe.exception.StripeException;
|
||||
import com.stripe.model.Charge;
|
||||
import com.stripe.model.Customer;
|
||||
@@ -268,6 +269,18 @@ public class StripeManager implements SubscriptionProcessorManager {
|
||||
.create(params, commonOptions(generateIdempotencyKeyForCreateSubscription(
|
||||
customerId, lastSubscriptionCreatedAt)));
|
||||
} catch (StripeException e) {
|
||||
|
||||
if (e instanceof CardException ce) {
|
||||
throw new CompletionException(new SubscriptionProcessorException(getProcessor(),
|
||||
new ChargeFailure(
|
||||
StringUtils.defaultIfBlank(ce.getDeclineCode(), ce.getCode()),
|
||||
e.getStripeError().getMessage(),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)));
|
||||
}
|
||||
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}, executor)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionProcessorException extends Exception {
|
||||
|
||||
private final SubscriptionProcessor processor;
|
||||
private final ChargeFailure chargeFailure;
|
||||
|
||||
public SubscriptionProcessorException(final SubscriptionProcessor processor,
|
||||
final ChargeFailure chargeFailure) {
|
||||
this.processor = processor;
|
||||
this.chargeFailure = chargeFailure;
|
||||
}
|
||||
|
||||
public SubscriptionProcessor getProcessor() {
|
||||
return processor;
|
||||
}
|
||||
|
||||
public ChargeFailure getChargeFailure() {
|
||||
return chargeFailure;
|
||||
}
|
||||
}
|
||||
@@ -155,11 +155,6 @@ public interface SubscriptionProcessorManager {
|
||||
|
||||
}
|
||||
|
||||
record ChargeFailure(String code, String message, @Nullable String outcomeNetworkStatus,
|
||||
@Nullable String outcomeReason, @Nullable String outcomeType) {
|
||||
|
||||
}
|
||||
|
||||
record ReceiptItem(String itemId, Instant expiration, long level) {
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user