Propagate certain subscription processor errors to client responses
This commit is contained in:
parent
2d187abf13
commit
b89e2e5355
|
@ -140,6 +140,7 @@ import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptio
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
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.SubscriptionProcessorExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
|
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
|
||||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||||
import org.whispersystems.textsecuregcm.metrics.ReportedMessageMetricsListener;
|
import org.whispersystems.textsecuregcm.metrics.ReportedMessageMetricsListener;
|
||||||
|
@ -858,6 +859,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
new ImpossiblePhoneNumberExceptionMapper(),
|
new ImpossiblePhoneNumberExceptionMapper(),
|
||||||
new NonNormalizedPhoneNumberExceptionMapper(),
|
new NonNormalizedPhoneNumberExceptionMapper(),
|
||||||
new RegistrationServiceSenderExceptionMapper(),
|
new RegistrationServiceSenderExceptionMapper(),
|
||||||
|
new SubscriptionProcessorExceptionMapper(),
|
||||||
new JsonMappingExceptionMapper()
|
new JsonMappingExceptionMapper()
|
||||||
).forEach(exceptionMapper -> {
|
).forEach(exceptionMapper -> {
|
||||||
environment.jersey().register(exceptionMapper);
|
environment.jersey().register(exceptionMapper);
|
||||||
|
|
|
@ -89,6 +89,7 @@ import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult;
|
import org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
|
import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
||||||
|
@ -1078,48 +1079,6 @@ public class SubscriptionController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ChargeFailure {
|
|
||||||
private final String code;
|
|
||||||
private final String message;
|
|
||||||
private final String outcomeNetworkStatus;
|
|
||||||
private final String outcomeReason;
|
|
||||||
private final String outcomeType;
|
|
||||||
|
|
||||||
@JsonCreator
|
|
||||||
public ChargeFailure(
|
|
||||||
@JsonProperty("code") String code,
|
|
||||||
@JsonProperty("message") String message,
|
|
||||||
@JsonProperty("outcomeNetworkStatus") String outcomeNetworkStatus,
|
|
||||||
@JsonProperty("outcomeReason") String outcomeReason,
|
|
||||||
@JsonProperty("outcomeType") String outcomeType) {
|
|
||||||
this.code = code;
|
|
||||||
this.message = message;
|
|
||||||
this.outcomeNetworkStatus = outcomeNetworkStatus;
|
|
||||||
this.outcomeReason = outcomeReason;
|
|
||||||
this.outcomeType = outcomeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getOutcomeNetworkStatus() {
|
|
||||||
return outcomeNetworkStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getOutcomeReason() {
|
|
||||||
return outcomeReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getOutcomeType() {
|
|
||||||
return outcomeType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Subscription subscription;
|
private final Subscription subscription;
|
||||||
private final ChargeFailure chargeFailure;
|
private final ChargeFailure chargeFailure;
|
||||||
|
|
||||||
|
@ -1158,31 +1117,20 @@ public class SubscriptionController {
|
||||||
final SubscriptionProcessorManager manager = getManagerForProcessor(record.getProcessorCustomer().orElseThrow().processor());
|
final SubscriptionProcessorManager manager = getManagerForProcessor(record.getProcessorCustomer().orElseThrow().processor());
|
||||||
|
|
||||||
return manager.getSubscription(record.subscriptionId).thenCompose(subscription ->
|
return manager.getSubscription(record.subscriptionId).thenCompose(subscription ->
|
||||||
manager.getSubscriptionInformation(subscription).thenApply(subscriptionInformation -> {
|
manager.getSubscriptionInformation(subscription).thenApply(subscriptionInformation -> Response.ok(
|
||||||
final GetSubscriptionInformationResponse.ChargeFailure chargeFailure = Optional.ofNullable(subscriptionInformation.chargeFailure())
|
new GetSubscriptionInformationResponse(
|
||||||
.map(chargeFailure1 -> new GetSubscriptionInformationResponse.ChargeFailure(
|
new GetSubscriptionInformationResponse.Subscription(
|
||||||
subscriptionInformation.chargeFailure().code(),
|
subscriptionInformation.level(),
|
||||||
subscriptionInformation.chargeFailure().message(),
|
subscriptionInformation.billingCycleAnchor(),
|
||||||
subscriptionInformation.chargeFailure().outcomeNetworkStatus(),
|
subscriptionInformation.endOfCurrentPeriod(),
|
||||||
subscriptionInformation.chargeFailure().outcomeReason(),
|
subscriptionInformation.active(),
|
||||||
subscriptionInformation.chargeFailure().outcomeType()
|
subscriptionInformation.cancelAtPeriodEnd(),
|
||||||
))
|
subscriptionInformation.price().currency(),
|
||||||
.orElse(null);
|
subscriptionInformation.price().amount(),
|
||||||
return Response.ok(
|
subscriptionInformation.status().getApiValue(),
|
||||||
new GetSubscriptionInformationResponse(
|
manager.getProcessor()),
|
||||||
new GetSubscriptionInformationResponse.Subscription(
|
subscriptionInformation.chargeFailure()
|
||||||
subscriptionInformation.level(),
|
)).build()));
|
||||||
subscriptionInformation.billingCycleAnchor(),
|
|
||||||
subscriptionInformation.endOfCurrentPeriod(),
|
|
||||||
subscriptionInformation.active(),
|
|
||||||
subscriptionInformation.cancelAtPeriodEnd(),
|
|
||||||
subscriptionInformation.price().currency(),
|
|
||||||
subscriptionInformation.price().amount(),
|
|
||||||
subscriptionInformation.status().getApiValue(),
|
|
||||||
manager.getProcessor()),
|
|
||||||
chargeFailure
|
|
||||||
)).build();
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,6 +49,8 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(BraintreeManager.class);
|
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 static final String PAYPAL_PAYMENT_ALREADY_COMPLETED_PROCESSOR_CODE = "2094";
|
||||||
private final BraintreeGateway braintreeGateway;
|
private final BraintreeGateway braintreeGateway;
|
||||||
private final BraintreeGraphqlClient braintreeGraphqlClient;
|
private final BraintreeGraphqlClient braintreeGraphqlClient;
|
||||||
|
@ -184,11 +186,18 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||||
new PayPalChargeSuccessDetails(successfulTx.getGraphQLId()));
|
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(
|
default -> {
|
||||||
new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR));
|
logger.info("PayPal charge unexpectedly failed: {}", unsuccessfulTx.getProcessorResponseCode());
|
||||||
|
|
||||||
|
yield CompletableFuture.failedFuture(
|
||||||
|
new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR));
|
||||||
|
}
|
||||||
|
};
|
||||||
}, executor));
|
}, 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
|
@Override
|
||||||
public CompletableFuture<ProcessorCustomer> createCustomer(final byte[] subscriberUser) {
|
public CompletableFuture<ProcessorCustomer> createCustomer(final byte[] subscriberUser) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
@ -258,7 +261,9 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||||
}
|
}
|
||||||
}, executor)
|
}, executor)
|
||||||
.thenApply(result -> {
|
.thenApply(result -> {
|
||||||
assertResultSuccess(result);
|
if (!result.isSuccess()) {
|
||||||
|
throw new CompletionException(new BraintreeException(result.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
return new ProcessorCustomer(result.getTarget().getId(), SubscriptionProcessor.BRAINTREE);
|
return new ProcessorCustomer(result.getTarget().getId(), SubscriptionProcessor.BRAINTREE);
|
||||||
});
|
});
|
||||||
|
@ -336,7 +341,19 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||||
.done()
|
.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();
|
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
|
// 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
|
// so we must end the existing one and create a new one
|
||||||
return cancelSubscriptionAtEndOfCurrentPeriod(subscription)
|
return cancelSubscriptionAtEndOfCurrentPeriod(subscription)
|
||||||
.thenCompose(ignored -> {
|
.thenCompose(ignored -> {
|
||||||
|
@ -413,37 +430,13 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||||
final Instant anchor = subscription.getFirstBillingDate().toInstant();
|
final Instant anchor = subscription.getFirstBillingDate().toInstant();
|
||||||
final Instant endOfCurrentPeriod = subscription.getBillingPeriodEndDate().toInstant();
|
final Instant endOfCurrentPeriod = subscription.getBillingPeriodEndDate().toInstant();
|
||||||
|
|
||||||
final Optional<Transaction> maybeTransaction = getLatestTransactionForSubscription(subscription);
|
final ChargeFailure chargeFailure = getLatestTransactionForSubscription(subscription).map(transaction -> {
|
||||||
|
|
||||||
final ChargeFailure chargeFailure = maybeTransaction.map(transaction -> {
|
|
||||||
|
|
||||||
if (getPaymentStatus(transaction.getStatus()).equals(PaymentStatus.SUCCEEDED)) {
|
if (getPaymentStatus(transaction.getStatus()).equals(PaymentStatus.SUCCEEDED)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return createChargeFailure(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);
|
|
||||||
|
|
||||||
}).orElse(null);
|
}).orElse(null);
|
||||||
|
|
||||||
|
|
||||||
return new SubscriptionInformation(
|
return new SubscriptionInformation(
|
||||||
new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT),
|
new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT),
|
||||||
SubscriptionCurrencyUtil.convertBraintreeAmountToApiAmount(plan.getCurrencyIsoCode(), plan.getPrice())),
|
SubscriptionCurrencyUtil.convertBraintreeAmountToApiAmount(plan.getCurrencyIsoCode(), plan.getPrice())),
|
||||||
|
@ -458,6 +451,29 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||||
}, executor);
|
}, 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
|
@Override
|
||||||
public CompletableFuture<Void> cancelAllActiveSubscriptions(String customerId) {
|
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.base.Strings;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.stripe.StripeClient;
|
import com.stripe.StripeClient;
|
||||||
|
import com.stripe.exception.CardException;
|
||||||
import com.stripe.exception.StripeException;
|
import com.stripe.exception.StripeException;
|
||||||
import com.stripe.model.Charge;
|
import com.stripe.model.Charge;
|
||||||
import com.stripe.model.Customer;
|
import com.stripe.model.Customer;
|
||||||
|
@ -268,6 +269,18 @@ public class StripeManager implements SubscriptionProcessorManager {
|
||||||
.create(params, commonOptions(generateIdempotencyKeyForCreateSubscription(
|
.create(params, commonOptions(generateIdempotencyKeyForCreateSubscription(
|
||||||
customerId, lastSubscriptionCreatedAt)));
|
customerId, lastSubscriptionCreatedAt)));
|
||||||
} catch (StripeException e) {
|
} 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);
|
throw new CompletionException(e);
|
||||||
}
|
}
|
||||||
}, executor)
|
}, 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) {
|
record ReceiptItem(String itemId, Instant expiration, long level) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ import java.util.function.Predicate;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import javax.ws.rs.client.Entity;
|
import javax.ws.rs.client.Entity;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||||
import org.glassfish.jersey.server.ServerProperties;
|
import org.glassfish.jersey.server.ServerProperties;
|
||||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
@ -63,12 +64,15 @@ import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetSu
|
||||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
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.SubscriptionProcessorExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure;
|
||||||
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.SubscriptionProcessor;
|
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorException;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorManager;
|
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorManager;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
@ -112,6 +116,7 @@ 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 PolymorphicAuthValueFactoryProvider.Binder<>(Set.of(
|
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(Set.of(
|
||||||
AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||||
.setMapper(SystemMapper.jsonMapper())
|
.setMapper(SystemMapper.jsonMapper())
|
||||||
|
@ -189,6 +194,32 @@ class SubscriptionControllerTest {
|
||||||
assertThat(response.getStatus()).isEqualTo(422);
|
assertThat(response.getStatus()).isEqualTo(422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void confirmPaypalBoostProcessorError() {
|
||||||
|
|
||||||
|
when(BRAINTREE_MANAGER.captureOneTimePayment(anyString(), anyString(), anyString(), anyString(), anyLong(),
|
||||||
|
anyLong()))
|
||||||
|
.thenReturn(CompletableFuture.failedFuture(new SubscriptionProcessorException(SubscriptionProcessor.BRAINTREE,
|
||||||
|
new ChargeFailure("2046", "Declined", null, null, null))));
|
||||||
|
|
||||||
|
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/paypal/confirm")
|
||||||
|
.request()
|
||||||
|
.post(Entity.json(Map.of("payerId", "payer123",
|
||||||
|
"paymentId", "PAYID-456",
|
||||||
|
"paymentToken", "EC-789",
|
||||||
|
"currency", "usd",
|
||||||
|
"amount", 123)));
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(SubscriptionProcessorExceptionMapper.EXTERNAL_SERVICE_ERROR_STATUS_CODE);
|
||||||
|
|
||||||
|
final Map responseMap = response.readEntity(Map.class);
|
||||||
|
assertThat(responseMap.get("processor")).isEqualTo("BRAINTREE");
|
||||||
|
assertThat(responseMap.get("chargeFailure")).asInstanceOf(
|
||||||
|
InstanceOfAssertFactories.map(String.class, Object.class))
|
||||||
|
.extracting("code")
|
||||||
|
.isEqualTo("2046");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createBoostReceiptNoRequest() {
|
void createBoostReceiptNoRequest() {
|
||||||
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/receipt_credentials")
|
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/receipt_credentials")
|
||||||
|
@ -230,7 +261,7 @@ class SubscriptionControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void success() {
|
void createSubscriptionSuccess() {
|
||||||
when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong()))
|
when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(mock(SubscriptionProcessorManager.SubscriptionId.class)));
|
.thenReturn(CompletableFuture.completedFuture(mock(SubscriptionProcessorManager.SubscriptionId.class)));
|
||||||
|
|
||||||
|
@ -244,6 +275,30 @@ class SubscriptionControllerTest {
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createSubscriptionProcessorDeclined() {
|
||||||
|
when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong()))
|
||||||
|
.thenReturn(CompletableFuture.failedFuture(new SubscriptionProcessorException(SubscriptionProcessor.STRIPE,
|
||||||
|
new ChargeFailure("card_declined", "Insufficient funds", null, null, null))));
|
||||||
|
|
||||||
|
final String level = String.valueOf(levelId);
|
||||||
|
final String idempotencyKey = UUID.randomUUID().toString();
|
||||||
|
final Response response = RESOURCE_EXTENSION.target(
|
||||||
|
String.format("/v1/subscription/%s/level/%s/%s/%s", subscriberId, level, currency, idempotencyKey))
|
||||||
|
.request()
|
||||||
|
.put(Entity.json(""));
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(
|
||||||
|
SubscriptionProcessorExceptionMapper.EXTERNAL_SERVICE_ERROR_STATUS_CODE);
|
||||||
|
|
||||||
|
final Map responseMap = response.readEntity(Map.class);
|
||||||
|
assertThat(responseMap.get("processor")).isEqualTo("STRIPE");
|
||||||
|
assertThat(responseMap.get("chargeFailure")).asInstanceOf(
|
||||||
|
InstanceOfAssertFactories.map(String.class, Object.class))
|
||||||
|
.extracting("code")
|
||||||
|
.isEqualTo("card_declined");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void missingCustomerId() {
|
void missingCustomerId() {
|
||||||
final byte[] subscriberUserAndKey = new byte[32];
|
final byte[] subscriberUserAndKey = new byte[32];
|
||||||
|
|
Loading…
Reference in New Issue