From 207ae6129bafd10e19e52c47c587f38b348950eb Mon Sep 17 00:00:00 2001
From: Katherine <katherine@signal.org>
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          | 42 +++++++++++++++----
 .../SubscriptionProcessorManager.java         |  4 +-
 5 files changed, 67 insertions(+), 16 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<Transaction> 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;
-
-              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(
+              boolean paymentProcessing = false;
+              PaymentMethod paymentMethod = null;
+
+              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) {
 
   }
 
-- 
GitLab