remove HTTP layer exceptions from Stripe/Braintree managers
This commit is contained in:
parent
50bd30fb1f
commit
237d0fd4e2
|
@ -182,7 +182,6 @@ import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper
|
||||||
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.SubscriptionExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.SubscriptionExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.SubscriptionProcessorExceptionMapper;
|
|
||||||
import org.whispersystems.textsecuregcm.metrics.MessageMetrics;
|
import org.whispersystems.textsecuregcm.metrics.MessageMetrics;
|
||||||
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
|
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
|
||||||
import org.whispersystems.textsecuregcm.metrics.MetricsHttpChannelListener;
|
import org.whispersystems.textsecuregcm.metrics.MetricsHttpChannelListener;
|
||||||
|
@ -1217,7 +1216,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
new ImpossiblePhoneNumberExceptionMapper(),
|
new ImpossiblePhoneNumberExceptionMapper(),
|
||||||
new NonNormalizedPhoneNumberExceptionMapper(),
|
new NonNormalizedPhoneNumberExceptionMapper(),
|
||||||
new RegistrationServiceSenderExceptionMapper(),
|
new RegistrationServiceSenderExceptionMapper(),
|
||||||
new SubscriptionProcessorExceptionMapper(),
|
|
||||||
new SubscriptionExceptionMapper(),
|
new SubscriptionExceptionMapper(),
|
||||||
new JsonMappingExceptionMapper()
|
new JsonMappingExceptionMapper()
|
||||||
).forEach(exceptionMapper -> {
|
).forEach(exceptionMapper -> {
|
||||||
|
|
|
@ -5,17 +5,48 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.mappers;
|
package org.whispersystems.textsecuregcm.mappers;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import io.dropwizard.jersey.errors.ErrorMessage;
|
import io.dropwizard.jersey.errors.ErrorMessage;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.ext.ExceptionMapper;
|
import javax.ws.rs.ext.ExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class SubscriptionExceptionMapper implements ExceptionMapper<SubscriptionException> {
|
public class SubscriptionExceptionMapper implements ExceptionMapper<SubscriptionException> {
|
||||||
|
@VisibleForTesting
|
||||||
|
public static final int PROCESSOR_ERROR_STATUS_CODE = 440;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response toResponse(final SubscriptionException exception) {
|
public Response toResponse(final SubscriptionException exception) {
|
||||||
|
|
||||||
|
// Some exceptions have specific error body formats
|
||||||
|
if (exception instanceof SubscriptionException.AmountTooSmall e) {
|
||||||
|
return Response
|
||||||
|
.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "amount_too_small"))
|
||||||
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
if (exception instanceof SubscriptionException.ProcessorException e) {
|
||||||
|
return Response.status(PROCESSOR_ERROR_STATUS_CODE)
|
||||||
|
.entity(Map.of(
|
||||||
|
"processor", e.getProcessor().name(),
|
||||||
|
"chargeFailure", e.getChargeFailure()
|
||||||
|
))
|
||||||
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
if (exception instanceof SubscriptionException.ChargeFailurePaymentRequired e) {
|
||||||
|
return Response
|
||||||
|
.status(Response.Status.PAYMENT_REQUIRED)
|
||||||
|
.entity(Map.of("chargeFailure", e.getChargeFailure()))
|
||||||
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we'll return a generic error message WebApplicationException, with a detailed error if one is provided
|
||||||
final Response.Status status = (switch (exception) {
|
final Response.Status status = (switch (exception) {
|
||||||
case SubscriptionException.NotFound e -> Response.Status.NOT_FOUND;
|
case SubscriptionException.NotFound e -> Response.Status.NOT_FOUND;
|
||||||
case SubscriptionException.Forbidden e -> Response.Status.FORBIDDEN;
|
case SubscriptionException.Forbidden e -> Response.Status.FORBIDDEN;
|
||||||
|
@ -36,5 +67,6 @@ public class SubscriptionExceptionMapper implements ExceptionMapper<Subscription
|
||||||
.fromResponse(wae.getResponse())
|
.fromResponse(wae.getResponse())
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
.entity(new ErrorMessage(wae.getResponse().getStatus(), wae.getLocalizedMessage())).build();
|
.entity(new ErrorMessage(wae.getResponse().getStatus(), wae.getLocalizedMessage())).build();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.mappers;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
import javax.ws.rs.ext.ExceptionMapper;
|
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorException;
|
|
||||||
|
|
||||||
public class SubscriptionProcessorExceptionMapper implements ExceptionMapper<SubscriptionProcessorException> {
|
|
||||||
|
|
||||||
public static final int EXTERNAL_SERVICE_ERROR_STATUS_CODE = 440;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Response toResponse(final SubscriptionProcessorException exception) {
|
|
||||||
return Response.status(EXTERNAL_SERVICE_ERROR_STATUS_CODE)
|
|
||||||
.entity(Map.of(
|
|
||||||
"processor", exception.getProcessor().name(),
|
|
||||||
"chargeFailure", exception.getChargeFailure()
|
|
||||||
))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,6 +6,8 @@ package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.PaymentProvider;
|
||||||
|
|
||||||
public class SubscriptionException extends Exception {
|
public class SubscriptionException extends Exception {
|
||||||
|
|
||||||
|
@ -53,39 +55,91 @@ public class SubscriptionException extends Exception {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class InvalidLevel extends InvalidArguments {
|
public static class InvalidLevel extends InvalidArguments {
|
||||||
|
|
||||||
public InvalidLevel() {
|
public InvalidLevel() {
|
||||||
super(null, null);
|
super(null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class AmountTooSmall extends InvalidArguments {
|
||||||
|
|
||||||
|
public AmountTooSmall() {
|
||||||
|
super(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class PaymentRequiresAction extends InvalidArguments {
|
public static class PaymentRequiresAction extends InvalidArguments {
|
||||||
|
|
||||||
public PaymentRequiresAction(String message) {
|
public PaymentRequiresAction(String message) {
|
||||||
super(message, null);
|
super(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PaymentRequiresAction() {
|
public PaymentRequiresAction() {
|
||||||
super(null, null);
|
super(null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class PaymentRequired extends SubscriptionException {
|
public static class PaymentRequired extends SubscriptionException {
|
||||||
|
|
||||||
public PaymentRequired() {
|
public PaymentRequired() {
|
||||||
super(null, null);
|
super(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PaymentRequired(String message) {
|
public PaymentRequired(String message) {
|
||||||
super(null, message);
|
super(null, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class ChargeFailurePaymentRequired extends SubscriptionException {
|
||||||
|
|
||||||
|
private final ChargeFailure chargeFailure;
|
||||||
|
|
||||||
|
public ChargeFailurePaymentRequired(final ChargeFailure chargeFailure) {
|
||||||
|
super(null, null);
|
||||||
|
this.chargeFailure = chargeFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChargeFailure getChargeFailure() {
|
||||||
|
return chargeFailure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ProcessorException extends SubscriptionException {
|
||||||
|
|
||||||
|
private final PaymentProvider processor;
|
||||||
|
private final ChargeFailure chargeFailure;
|
||||||
|
|
||||||
|
public ProcessorException(final PaymentProvider processor, final ChargeFailure chargeFailure) {
|
||||||
|
super(null, null);
|
||||||
|
this.processor = processor;
|
||||||
|
this.chargeFailure = chargeFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentProvider getProcessor() {
|
||||||
|
return processor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChargeFailure getChargeFailure() {
|
||||||
|
return chargeFailure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempted to retrieve a receipt for a subscription that hasn't yet been charged or the invoice is in the open state
|
* Attempted to retrieve a receipt for a subscription that hasn't yet been charged or the invoice is in the open
|
||||||
|
* state
|
||||||
*/
|
*/
|
||||||
public static class ReceiptRequestedForOpenPayment extends SubscriptionException {
|
public static class ReceiptRequestedForOpenPayment extends SubscriptionException {
|
||||||
|
|
||||||
public ReceiptRequestedForOpenPayment() {
|
public ReceiptRequestedForOpenPayment() {
|
||||||
super(null, null);
|
super(null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ProcessorConflict extends SubscriptionException {
|
public static class ProcessorConflict extends SubscriptionException {
|
||||||
|
public ProcessorConflict() {
|
||||||
|
super(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
public ProcessorConflict(final String message) {
|
public ProcessorConflict(final String message) {
|
||||||
super(null, message);
|
super(null, message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import com.google.cloud.pubsub.v1.PublisherInterface;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.pubsub.v1.PubsubMessage;
|
import com.google.pubsub.v1.PubsubMessage;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
@ -39,9 +40,6 @@ import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.ws.rs.ClientErrorException;
|
|
||||||
import javax.ws.rs.WebApplicationException;
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||||
|
@ -199,8 +197,7 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||||
);
|
);
|
||||||
|
|
||||||
if (search.getMaximumSize() == 0) {
|
if (search.getMaximumSize() == 0) {
|
||||||
return CompletableFuture.failedFuture(
|
return CompletableFuture.failedFuture(ExceptionUtils.wrap(new IOException()));
|
||||||
new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final Transaction successfulTx = search.getFirst();
|
final Transaction successfulTx = search.getFirst();
|
||||||
|
@ -214,13 +211,12 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||||
return switch (unsuccessfulTx.getProcessorResponseCode()) {
|
return switch (unsuccessfulTx.getProcessorResponseCode()) {
|
||||||
case GENERIC_DECLINED_PROCESSOR_CODE, PAYPAL_FUNDING_INSTRUMENT_DECLINED_PROCESSOR_CODE ->
|
case GENERIC_DECLINED_PROCESSOR_CODE, PAYPAL_FUNDING_INSTRUMENT_DECLINED_PROCESSOR_CODE ->
|
||||||
CompletableFuture.failedFuture(
|
CompletableFuture.failedFuture(
|
||||||
new SubscriptionProcessorException(getProvider(), createChargeFailure(unsuccessfulTx)));
|
new SubscriptionException.ProcessorException(getProvider(), createChargeFailure(unsuccessfulTx)));
|
||||||
|
|
||||||
default -> {
|
default -> {
|
||||||
logger.info("PayPal charge unexpectedly failed: {}", unsuccessfulTx.getProcessorResponseCode());
|
logger.info("PayPal charge unexpectedly failed: {}", unsuccessfulTx.getProcessorResponseCode());
|
||||||
|
|
||||||
yield CompletableFuture.failedFuture(
|
yield CompletableFuture.failedFuture(ExceptionUtils.wrap(new IOException()));
|
||||||
new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, executor));
|
}, executor));
|
||||||
|
@ -391,7 +387,7 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||||
return getDefaultPaymentMethod(customerId)
|
return getDefaultPaymentMethod(customerId)
|
||||||
.thenCompose(paymentMethod -> {
|
.thenCompose(paymentMethod -> {
|
||||||
if (paymentMethod == null) {
|
if (paymentMethod == null) {
|
||||||
throw new ClientErrorException(Response.Status.CONFLICT);
|
throw ExceptionUtils.wrap(new SubscriptionException.ProcessorConflict());
|
||||||
}
|
}
|
||||||
|
|
||||||
final Optional<Subscription> maybeExistingSubscription = paymentMethod.getSubscriptions().stream()
|
final Optional<Subscription> maybeExistingSubscription = paymentMethod.getSubscriptions().stream()
|
||||||
|
@ -426,7 +422,7 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||||
if (result.getTarget() != null) {
|
if (result.getTarget() != null) {
|
||||||
completionException = result.getTarget().getTransactions().stream().findFirst()
|
completionException = result.getTarget().getTransactions().stream().findFirst()
|
||||||
.map(transaction -> new CompletionException(
|
.map(transaction -> new CompletionException(
|
||||||
new SubscriptionProcessorException(getProvider(), createChargeFailure(transaction))))
|
new SubscriptionException.ProcessorException(getProvider(), createChargeFailure(transaction))))
|
||||||
.orElseGet(() -> new CompletionException(new BraintreeException(result.getMessage())));
|
.orElseGet(() -> new CompletionException(new BraintreeException(result.getMessage())));
|
||||||
} else {
|
} else {
|
||||||
completionException = new CompletionException(new BraintreeException(result.getMessage()));
|
completionException = new CompletionException(new BraintreeException(result.getMessage()));
|
||||||
|
@ -460,9 +456,8 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||||
return cancelSubscriptionAtEndOfCurrentPeriod(subscription)
|
return cancelSubscriptionAtEndOfCurrentPeriod(subscription)
|
||||||
.thenCompose(ignored -> {
|
.thenCompose(ignored -> {
|
||||||
|
|
||||||
final Transaction transaction = getLatestTransactionForSubscription(subscription).orElseThrow(
|
final Transaction transaction = getLatestTransactionForSubscription(subscription)
|
||||||
() -> new ClientErrorException(
|
.orElseThrow(() -> ExceptionUtils.wrap(new SubscriptionException.ProcessorConflict()));
|
||||||
Response.Status.CONFLICT));
|
|
||||||
|
|
||||||
final Customer customer = transaction.getCustomer();
|
final Customer customer = transaction.getCustomer();
|
||||||
|
|
||||||
|
@ -615,10 +610,7 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||||
if (subscriptionStatus.equals(SubscriptionStatus.ACTIVE) || subscriptionStatus.equals(SubscriptionStatus.PAST_DUE)) {
|
if (subscriptionStatus.equals(SubscriptionStatus.ACTIVE) || subscriptionStatus.equals(SubscriptionStatus.PAST_DUE)) {
|
||||||
throw ExceptionUtils.wrap(new SubscriptionException.ReceiptRequestedForOpenPayment());
|
throw ExceptionUtils.wrap(new SubscriptionException.ReceiptRequestedForOpenPayment());
|
||||||
}
|
}
|
||||||
|
throw ExceptionUtils.wrap(new SubscriptionException.ChargeFailurePaymentRequired(createChargeFailure(transaction)));
|
||||||
throw new WebApplicationException(Response.status(Response.Status.PAYMENT_REQUIRED)
|
|
||||||
.entity(Map.of("chargeFailure", createChargeFailure(transaction)))
|
|
||||||
.build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final Instant paidAt = transaction.getSubscriptionDetails().getBillingPeriodStartDate().toInstant();
|
final Instant paidAt = transaction.getSubscriptionDetails().getBillingPeriodStartDate().toInstant();
|
||||||
|
|
|
@ -40,6 +40,7 @@ import com.stripe.param.SubscriptionRetrieveParams;
|
||||||
import com.stripe.param.SubscriptionUpdateParams;
|
import com.stripe.param.SubscriptionUpdateParams;
|
||||||
import com.stripe.param.SubscriptionUpdateParams.BillingCycleAnchor;
|
import com.stripe.param.SubscriptionUpdateParams.BillingCycleAnchor;
|
||||||
import com.stripe.param.SubscriptionUpdateParams.ProrationBehavior;
|
import com.stripe.param.SubscriptionUpdateParams.ProrationBehavior;
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
@ -55,6 +56,7 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
|
@ -64,12 +66,6 @@ import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import javax.ws.rs.ClientErrorException;
|
|
||||||
import javax.ws.rs.InternalServerErrorException;
|
|
||||||
import javax.ws.rs.NotFoundException;
|
|
||||||
import javax.ws.rs.WebApplicationException;
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
import javax.ws.rs.core.Response.Status;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -201,7 +197,8 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a payment intent. May throw a 400 WebApplicationException if the amount is too small.
|
* Creates a payment intent. May throw a
|
||||||
|
* {@link SubscriptionException.AmountTooSmall} if the amount is too small.
|
||||||
*/
|
*/
|
||||||
public CompletableFuture<PaymentIntent> createPaymentIntent(final String currency,
|
public CompletableFuture<PaymentIntent> createPaymentIntent(final String currency,
|
||||||
final long amount,
|
final long amount,
|
||||||
|
@ -223,10 +220,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||||
return stripeClient.paymentIntents().create(builder.build(), commonOptions());
|
return stripeClient.paymentIntents().create(builder.build(), commonOptions());
|
||||||
} catch (StripeException e) {
|
} catch (StripeException e) {
|
||||||
if ("amount_too_small".equalsIgnoreCase(e.getCode())) {
|
if ("amount_too_small".equalsIgnoreCase(e.getCode())) {
|
||||||
throw new WebApplicationException(Response
|
throw ExceptionUtils.wrap(new SubscriptionException.AmountTooSmall());
|
||||||
.status(Status.BAD_REQUEST)
|
|
||||||
.entity(Map.of("error", "amount_too_small"))
|
|
||||||
.build());
|
|
||||||
} else {
|
} else {
|
||||||
throw new CompletionException(e);
|
throw new CompletionException(e);
|
||||||
}
|
}
|
||||||
|
@ -303,7 +297,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||||
|
|
||||||
if (e instanceof CardException ce) {
|
if (e instanceof CardException ce) {
|
||||||
throw new CompletionException(
|
throw new CompletionException(
|
||||||
new SubscriptionProcessorException(getProvider(), createChargeFailureFromCardException(e, ce)));
|
new SubscriptionException.ProcessorException(getProvider(), createChargeFailureFromCardException(e, ce)));
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new CompletionException(e);
|
throw new CompletionException(e);
|
||||||
|
@ -356,10 +350,10 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||||
} catch (StripeException e) {
|
} catch (StripeException e) {
|
||||||
|
|
||||||
if (e instanceof CardException ce) {
|
if (e instanceof CardException ce) {
|
||||||
throw new CompletionException(
|
throw ExceptionUtils.wrap(
|
||||||
new SubscriptionProcessorException(getProvider(), createChargeFailureFromCardException(e, ce)));
|
new SubscriptionException.ProcessorException(getProvider(), createChargeFailureFromCardException(e, ce)));
|
||||||
}
|
}
|
||||||
throw new CompletionException(e);
|
throw ExceptionUtils.wrap(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, executor)
|
}, executor)
|
||||||
|
@ -385,8 +379,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||||
public CompletableFuture<Void> cancelAllActiveSubscriptions(String customerId) {
|
public CompletableFuture<Void> cancelAllActiveSubscriptions(String customerId) {
|
||||||
return getCustomer(customerId).thenCompose(customer -> {
|
return getCustomer(customerId).thenCompose(customer -> {
|
||||||
if (customer == null) {
|
if (customer == null) {
|
||||||
throw new InternalServerErrorException(
|
throw ExceptionUtils.wrap(new IOException("no customer record found for id " + customerId));
|
||||||
"no customer record found for id " + customerId);
|
|
||||||
}
|
}
|
||||||
return listNonCanceledSubscriptions(customer);
|
return listNonCanceledSubscriptions(customer);
|
||||||
}).thenCompose(subscriptions -> {
|
}).thenCompose(subscriptions -> {
|
||||||
|
@ -617,14 +610,15 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||||
ExceptionUtils.wrap(new SubscriptionException.ReceiptRequestedForOpenPayment()));
|
ExceptionUtils.wrap(new SubscriptionException.ReceiptRequestedForOpenPayment()));
|
||||||
}
|
}
|
||||||
if (!StringUtils.equalsIgnoreCase("paid", latestSubscriptionInvoice.getStatus())) {
|
if (!StringUtils.equalsIgnoreCase("paid", latestSubscriptionInvoice.getStatus())) {
|
||||||
final Response.ResponseBuilder responseBuilder = Response.status(Status.PAYMENT_REQUIRED);
|
return CompletableFuture.failedFuture(ExceptionUtils.wrap(Optional
|
||||||
if (latestSubscriptionInvoice.getChargeObject() != null) {
|
.ofNullable(latestSubscriptionInvoice.getChargeObject())
|
||||||
final Charge charge = latestSubscriptionInvoice.getChargeObject();
|
|
||||||
if (charge.getFailureCode() != null || charge.getFailureMessage() != null) {
|
// If the charge object has a failure reason we can present to the user, create a detailed exception
|
||||||
responseBuilder.entity(Map.of("chargeFailure", createChargeFailure(charge)));
|
.filter(charge -> charge.getFailureCode() != null || charge.getFailureMessage() != null)
|
||||||
}
|
.<SubscriptionException> map(charge -> new SubscriptionException.ChargeFailurePaymentRequired(createChargeFailure(charge)))
|
||||||
}
|
|
||||||
throw new WebApplicationException(responseBuilder.build());
|
// Otherwise, return a generic payment required error
|
||||||
|
.orElseGet(() -> new SubscriptionException.PaymentRequired())));
|
||||||
}
|
}
|
||||||
|
|
||||||
return getInvoiceLineItemsForInvoice(latestSubscriptionInvoice).thenCompose(invoiceLineItems -> {
|
return getInvoiceLineItemsForInvoice(latestSubscriptionInvoice).thenCompose(invoiceLineItems -> {
|
||||||
|
@ -688,15 +682,15 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||||
// This usually indicates that the client has made requests out of order, either by not confirming
|
// This usually indicates that the client has made requests out of order, either by not confirming
|
||||||
// the SetupIntent or not having the user authorize the transaction.
|
// the SetupIntent or not having the user authorize the transaction.
|
||||||
logger.debug("setupIntent {} missing expected fields", setupIntentId);
|
logger.debug("setupIntent {} missing expected fields", setupIntentId);
|
||||||
throw new ClientErrorException(Status.CONFLICT);
|
throw ExceptionUtils.wrap(new SubscriptionException.ProcessorConflict());
|
||||||
}
|
}
|
||||||
return setupIntent.getLatestAttemptObject().getPaymentMethodDetails().getIdeal().getGeneratedSepaDebit();
|
return setupIntent.getLatestAttemptObject().getPaymentMethodDetails().getIdeal().getGeneratedSepaDebit();
|
||||||
} catch (StripeException e) {
|
} catch (StripeException e) {
|
||||||
if (e.getStatusCode() == 404) {
|
if (e.getStatusCode() == 404) {
|
||||||
throw new NotFoundException();
|
throw ExceptionUtils.wrap(new SubscriptionException.NotFound());
|
||||||
}
|
}
|
||||||
logger.error("unexpected error from Stripe when retrieving setupIntent {}", setupIntentId, e);
|
logger.error("unexpected error from Stripe when retrieving setupIntent {}", setupIntentId, e);
|
||||||
throw new CompletionException(e);
|
throw ExceptionUtils.wrap(e);
|
||||||
}
|
}
|
||||||
}, executor);
|
}, executor);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.subscriptions;
|
|
||||||
|
|
||||||
public class SubscriptionProcessorException extends Exception {
|
|
||||||
|
|
||||||
private final PaymentProvider processor;
|
|
||||||
private final ChargeFailure chargeFailure;
|
|
||||||
|
|
||||||
public SubscriptionProcessorException(final PaymentProvider processor,
|
|
||||||
final ChargeFailure chargeFailure) {
|
|
||||||
this.processor = processor;
|
|
||||||
this.chargeFailure = chargeFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PaymentProvider getProcessor() {
|
|
||||||
return processor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChargeFailure getChargeFailure() {
|
|
||||||
return chargeFailure;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -76,10 +76,10 @@ import org.whispersystems.textsecuregcm.entities.Badge;
|
||||||
import org.whispersystems.textsecuregcm.entities.BadgeSvg;
|
import org.whispersystems.textsecuregcm.entities.BadgeSvg;
|
||||||
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.SubscriptionExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.SubscriptionExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.SubscriptionProcessorExceptionMapper;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.OneTimeDonationsManager;
|
import org.whispersystems.textsecuregcm.storage.OneTimeDonationsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Subscriptions;
|
import org.whispersystems.textsecuregcm.storage.Subscriptions;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator;
|
import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator;
|
||||||
|
@ -93,7 +93,6 @@ import org.whispersystems.textsecuregcm.subscriptions.PaymentStatus;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
|
import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentProvider;
|
import org.whispersystems.textsecuregcm.subscriptions.PaymentProvider;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorException;
|
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.CustomerAwareSubscriptionPaymentProcessor;
|
import org.whispersystems.textsecuregcm.subscriptions.CustomerAwareSubscriptionPaymentProcessor;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
import org.whispersystems.textsecuregcm.util.MockUtils;
|
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||||
|
@ -133,7 +132,6 @@ class SubscriptionControllerTest {
|
||||||
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
|
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
|
||||||
.addProvider(AuthHelper.getAuthFilter())
|
.addProvider(AuthHelper.getAuthFilter())
|
||||||
.addProvider(CompletionExceptionMapper.class)
|
.addProvider(CompletionExceptionMapper.class)
|
||||||
.addProvider(SubscriptionProcessorExceptionMapper.class)
|
|
||||||
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
|
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
|
||||||
.addProvider(SubscriptionExceptionMapper.class)
|
.addProvider(SubscriptionExceptionMapper.class)
|
||||||
.setMapper(SystemMapper.jsonMapper())
|
.setMapper(SystemMapper.jsonMapper())
|
||||||
|
@ -340,7 +338,7 @@ class SubscriptionControllerTest {
|
||||||
|
|
||||||
when(BRAINTREE_MANAGER.captureOneTimePayment(anyString(), anyString(), anyString(), anyString(), anyLong(),
|
when(BRAINTREE_MANAGER.captureOneTimePayment(anyString(), anyString(), anyString(), anyString(), anyLong(),
|
||||||
anyLong(), any()))
|
anyLong(), any()))
|
||||||
.thenReturn(CompletableFuture.failedFuture(new SubscriptionProcessorException(PaymentProvider.BRAINTREE,
|
.thenReturn(CompletableFuture.failedFuture(new SubscriptionException.ProcessorException(PaymentProvider.BRAINTREE,
|
||||||
new ChargeFailure("2046", "Declined", null, null, null))));
|
new ChargeFailure("2046", "Declined", null, null, null))));
|
||||||
|
|
||||||
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/paypal/confirm")
|
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/paypal/confirm")
|
||||||
|
@ -351,7 +349,7 @@ class SubscriptionControllerTest {
|
||||||
"currency", "usd",
|
"currency", "usd",
|
||||||
"amount", 123)));
|
"amount", 123)));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(SubscriptionProcessorExceptionMapper.EXTERNAL_SERVICE_ERROR_STATUS_CODE);
|
assertThat(response.getStatus()).isEqualTo(SubscriptionExceptionMapper.PROCESSOR_ERROR_STATUS_CODE);
|
||||||
|
|
||||||
final Map responseMap = response.readEntity(Map.class);
|
final Map responseMap = response.readEntity(Map.class);
|
||||||
assertThat(responseMap.get("processor")).isEqualTo("BRAINTREE");
|
assertThat(responseMap.get("processor")).isEqualTo("BRAINTREE");
|
||||||
|
@ -419,7 +417,7 @@ class SubscriptionControllerTest {
|
||||||
@Test
|
@Test
|
||||||
void createSubscriptionProcessorDeclined() {
|
void createSubscriptionProcessorDeclined() {
|
||||||
when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong()))
|
when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong()))
|
||||||
.thenReturn(CompletableFuture.failedFuture(new SubscriptionProcessorException(PaymentProvider.STRIPE,
|
.thenReturn(CompletableFuture.failedFuture(new SubscriptionException.ProcessorException(PaymentProvider.STRIPE,
|
||||||
new ChargeFailure("card_declined", "Insufficient funds", null, null, null))));
|
new ChargeFailure("card_declined", "Insufficient funds", null, null, null))));
|
||||||
|
|
||||||
final String level = String.valueOf(levelId);
|
final String level = String.valueOf(levelId);
|
||||||
|
@ -429,8 +427,7 @@ class SubscriptionControllerTest {
|
||||||
.request()
|
.request()
|
||||||
.put(Entity.json(""));
|
.put(Entity.json(""));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(
|
assertThat(response.getStatus()).isEqualTo(SubscriptionExceptionMapper.PROCESSOR_ERROR_STATUS_CODE);
|
||||||
SubscriptionProcessorExceptionMapper.EXTERNAL_SERVICE_ERROR_STATUS_CODE);
|
|
||||||
|
|
||||||
final Map responseMap = response.readEntity(Map.class);
|
final Map responseMap = response.readEntity(Map.class);
|
||||||
assertThat(responseMap.get("processor")).isEqualTo("STRIPE");
|
assertThat(responseMap.get("processor")).isEqualTo("STRIPE");
|
||||||
|
|
Loading…
Reference in New Issue