Add `paymentMethod` and `paymentProcessing` fields to `GET /v1/subscription/{subscriberId}` endpoint

This commit is contained in:
Katherine 2023-10-10 09:56:50 -07:00 committed by GitHub
parent e1aa734c40
commit 207ae6129b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 66 additions and 15 deletions

View File

@ -394,6 +394,7 @@ public class SubscriptionController {
return switch (paymentMethod) { return switch (paymentMethod) {
case CARD, SEPA_DEBIT -> stripeManager; case CARD, SEPA_DEBIT -> stripeManager;
case PAYPAL -> braintreeManager; 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, public record Subscription(long level, Instant billingCycleAnchor, Instant endOfCurrentPeriod, boolean active,
boolean cancelAtPeriodEnd, String currency, BigDecimal amount, String status, 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().currency(),
subscriptionInformation.price().amount(), subscriptionInformation.price().amount(),
subscriptionInformation.status().getApiValue(), subscriptionInformation.status().getApiValue(),
manager.getProcessor()), manager.getProcessor(),
subscriptionInformation.paymentMethod(),
subscriptionInformation.paymentProcessing()),
subscriptionInformation.chargeFailure() subscriptionInformation.chargeFailure()
)).build())); )).build()));
}); });

View File

@ -426,12 +426,17 @@ 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 ChargeFailure chargeFailure = getLatestTransactionForSubscription(subscription).map(transaction -> { boolean paymentProcessing = false;
if (getPaymentStatus(transaction.getStatus()).equals(PaymentStatus.SUCCEEDED)) { ChargeFailure chargeFailure = null;
return 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( return new SubscriptionInformation(
new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT), new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT),
@ -442,11 +447,25 @@ public class BraintreeManager implements SubscriptionProcessorManager {
Subscription.Status.ACTIVE == subscription.getStatus(), Subscription.Status.ACTIVE == subscription.getStatus(),
!subscription.neverExpires(), !subscription.neverExpires(),
getSubscriptionStatus(subscription.getStatus()), getSubscriptionStatus(subscription.getStatus()),
latestTransaction.map(this::getPaymentMethodFromTransaction).orElse(PaymentMethod.PAYPAL),
paymentProcessing,
chargeFailure chargeFailure
); );
}, executor); }, 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) { private ChargeFailure createChargeFailure(Transaction transaction) {
final String code; final String code;

View File

@ -6,6 +6,7 @@
package org.whispersystems.textsecuregcm.subscriptions; package org.whispersystems.textsecuregcm.subscriptions;
public enum PaymentMethod { public enum PaymentMethod {
UNKNOWN,
/** /**
* A credit card or debit card, including those from Apple Pay and Google Pay * A credit card or debit card, including those from Apple Pay and Google Pay
*/ */

View File

@ -67,10 +67,12 @@ import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang3.StringUtils; 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.Conversions;
public class StripeManager implements SubscriptionProcessorManager { 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_LEVEL = "level";
private final StripeClient stripeClient; private final StripeClient stripeClient;
@ -483,17 +485,30 @@ public class StripeManager implements SubscriptionProcessorManager {
return getPriceForSubscription(subscription).thenCompose(price -> return getPriceForSubscription(subscription).thenCompose(price ->
getLevelForPrice(price).thenApply(level -> { getLevelForPrice(price).thenApply(level -> {
ChargeFailure chargeFailure = null; ChargeFailure chargeFailure = null;
boolean paymentProcessing = false;
PaymentMethod paymentMethod = null;
if (subscription.getLatestInvoiceObject() != null && subscription.getLatestInvoiceObject().getChargeObject() != null && if (subscription.getLatestInvoiceObject() != null) {
(subscription.getLatestInvoiceObject().getChargeObject().getFailureCode() != null || subscription.getLatestInvoiceObject().getChargeObject().getFailureMessage() != null)) { final Invoice invoice = subscription.getLatestInvoiceObject();
Charge charge = subscription.getLatestInvoiceObject().getChargeObject(); paymentProcessing = "open".equals(invoice.getStatus());
Charge.Outcome outcome = charge.getOutcome();
chargeFailure = new ChargeFailure( 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.getFailureCode(),
charge.getFailureMessage(), charge.getFailureMessage(),
outcome != null ? outcome.getNetworkStatus() : null, outcome != null ? outcome.getNetworkStatus() : null,
outcome != null ? outcome.getReason() : null, outcome != null ? outcome.getReason() : null,
outcome != null ? outcome.getType() : null); outcome != null ? outcome.getType() : null);
}
if (charge.getPaymentMethodDetails() != null
&& charge.getPaymentMethodDetails().getType() != null) {
paymentMethod = getPaymentMethodFromStripeString(charge.getPaymentMethodDetails().getType(), invoice.getId());
}
}
} }
return new SubscriptionInformation( return new SubscriptionInformation(
@ -504,11 +519,24 @@ public class StripeManager implements SubscriptionProcessorManager {
Objects.equals(subscription.getStatus(), "active"), Objects.equals(subscription.getStatus(), "active"),
subscription.getCancelAtPeriodEnd(), subscription.getCancelAtPeriodEnd(),
getSubscriptionStatus(subscription.getStatus()), getSubscriptionStatus(subscription.getStatus()),
paymentMethod,
paymentProcessing,
chargeFailure 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) { private Subscription getSubscription(Object subscriptionObj) {
if (!(subscriptionObj instanceof final Subscription subscription)) { if (!(subscriptionObj instanceof final Subscription subscription)) {
throw new IllegalArgumentException("invalid subscription object: " + subscriptionObj.getClass().getName()); throw new IllegalArgumentException("invalid subscription object: " + subscriptionObj.getClass().getName());

View File

@ -144,8 +144,8 @@ public interface SubscriptionProcessorManager {
record SubscriptionInformation(SubscriptionPrice price, long level, Instant billingCycleAnchor, record SubscriptionInformation(SubscriptionPrice price, long level, Instant billingCycleAnchor,
Instant endOfCurrentPeriod, boolean active, boolean cancelAtPeriodEnd, Instant endOfCurrentPeriod, boolean active, boolean cancelAtPeriodEnd,
SubscriptionStatus status, SubscriptionStatus status, PaymentMethod paymentMethod, boolean paymentProcessing,
ChargeFailure chargeFailure) { @Nullable ChargeFailure chargeFailure) {
} }