0
0
mirror of https://github.com/signalapp/Signal-Server.git synced 2024-09-20 12:02:18 +02:00

Set error_if_incomplete for subscription payment behavior

This commit is contained in:
Chris Eager 2022-11-16 12:08:21 -06:00 committed by GitHub
parent 836307b0c7
commit 986545a140
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 137 additions and 2 deletions

View File

@ -12,6 +12,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.stripe.exception.StripeException;
import com.stripe.model.Charge;
import com.stripe.model.Charge.Outcome;
import com.stripe.model.Invoice;
@ -36,6 +37,7 @@ import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.crypto.Mac;
@ -294,6 +296,7 @@ public class SubscriptionController {
public enum Type {
UNSUPPORTED_LEVEL,
UNSUPPORTED_CURRENCY,
PAYMENT_REQUIRES_ACTION,
}
private final Type type;
@ -374,6 +377,22 @@ public class SubscriptionController {
// retries this request
stripeManager.createSubscription(processorCustomer.customerId(), priceConfiguration.getId(), level,
lastSubscriptionCreatedAt)
.exceptionally(e -> {
if (e.getCause() instanceof StripeException stripeException
&& stripeException.getCode().equals("subscription_payment_intent_requires_action")) {
throw new BadRequestException(Response.status(Status.BAD_REQUEST)
.entity(new SetSubscriptionLevelErrorResponse(List.of(
new SetSubscriptionLevelErrorResponse.Error(
SetSubscriptionLevelErrorResponse.Error.Type.PAYMENT_REQUIRES_ACTION, null
)
))).build());
}
if (e instanceof RuntimeException re) {
throw re;
}
throw new CompletionException(e);
})
.thenCompose(subscription -> subscriptionManager.subscriptionCreated(
requestData.subscriberUser, subscription.getId(), requestData.now, level)
.thenApply(unused -> subscription)))

View File

@ -211,6 +211,7 @@ public class StripeManager implements SubscriptionProcessorManager {
SubscriptionCreateParams params = SubscriptionCreateParams.builder()
.setCustomer(customerId)
.setOffSession(true)
.setPaymentBehavior(SubscriptionCreateParams.PaymentBehavior.ERROR_IF_INCOMPLETE)
.addItem(SubscriptionCreateParams.Item.builder()
.setPrice(priceId)
.build())
@ -250,6 +251,7 @@ public class StripeManager implements SubscriptionProcessorManager {
.setProrationBehavior(ProrationBehavior.NONE)
.setBillingCycleAnchor(BillingCycleAnchor.NOW)
.setOffSession(true)
.setPaymentBehavior(SubscriptionUpdateParams.PaymentBehavior.ERROR_IF_INCOMPLETE)
.addAllItem(items)
.build();
try {

View File

@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
@ -14,6 +15,8 @@ import static org.mockito.Mockito.when;
import static org.whispersystems.textsecuregcm.util.AttributeValues.b;
import static org.whispersystems.textsecuregcm.util.AttributeValues.n;
import com.stripe.exception.ApiException;
import com.stripe.model.Subscription;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
@ -26,13 +29,16 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
@ -111,6 +117,115 @@ class SubscriptionControllerTest {
assertThat(response.getStatus()).isEqualTo(422);
}
@Nested
class SetSubscriptionLevel {
private final long levelId = 5L;
private final String currency = "eur";
private String subscriberId;
@BeforeEach
void setUp() {
when(CLOCK.instant()).thenReturn(Instant.now());
final byte[] subscriberUserAndKey = new byte[32];
Arrays.fill(subscriberUserAndKey, (byte) 1);
subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey);
final ProcessorCustomer processorCustomer = new ProcessorCustomer("testCustomerId", SubscriptionProcessor.STRIPE);
final Map<String, AttributeValue> dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]),
SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()),
SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()),
SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID, b(processorCustomer.toDynamoBytes())
);
final SubscriptionManager.Record record = SubscriptionManager.Record.from(
Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem);
when(SUBSCRIPTION_MANAGER.get(eq(Arrays.copyOfRange(subscriberUserAndKey, 0, 16)), any()))
.thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record)));
final SubscriptionLevelConfiguration levelConfig = mock(SubscriptionLevelConfiguration.class);
when(SUBSCRIPTION_CONFIG.getLevels())
.thenReturn(Map.of(levelId, levelConfig));
final SubscriptionPriceConfiguration priceConfig = new SubscriptionPriceConfiguration("testPriceId",
BigDecimal.TEN);
when(levelConfig.getPrices())
.thenReturn(Map.of(currency, priceConfig));
when(SUBSCRIPTION_MANAGER.subscriptionCreated(any(), any(), any(), anyLong()))
.thenReturn(CompletableFuture.completedFuture(null));
}
@Test
void success() {
when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong()))
.thenReturn(CompletableFuture.completedFuture(mock(Subscription.class)));
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(200);
}
@Test
void missingCustomerId() {
final byte[] subscriberUserAndKey = new byte[32];
Arrays.fill(subscriberUserAndKey, (byte) 1);
subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey);
final Map<String, AttributeValue> dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]),
SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()),
SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond())
// missing processor:customer field
);
final SubscriptionManager.Record record = SubscriptionManager.Record.from(
Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem);
when(SUBSCRIPTION_MANAGER.get(eq(Arrays.copyOfRange(subscriberUserAndKey, 0, 16)), any()))
.thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record)));
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(409);
}
@Test
void stripePaymentIntentRequiresAction() {
final ApiException stripeException = new ApiException("Payment intent requires action",
UUID.randomUUID().toString(), "subscription_payment_intent_requires_action", 400, new Exception());
when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong()))
.thenReturn(CompletableFuture.failedFuture(new CompletionException(stripeException)));
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(400);
assertThat(response.readEntity(SubscriptionController.SetSubscriptionLevelErrorResponse.class))
.satisfies(errorResponse -> {
assertThat(errorResponse.getErrors())
.anySatisfy(error -> {
assertThat(error.getType()).isEqualTo(
SubscriptionController.SetSubscriptionLevelErrorResponse.Error.Type.PAYMENT_REQUIRES_ACTION);
});
});
}
}
@Test
void createSubscriber() {
when(CLOCK.instant()).thenReturn(Instant.now());
@ -205,8 +320,7 @@ class SubscriptionControllerTest {
final SubscriptionManager.Record record = SubscriptionManager.Record.from(
Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem);
when(SUBSCRIPTION_MANAGER.create(any(), any(), any(Instant.class)))
.thenReturn(CompletableFuture.completedFuture(
record));
.thenReturn(CompletableFuture.completedFuture(record));
final Response createSubscriberResponse = RESOURCE_EXTENSION
.target(String.format("/v1/subscription/%s", subscriberId))