From 207ae6129bafd10e19e52c47c587f38b348950eb Mon Sep 17 00:00:00 2001 From: Katherine Date: Tue, 10 Oct 2023 09:56:50 -0700 Subject: [PATCH] Add `paymentMethod` and `paymentProcessing` fields to `GET /v1/subscription/{subscriberId}` endpoint --- .../controllers/SubscriptionController.java | 7 +++- .../subscriptions/BraintreeManager.java | 29 +++++++++++--- .../subscriptions/PaymentMethod.java | 1 + .../subscriptions/StripeManager.java | 40 ++++++++++++++++--- .../SubscriptionProcessorManager.java | 4 +- 5 files changed, 66 insertions(+), 15 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java index 45983b875..eb97c1bd5 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java @@ -394,6 +394,7 @@ public class SubscriptionController { return switch (paymentMethod) { case CARD, SEPA_DEBIT -> stripeManager; case PAYPAL -> braintreeManager; + case UNKNOWN -> throw new BadRequestException("Invalid payment method"); }; } @@ -886,7 +887,7 @@ public class SubscriptionController { public record Subscription(long level, Instant billingCycleAnchor, Instant endOfCurrentPeriod, boolean active, boolean cancelAtPeriodEnd, String currency, BigDecimal amount, String status, - SubscriptionProcessor processor) { + SubscriptionProcessor processor, PaymentMethod paymentMethod, boolean paymentProcessing) { } } @@ -919,7 +920,9 @@ public class SubscriptionController { subscriptionInformation.price().currency(), subscriptionInformation.price().amount(), subscriptionInformation.status().getApiValue(), - manager.getProcessor()), + manager.getProcessor(), + subscriptionInformation.paymentMethod(), + subscriptionInformation.paymentProcessing()), subscriptionInformation.chargeFailure() )).build())); }); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java index 1d30c487e..3d039029f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java @@ -426,12 +426,17 @@ public class BraintreeManager implements SubscriptionProcessorManager { final Instant anchor = subscription.getFirstBillingDate().toInstant(); final Instant endOfCurrentPeriod = subscription.getBillingPeriodEndDate().toInstant(); - final ChargeFailure chargeFailure = getLatestTransactionForSubscription(subscription).map(transaction -> { - if (getPaymentStatus(transaction.getStatus()).equals(PaymentStatus.SUCCEEDED)) { - return null; + boolean paymentProcessing = false; + ChargeFailure chargeFailure = null; + + final Optional latestTransaction = getLatestTransactionForSubscription(subscription); + + if (latestTransaction.isPresent()){ + paymentProcessing = isPaymentProcessing(latestTransaction.get().getStatus()); + if (!getPaymentStatus(latestTransaction.get().getStatus()).equals(PaymentStatus.SUCCEEDED)) { + chargeFailure = createChargeFailure(latestTransaction.get()); } - return createChargeFailure(transaction); - }).orElse(null); + } return new SubscriptionInformation( new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT), @@ -442,11 +447,25 @@ public class BraintreeManager implements SubscriptionProcessorManager { Subscription.Status.ACTIVE == subscription.getStatus(), !subscription.neverExpires(), getSubscriptionStatus(subscription.getStatus()), + latestTransaction.map(this::getPaymentMethodFromTransaction).orElse(PaymentMethod.PAYPAL), + paymentProcessing, chargeFailure ); }, executor); } + private PaymentMethod getPaymentMethodFromTransaction(Transaction transaction) { + if (transaction.getPayPalDetails() != null) { + return PaymentMethod.PAYPAL; + } + logger.error("Unexpected payment method from Braintree: {}, transaction id {}", transaction.getPaymentInstrumentType(), transaction.getId()); + return PaymentMethod.UNKNOWN; + } + + private static boolean isPaymentProcessing(final Transaction.Status status) { + return status == Transaction.Status.SETTLEMENT_PENDING; + } + private ChargeFailure createChargeFailure(Transaction transaction) { final String code; diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/PaymentMethod.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/PaymentMethod.java index 4738437ea..1477e45c3 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/PaymentMethod.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/PaymentMethod.java @@ -6,6 +6,7 @@ package org.whispersystems.textsecuregcm.subscriptions; public enum PaymentMethod { + UNKNOWN, /** * A credit card or debit card, including those from Apple Pay and Google Pay */ diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java index c1dc9c6b1..741df5e46 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java @@ -67,10 +67,12 @@ 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.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.util.Conversions; public class StripeManager implements SubscriptionProcessorManager { - + private static final Logger logger = LoggerFactory.getLogger(StripeManager.class); private static final String METADATA_KEY_LEVEL = "level"; private final StripeClient stripeClient; @@ -483,17 +485,30 @@ public class StripeManager implements SubscriptionProcessorManager { return getPriceForSubscription(subscription).thenCompose(price -> getLevelForPrice(price).thenApply(level -> { ChargeFailure chargeFailure = null; + boolean paymentProcessing = false; + PaymentMethod paymentMethod = null; - if (subscription.getLatestInvoiceObject() != null && subscription.getLatestInvoiceObject().getChargeObject() != null && - (subscription.getLatestInvoiceObject().getChargeObject().getFailureCode() != null || subscription.getLatestInvoiceObject().getChargeObject().getFailureMessage() != null)) { - Charge charge = subscription.getLatestInvoiceObject().getChargeObject(); - Charge.Outcome outcome = charge.getOutcome(); - chargeFailure = new ChargeFailure( + if (subscription.getLatestInvoiceObject() != null) { + final Invoice invoice = subscription.getLatestInvoiceObject(); + paymentProcessing = "open".equals(invoice.getStatus()); + + if (invoice.getChargeObject() != null) { + final Charge charge = invoice.getChargeObject(); + if (charge.getFailureCode() != null || charge.getFailureMessage() != null) { + Charge.Outcome outcome = charge.getOutcome(); + chargeFailure = new ChargeFailure( charge.getFailureCode(), charge.getFailureMessage(), outcome != null ? outcome.getNetworkStatus() : null, outcome != null ? outcome.getReason() : null, outcome != null ? outcome.getType() : null); + } + + if (charge.getPaymentMethodDetails() != null + && charge.getPaymentMethodDetails().getType() != null) { + paymentMethod = getPaymentMethodFromStripeString(charge.getPaymentMethodDetails().getType(), invoice.getId()); + } + } } return new SubscriptionInformation( @@ -504,11 +519,24 @@ public class StripeManager implements SubscriptionProcessorManager { Objects.equals(subscription.getStatus(), "active"), subscription.getCancelAtPeriodEnd(), getSubscriptionStatus(subscription.getStatus()), + paymentMethod, + paymentProcessing, chargeFailure ); })); } + private static PaymentMethod getPaymentMethodFromStripeString(final String paymentMethodString, final String invoiceId) { + return switch (paymentMethodString) { + case "sepa_debit" -> PaymentMethod.SEPA_DEBIT; + case "card" -> PaymentMethod.CARD; + default -> { + logger.error("Unexpected payment method from Stripe: {}, invoice id: {}", paymentMethodString, invoiceId); + yield PaymentMethod.UNKNOWN; + } + }; + } + private Subscription getSubscription(Object subscriptionObj) { if (!(subscriptionObj instanceof final Subscription subscription)) { throw new IllegalArgumentException("invalid subscription object: " + subscriptionObj.getClass().getName()); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java index 7be7b2c2d..e82d6e5bc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java @@ -144,8 +144,8 @@ public interface SubscriptionProcessorManager { record SubscriptionInformation(SubscriptionPrice price, long level, Instant billingCycleAnchor, Instant endOfCurrentPeriod, boolean active, boolean cancelAtPeriodEnd, - SubscriptionStatus status, - ChargeFailure chargeFailure) { + SubscriptionStatus status, PaymentMethod paymentMethod, boolean paymentProcessing, + @Nullable ChargeFailure chargeFailure) { }