Attach client platforms when creating donations
This commit is contained in:
parent
b8f64fe3d4
commit
ed72d7f9ec
|
@ -46,8 +46,6 @@ public class DonationController {
|
|||
ReceiptCredentialPresentation build(byte[] bytes) throws InvalidInputException;
|
||||
}
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DonationController.class);
|
||||
|
||||
private final Clock clock;
|
||||
private final ServerZkReceiptOperations serverZkReceiptOperations;
|
||||
private final RedeemedReceiptsManager redeemedReceiptsManager;
|
||||
|
|
|
@ -10,7 +10,6 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
|||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.stripe.exception.StripeException;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
|
@ -36,6 +35,7 @@ import java.util.concurrent.CompletionException;
|
|||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.validation.Valid;
|
||||
|
@ -99,6 +99,9 @@ import org.whispersystems.textsecuregcm.subscriptions.SubscriptionCurrencyUtil;
|
|||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorManager;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
import org.whispersystems.websocket.auth.ReadOnly;
|
||||
|
||||
@Path("/v1/subscription")
|
||||
|
@ -126,7 +129,6 @@ public class SubscriptionController {
|
|||
private static final String TYPE_TAG_NAME = "type";
|
||||
private static final String SUBSCRIPTION_TYPE_TAG_NAME = "subscriptionType";
|
||||
private static final String EURO_CURRENCY_CODE = "EUR";
|
||||
private static final Semver LAST_PROBLEMATIC_IOS_VERSION = new Semver("6.44.0");
|
||||
|
||||
public SubscriptionController(
|
||||
@Nonnull Clock clock,
|
||||
|
@ -290,7 +292,8 @@ public class SubscriptionController {
|
|||
public CompletableFuture<Response> createPaymentMethod(
|
||||
@ReadOnly @Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId,
|
||||
@QueryParam("type") @DefaultValue("CARD") PaymentMethod paymentMethodType) {
|
||||
@QueryParam("type") @DefaultValue("CARD") PaymentMethod paymentMethodType,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) @Nullable final String userAgentString) {
|
||||
|
||||
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
|
||||
|
||||
|
@ -309,7 +312,7 @@ public class SubscriptionController {
|
|||
|
||||
return CompletableFuture.completedFuture(record);
|
||||
})
|
||||
.orElseGet(() -> subscriptionProcessorManager.createCustomer(requestData.subscriberUser)
|
||||
.orElseGet(() -> subscriptionProcessorManager.createCustomer(requestData.subscriberUser, getClientPlatform(userAgentString))
|
||||
.thenApply(ProcessorCustomer::customerId)
|
||||
.thenCompose(customerId -> subscriptionManager.setProcessorAndCustomerId(record,
|
||||
new ProcessorCustomer(customerId, subscriptionProcessorManager.getProcessor()),
|
||||
|
@ -345,7 +348,8 @@ public class SubscriptionController {
|
|||
@ReadOnly @Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId,
|
||||
@NotNull @Valid CreatePayPalBillingAgreementRequest request,
|
||||
@Context ContainerRequestContext containerRequestContext) {
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) @Nullable final String userAgentString) {
|
||||
|
||||
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
|
||||
|
||||
|
@ -362,7 +366,7 @@ public class SubscriptionController {
|
|||
}
|
||||
return CompletableFuture.completedFuture(record);
|
||||
})
|
||||
.orElseGet(() -> braintreeManager.createCustomer(requestData.subscriberUser)
|
||||
.orElseGet(() -> braintreeManager.createCustomer(requestData.subscriberUser, getClientPlatform(userAgentString))
|
||||
.thenApply(ProcessorCustomer::customerId)
|
||||
.thenCompose(customerId -> subscriptionManager.setProcessorAndCustomerId(record,
|
||||
new ProcessorCustomer(customerId, braintreeManager.getProcessor()),
|
||||
|
@ -665,7 +669,9 @@ public class SubscriptionController {
|
|||
@Path("/boost/create")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> createBoostPaymentIntent(@NotNull @Valid CreateBoostRequest request) {
|
||||
public CompletableFuture<Response> createBoostPaymentIntent(@NotNull @Valid CreateBoostRequest request,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) {
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
if (request.level == null) {
|
||||
request.level = oneTimeDonationConfiguration.boost().level();
|
||||
|
@ -683,7 +689,7 @@ public class SubscriptionController {
|
|||
}
|
||||
validateRequestCurrencyAmount(request, amount, stripeManager);
|
||||
})
|
||||
.thenCompose(unused -> stripeManager.createPaymentIntent(request.currency, request.amount, request.level))
|
||||
.thenCompose(unused -> stripeManager.createPaymentIntent(request.currency, request.amount, request.level, getClientPlatform(userAgent)))
|
||||
.thenApply(paymentIntent -> Response.ok(new CreateBoostResponse(paymentIntent.getClientSecret())).build());
|
||||
}
|
||||
|
||||
|
@ -769,7 +775,8 @@ public class SubscriptionController {
|
|||
@Path("/boost/paypal/confirm")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> confirmPayPalBoost(@NotNull @Valid ConfirmPayPalBoostRequest request) {
|
||||
public CompletableFuture<Response> confirmPayPalBoost(@NotNull @Valid ConfirmPayPalBoostRequest request,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) {
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
if (request.level == null) {
|
||||
|
@ -1072,6 +1079,15 @@ public class SubscriptionController {
|
|||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ClientPlatform getClientPlatform(@Nullable final String userAgentString) {
|
||||
try {
|
||||
return UserAgentUtil.parseUserAgentString(userAgentString).getPlatform();
|
||||
} catch (final UnrecognizedUserAgentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private record RequestData(@Nonnull byte[] subscriberBytes,
|
||||
@Nonnull byte[] subscriberUser,
|
||||
@Nonnull byte[] subscriberKey,
|
||||
|
|
|
@ -44,6 +44,7 @@ import org.slf4j.LoggerFactory;
|
|||
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
|
||||
public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
|
||||
|
@ -252,10 +253,15 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ProcessorCustomer> createCustomer(final byte[] subscriberUser) {
|
||||
public CompletableFuture<ProcessorCustomer> createCustomer(final byte[] subscriberUser, @Nullable final ClientPlatform clientPlatform) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final CustomerRequest request = new CustomerRequest()
|
||||
CustomerRequest request = new CustomerRequest()
|
||||
.customField("subscriber_user", HexFormat.of().formatHex(subscriberUser));
|
||||
|
||||
if (clientPlatform != null) {
|
||||
request.customField("client_platform", clientPlatform.name().toLowerCase());
|
||||
}
|
||||
|
||||
try {
|
||||
return braintreeGateway.customer().create(request);
|
||||
} catch (BraintreeException e) {
|
||||
|
|
|
@ -74,10 +74,12 @@ import org.apache.commons.lang3.StringUtils;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
|
||||
public class StripeManager implements SubscriptionProcessorManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(StripeManager.class);
|
||||
private static final String METADATA_KEY_LEVEL = "level";
|
||||
private static final String METADATA_KEY_CLIENT_PLATFORM = "clientPlatform";
|
||||
|
||||
private final StripeClient stripeClient;
|
||||
private final Executor executor;
|
||||
|
@ -127,14 +129,18 @@ public class StripeManager implements SubscriptionProcessorManager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ProcessorCustomer> createCustomer(byte[] subscriberUser) {
|
||||
public CompletableFuture<ProcessorCustomer> createCustomer(final byte[] subscriberUser, @Nullable final ClientPlatform clientPlatform) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
CustomerCreateParams params = CustomerCreateParams.builder()
|
||||
.putMetadata("subscriberUser", HexFormat.of().formatHex(subscriberUser))
|
||||
.build();
|
||||
final CustomerCreateParams.Builder builder = CustomerCreateParams.builder()
|
||||
.putMetadata("subscriberUser", HexFormat.of().formatHex(subscriberUser));
|
||||
|
||||
if (clientPlatform != null) {
|
||||
builder.putMetadata(METADATA_KEY_CLIENT_PLATFORM, clientPlatform.name().toLowerCase());
|
||||
}
|
||||
|
||||
try {
|
||||
return stripeClient.customers()
|
||||
.create(params, commonOptions(generateIdempotencyKeyForSubscriberUser(subscriberUser)));
|
||||
.create(builder.build(), commonOptions(generateIdempotencyKeyForSubscriberUser(subscriberUser)));
|
||||
} catch (StripeException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
|
@ -194,16 +200,24 @@ public class StripeManager implements SubscriptionProcessorManager {
|
|||
/**
|
||||
* Creates a payment intent. May throw a 400 WebApplicationException if the amount is too small.
|
||||
*/
|
||||
public CompletableFuture<PaymentIntent> createPaymentIntent(String currency, long amount, long level) {
|
||||
public CompletableFuture<PaymentIntent> createPaymentIntent(final String currency,
|
||||
final long amount,
|
||||
final long level,
|
||||
@Nullable final ClientPlatform clientPlatform) {
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
PaymentIntentCreateParams params = PaymentIntentCreateParams.builder()
|
||||
final PaymentIntentCreateParams.Builder builder = PaymentIntentCreateParams.builder()
|
||||
.setAmount(amount)
|
||||
.setCurrency(currency.toLowerCase(Locale.ROOT))
|
||||
.setDescription(boostDescription)
|
||||
.putMetadata("level", Long.toString(level))
|
||||
.build();
|
||||
.putMetadata("level", Long.toString(level));
|
||||
|
||||
if (clientPlatform != null) {
|
||||
builder.putMetadata(METADATA_KEY_CLIENT_PLATFORM, clientPlatform.name().toLowerCase());
|
||||
}
|
||||
|
||||
try {
|
||||
return stripeClient.paymentIntents().create(params, commonOptions());
|
||||
return stripeClient.paymentIntents().create(builder.build(), commonOptions());
|
||||
} catch (StripeException e) {
|
||||
if ("amount_too_small".equalsIgnoreCase(e.getCode())) {
|
||||
throw new WebApplicationException(Response
|
||||
|
|
|
@ -13,6 +13,7 @@ import java.util.concurrent.CompletableFuture;
|
|||
import javax.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
|
||||
public interface SubscriptionProcessorManager {
|
||||
SubscriptionProcessor getProcessor();
|
||||
|
@ -23,7 +24,7 @@ public interface SubscriptionProcessorManager {
|
|||
|
||||
CompletableFuture<PaymentDetails> getPaymentDetails(String paymentId);
|
||||
|
||||
CompletableFuture<ProcessorCustomer> createCustomer(byte[] subscriberUser);
|
||||
CompletableFuture<ProcessorCustomer> createCustomer(byte[] subscriberUser, @Nullable ClientPlatform clientPlatform);
|
||||
|
||||
CompletableFuture<String> createPaymentMethodSetupToken(String customerId);
|
||||
|
||||
|
|
|
@ -231,7 +231,7 @@ class SubscriptionControllerTest {
|
|||
void testCreateBoostPaymentIntent() {
|
||||
when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.CARD))
|
||||
.thenReturn(Set.of("usd", "jpy", "bif", "eur"));
|
||||
when(STRIPE_MANAGER.createPaymentIntent(anyString(), anyLong(), anyLong()))
|
||||
when(STRIPE_MANAGER.createPaymentIntent(anyString(), anyLong(), anyLong(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(PAYMENT_INTENT));
|
||||
|
||||
String clientSecret = "some_client_secret";
|
||||
|
@ -584,7 +584,7 @@ class SubscriptionControllerTest {
|
|||
final String customerId = "some-customer-id";
|
||||
final ProcessorCustomer customer = new ProcessorCustomer(
|
||||
customerId, SubscriptionProcessor.STRIPE);
|
||||
when(STRIPE_MANAGER.createCustomer(any()))
|
||||
when(STRIPE_MANAGER.createCustomer(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(customer));
|
||||
|
||||
final Map<String, AttributeValue> dynamoItemWithProcessorCustomer = new HashMap<>(dynamoItem);
|
||||
|
|
Loading…
Reference in New Issue