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) {
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()));
});

View File

@ -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;

View File

@ -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
*/

View File

@ -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());

View File

@ -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) {
}