Add playbilling endpoint to /v1/subscriptions

This commit is contained in:
ravi-signal
2024-08-30 12:50:18 -05:00
committed by GitHub
parent 3b4d445ca8
commit 564dba3053
17 changed files with 614 additions and 272 deletions

View File

@@ -53,7 +53,7 @@ import org.whispersystems.textsecuregcm.util.GoogleApiUtil;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
public class BraintreeManager implements SubscriptionPaymentProcessor {
public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcessor {
private static final Logger logger = LoggerFactory.getLogger(BraintreeManager.class);
@@ -496,10 +496,9 @@ public class BraintreeManager implements SubscriptionPaymentProcessor {
}
@Override
public CompletableFuture<SubscriptionInformation> getSubscriptionInformation(Object subscriptionObj) {
final Subscription subscription = getSubscription(subscriptionObj);
return CompletableFuture.supplyAsync(() -> {
public CompletableFuture<SubscriptionInformation> getSubscriptionInformation(final String subscriptionId) {
return getSubscription(subscriptionId).thenApplyAsync(subscriptionObj -> {
final Subscription subscription = getSubscription(subscriptionObj);
final Plan plan = braintreeGateway.plan().find(subscription.getPlanId());
@@ -531,10 +530,12 @@ public class BraintreeManager implements SubscriptionPaymentProcessor {
Subscription.Status.ACTIVE == subscription.getStatus(),
!subscription.neverExpires(),
getSubscriptionStatus(subscription.getStatus(), latestTransactionFailed),
PaymentProvider.BRAINTREE,
latestTransaction.map(this::getPaymentMethodFromTransaction).orElse(PaymentMethod.PAYPAL),
paymentProcessing,
chargeFailure
);
}, executor);
}

View File

@@ -5,9 +5,51 @@
package org.whispersystems.textsecuregcm.subscriptions;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.annotation.Nullable;
public record ChargeFailure(String code, String message, @Nullable String outcomeNetworkStatus,
@Nullable String outcomeReason, @Nullable String outcomeType) {
/**
* Information about a charge failure.
* <p>
* This is returned directly from {@link org.whispersystems.textsecuregcm.controllers.SubscriptionController}, so modify
* with care.
*/
@Schema(description = """
Meaningfully interpreting chargeFailure response fields requires inspecting the processor field first.
}
For Stripe, code will be one of the [codes defined here](https://stripe.com/docs/api/charges/object#charge_object-failure_code),
while message [may contain a further textual description](https://stripe.com/docs/api/charges/object#charge_object-failure_message).
The outcome fields are nullable, but present values will directly map to Stripe [response properties](https://stripe.com/docs/api/charges/object#charge_object-outcome-network_status)
For Braintree, the outcome fields will be null. The code and message will contain one of
- a processor decline code (as a string) in code, and associated text in message, as defined this [table](https://developer.paypal.com/braintree/docs/reference/general/processor-responses/authorization-responses)
- `gateway` in code, with a [reason](https://developer.paypal.com/braintree/articles/control-panel/transactions/gateway-rejections) in message
- `code` = "unknown", message = "unknown"
IAP payment processors will never include charge failure information, and detailed order information should be
retrieved from the payment processor directly
""")
public record ChargeFailure(
@Schema(description = """
See [Stripe failure codes](https://stripe.com/docs/api/charges/object#charge_object-failure_code) or
[Braintree decline codes](https://developer.paypal.com/braintree/docs/reference/general/processor-responses/authorization-responses#decline-codes)
depending on which processor was used
""")
String code,
@Schema(description = """
See [Stripe failure codes](https://stripe.com/docs/api/charges/object#charge_object-failure_code) or
[Braintree decline codes](https://developer.paypal.com/braintree/docs/reference/general/processor-responses/authorization-responses#decline-codes)
depending on which processor was used
""")
String message,
@Schema(externalDocs = @ExternalDocumentation(description = "Outcome Network Status", url = "https://stripe.com/docs/api/charges/object#charge_object-outcome-network_status"))
@Nullable String outcomeNetworkStatus,
@Schema(externalDocs = @ExternalDocumentation(description = "Outcome Reason", url = "https://stripe.com/docs/api/charges/object#charge_object-outcome-reason"))
@Nullable String outcomeReason,
@Schema(externalDocs = @ExternalDocumentation(description = "Outcome Type", url = "https://stripe.com/docs/api/charges/object#charge_object-outcome-type"))
@Nullable String outcomeType) {}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.subscriptions;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
/**
* Interface for an external payment provider that has an API-accessible notion of customer that implementations can
* manage. Payment providers that let you add and remove payment methods to an existing customer should implement this
* interface. Contrast this with the super interface {@link SubscriptionPaymentProcessor}, which allows for a payment
* provider with an API that only operations on subscriptions.
*/
public interface CustomerAwareSubscriptionPaymentProcessor extends SubscriptionPaymentProcessor {
boolean supportsPaymentMethod(PaymentMethod paymentMethod);
Set<String> getSupportedCurrenciesForPaymentMethod(PaymentMethod paymentMethod);
CompletableFuture<ProcessorCustomer> createCustomer(byte[] subscriberUser, @Nullable ClientPlatform clientPlatform);
CompletableFuture<String> createPaymentMethodSetupToken(String customerId);
/**
* @param customerId
* @param paymentMethodToken a processor-specific token necessary
* @param currentSubscriptionId (nullable) an active subscription ID, in case it needs an explicit update
* @return
*/
CompletableFuture<Void> setDefaultPaymentMethodForCustomer(String customerId, String paymentMethodToken,
@Nullable String currentSubscriptionId);
CompletableFuture<Object> getSubscription(String subscriptionId);
CompletableFuture<SubscriptionId> createSubscription(String customerId, String templateId, long level,
long lastSubscriptionCreatedAt);
CompletableFuture<SubscriptionId> updateSubscription(
Object subscription, String templateId, long level, String idempotencyKey);
/**
* @param subscription
* @return the subscriptions current level and lower-case currency code
*/
CompletableFuture<LevelAndCurrency> getLevelAndCurrencyForSubscription(Object subscription);
record SubscriptionId(String id) {
}
record LevelAndCurrency(long level, String currency) {
}
}

View File

@@ -12,6 +12,9 @@ import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.AndroidPublisherRequest;
import com.google.api.services.androidpublisher.AndroidPublisherScopes;
import com.google.api.services.androidpublisher.model.BasePlan;
import com.google.api.services.androidpublisher.model.OfferDetails;
import com.google.api.services.androidpublisher.model.RegionalBasePlanConfig;
import com.google.api.services.androidpublisher.model.SubscriptionPurchaseLineItem;
import com.google.api.services.androidpublisher.model.SubscriptionPurchaseV2;
import com.google.api.services.androidpublisher.model.SubscriptionPurchasesAcknowledgeRequest;
@@ -28,6 +31,7 @@ import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -41,7 +45,6 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
import org.whispersystems.textsecuregcm.storage.PaymentTime;
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
/**
@@ -56,7 +59,7 @@ import org.whispersystems.textsecuregcm.util.ExceptionUtils;
* <li> querying the current status of a token's underlying subscription </li>
* </ul>
*/
public class GooglePlayBillingManager implements SubscriptionManager.Processor {
public class GooglePlayBillingManager implements SubscriptionPaymentProcessor {
private static final Logger logger = LoggerFactory.getLogger(GooglePlayBillingManager.class);
@@ -218,6 +221,67 @@ public class GooglePlayBillingManager implements SubscriptionManager.Processor {
});
}
@Override
public CompletableFuture<SubscriptionInformation> getSubscriptionInformation(final String purchaseToken) {
final CompletableFuture<SubscriptionPurchaseV2> subscriptionFuture = lookupSubscription(purchaseToken);
final CompletableFuture<SubscriptionPrice> priceFuture = subscriptionFuture.thenCompose(this::getSubscriptionPrice);
return subscriptionFuture.thenCombineAsync(priceFuture, (subscription, price) -> {
final SubscriptionPurchaseLineItem lineItem = getLineItem(subscription);
final Optional<Instant> expiration = getExpiration(lineItem);
final SubscriptionStatus status = switch (SubscriptionState
.fromString(subscription.getSubscriptionState())
.orElse(SubscriptionState.UNSPECIFIED)) {
case ACTIVE -> SubscriptionStatus.ACTIVE;
case PENDING -> SubscriptionStatus.INCOMPLETE;
case EXPIRED, ON_HOLD, PAUSED -> SubscriptionStatus.PAST_DUE;
case IN_GRACE_PERIOD -> SubscriptionStatus.UNPAID;
case CANCELED, PENDING_PURCHASE_CANCELED -> SubscriptionStatus.CANCELED;
case UNSPECIFIED -> SubscriptionStatus.UNKNOWN;
};
return new SubscriptionInformation(
price,
productIdToLevel(lineItem.getProductId()),
null, expiration.orElse(null),
expiration.map(clock.instant()::isBefore).orElse(false),
lineItem.getAutoRenewingPlan() != null && lineItem.getAutoRenewingPlan().getAutoRenewEnabled(),
status,
PaymentProvider.GOOGLE_PLAY_BILLING,
PaymentMethod.GOOGLE_PLAY_BILLING,
false,
null);
}, executor);
}
private CompletableFuture<SubscriptionPrice> getSubscriptionPrice(final SubscriptionPurchaseV2 subscriptionPurchase) {
final SubscriptionPurchaseLineItem lineItem = getLineItem(subscriptionPurchase);
final OfferDetails offerDetails = lineItem.getOfferDetails();
final String basePlanId = offerDetails.getBasePlanId();
return this.executeAsync(pub -> pub.monetization().subscriptions().get(packageName, lineItem.getProductId()))
.thenApplyAsync(subscription -> {
final BasePlan basePlan = subscription.getBasePlans().stream()
.filter(bp -> bp.getBasePlanId().equals(basePlanId))
.findFirst()
.orElseThrow(() -> ExceptionUtils.wrap(new IOException("unknown basePlanId " + basePlanId)));
final String region = subscriptionPurchase.getRegionCode();
final RegionalBasePlanConfig basePlanConfig = basePlan.getRegionalConfigs()
.stream()
.filter(rbpc -> Objects.equals(region, rbpc.getRegionCode()))
.findFirst()
.orElseThrow(() -> ExceptionUtils.wrap(new IOException("unknown subscription region " + region)));
return new SubscriptionPrice(
basePlanConfig.getPrice().getCurrencyCode().toUpperCase(Locale.ROOT),
SubscriptionCurrencyUtil.convertGoogleMoneyToApiAmount(basePlanConfig.getPrice()));
}, executor);
}
@Override
public CompletableFuture<ReceiptItem> getReceiptItem(String purchaseToken) {

View File

@@ -77,7 +77,7 @@ import org.whispersystems.textsecuregcm.storage.PaymentTime;
import org.whispersystems.textsecuregcm.util.Conversions;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
public class StripeManager implements SubscriptionPaymentProcessor {
public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor {
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";
@@ -364,6 +364,7 @@ public class StripeManager implements SubscriptionPaymentProcessor {
.thenApply(subscription1 -> new SubscriptionId(subscription1.getId()));
}
@Override
public CompletableFuture<Object> getSubscription(String subscriptionId) {
return CompletableFuture.supplyAsync(() -> {
SubscriptionRetrieveParams params = SubscriptionRetrieveParams.builder()
@@ -378,6 +379,7 @@ public class StripeManager implements SubscriptionPaymentProcessor {
}, executor);
}
@Override
public CompletableFuture<Void> cancelAllActiveSubscriptions(String customerId) {
return getCustomer(customerId).thenCompose(customer -> {
if (customer == null) {
@@ -536,46 +538,45 @@ public class StripeManager implements SubscriptionPaymentProcessor {
}
@Override
public CompletableFuture<SubscriptionInformation> getSubscriptionInformation(Object subscriptionObj) {
public CompletableFuture<SubscriptionInformation> getSubscriptionInformation(final String subscriptionId) {
return getSubscription(subscriptionId).thenApply(this::getSubscription).thenCompose(subscription ->
getPriceForSubscription(subscription).thenCompose(price ->
getLevelForPrice(price).thenApply(level -> {
ChargeFailure chargeFailure = null;
boolean paymentProcessing = false;
PaymentMethod paymentMethod = null;
final Subscription subscription = getSubscription(subscriptionObj);
if (subscription.getLatestInvoiceObject() != null) {
final Invoice invoice = subscription.getLatestInvoiceObject();
paymentProcessing = "open".equals(invoice.getStatus());
return getPriceForSubscription(subscription).thenCompose(price ->
getLevelForPrice(price).thenApply(level -> {
ChargeFailure chargeFailure = null;
boolean paymentProcessing = false;
PaymentMethod paymentMethod = null;
if (invoice.getChargeObject() != null) {
final Charge charge = invoice.getChargeObject();
if (charge.getFailureCode() != null || charge.getFailureMessage() != null) {
chargeFailure = createChargeFailure(charge);
}
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) {
chargeFailure = createChargeFailure(charge);
}
if (charge.getPaymentMethodDetails() != null
&& charge.getPaymentMethodDetails().getType() != null) {
paymentMethod = getPaymentMethodFromStripeString(charge.getPaymentMethodDetails().getType(), invoice.getId());
}
if (charge.getPaymentMethodDetails() != null
&& charge.getPaymentMethodDetails().getType() != null) {
paymentMethod = getPaymentMethodFromStripeString(charge.getPaymentMethodDetails().getType(), invoice.getId());
}
}
}
return new SubscriptionInformation(
new SubscriptionPrice(price.getCurrency().toUpperCase(Locale.ROOT), price.getUnitAmountDecimal()),
level,
Instant.ofEpochSecond(subscription.getBillingCycleAnchor()),
Instant.ofEpochSecond(subscription.getCurrentPeriodEnd()),
Objects.equals(subscription.getStatus(), "active"),
subscription.getCancelAtPeriodEnd(),
getSubscriptionStatus(subscription.getStatus()),
paymentMethod,
paymentProcessing,
chargeFailure
);
}));
return new SubscriptionInformation(
new SubscriptionPrice(price.getCurrency().toUpperCase(Locale.ROOT), price.getUnitAmountDecimal()),
level,
Instant.ofEpochSecond(subscription.getBillingCycleAnchor()),
Instant.ofEpochSecond(subscription.getCurrentPeriodEnd()),
Objects.equals(subscription.getStatus(), "active"),
subscription.getCancelAtPeriodEnd(),
getSubscriptionStatus(subscription.getStatus()),
PaymentProvider.STRIPE,
paymentMethod,
paymentProcessing,
chargeFailure
);
})));
}
private static PaymentMethod getPaymentMethodFromStripeString(final String paymentMethodString, final String invoiceId) {

View File

@@ -5,6 +5,7 @@
package org.whispersystems.textsecuregcm.subscriptions;
import com.google.api.services.androidpublisher.model.Money;
import java.math.BigDecimal;
import java.util.Locale;
import java.util.Set;
@@ -71,4 +72,16 @@ public class SubscriptionCurrencyUtil {
static BigDecimal convertBraintreeAmountToApiAmount(final String currency, final BigDecimal amount) {
return convertConfiguredAmountToApiAmount(currency, amount);
}
/**
* Convert Play Billing's representation of currency amounts to a Stripe-style amount
*
* @see org.whispersystems.textsecuregcm.subscriptions.SubscriptionCurrencyUtil#convertConfiguredAmountToApiAmount(String,
* BigDecimal)
*/
static BigDecimal convertGoogleMoneyToApiAmount(final Money money) {
final BigDecimal fractionalComponent = BigDecimal.valueOf(money.getNanos()).scaleByPowerOfTen(-9);
final BigDecimal amount = BigDecimal.valueOf(money.getUnits()).add(fractionalComponent);
return convertConfiguredAmountToApiAmount(money.getCurrencyCode(), amount);
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.subscriptions;
import java.time.Instant;
import javax.annotation.Nullable;
public record SubscriptionInformation(
SubscriptionPrice price,
long level,
Instant billingCycleAnchor,
Instant endOfCurrentPeriod,
boolean active,
boolean cancelAtPeriodEnd,
SubscriptionStatus status,
PaymentProvider paymentProvider,
PaymentMethod paymentMethod,
boolean paymentProcessing,
@Nullable ChargeFailure chargeFailure) {}

View File

@@ -2,139 +2,42 @@
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.subscriptions;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Set;
import org.whispersystems.textsecuregcm.storage.PaymentTime;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
public interface SubscriptionPaymentProcessor extends SubscriptionManager.Processor {
boolean supportsPaymentMethod(PaymentMethod paymentMethod);
Set<String> getSupportedCurrenciesForPaymentMethod(PaymentMethod paymentMethod);
CompletableFuture<ProcessorCustomer> createCustomer(byte[] subscriberUser, @Nullable ClientPlatform clientPlatform);
CompletableFuture<String> createPaymentMethodSetupToken(String customerId);
public interface SubscriptionPaymentProcessor {
PaymentProvider getProvider();
/**
* @param customerId
* @param paymentMethodToken a processor-specific token necessary
* @param currentSubscriptionId (nullable) an active subscription ID, in case it needs an explicit update
* @return
* A receipt of payment from a payment provider
*
* @param itemId An identifier for the payment that should be unique within the payment provider. Note that this
* must identify an actual individual charge, not the subscription as a whole.
* @param paymentTime The time this payment was for
* @param level The level which this payment corresponds to
*/
CompletableFuture<Void> setDefaultPaymentMethodForCustomer(String customerId, String paymentMethodToken,
@Nullable String currentSubscriptionId);
CompletableFuture<Object> getSubscription(String subscriptionId);
CompletableFuture<SubscriptionId> createSubscription(String customerId, String templateId, long level,
long lastSubscriptionCreatedAt);
CompletableFuture<SubscriptionId> updateSubscription(
Object subscription, String templateId, long level, String idempotencyKey);
record ReceiptItem(String itemId, PaymentTime paymentTime, long level) {}
/**
* @param subscription
* @return the subscriptions current level and lower-case currency code
* Retrieve a {@link ReceiptItem} for the subscriptionId stored in the subscriptions table
*
* @param subscriptionId A subscriptionId that potentially corresponds to a valid subscription
* @return A {@link ReceiptItem} if the subscription is valid
*/
CompletableFuture<LevelAndCurrency> getLevelAndCurrencyForSubscription(Object subscription);
CompletableFuture<ReceiptItem> getReceiptItem(String subscriptionId);
CompletableFuture<SubscriptionInformation> getSubscriptionInformation(Object subscription);
enum SubscriptionStatus {
/**
* The subscription is in good standing and the most recent payment was successful.
*/
ACTIVE("active"),
/**
* Payment failed when creating the subscription, or the subscriptions start date is in the future.
*/
INCOMPLETE("incomplete"),
/**
* Payment on the latest renewal failed but there are processor retries left, or payment wasn't attempted.
*/
PAST_DUE("past_due"),
/**
* The subscription has been canceled.
*/
CANCELED("canceled"),
/**
* The latest renewal hasn't been paid but the subscription remains in place.
*/
UNPAID("unpaid"),
/**
* The status from the downstream processor is unknown.
*/
UNKNOWN("unknown");
private final String apiValue;
SubscriptionStatus(String apiValue) {
this.apiValue = apiValue;
}
public static SubscriptionStatus forApiValue(String status) {
return switch (status) {
case "active" -> ACTIVE;
case "canceled", "incomplete_expired" -> CANCELED;
case "unpaid" -> UNPAID;
case "past_due" -> PAST_DUE;
case "incomplete" -> INCOMPLETE;
case "trialing" -> {
final Logger logger = LoggerFactory.getLogger(SubscriptionPaymentProcessor.class);
logger.error("Subscription has status that should never happen: {}", status);
yield UNKNOWN;
}
default -> {
final Logger logger = LoggerFactory.getLogger(SubscriptionPaymentProcessor.class);
logger.error("Subscription has unknown status: {}", status);
yield UNKNOWN;
}
};
}
public String getApiValue() {
return apiValue;
}
}
record SubscriptionId(String id) {
}
record SubscriptionInformation(SubscriptionPrice price, long level, Instant billingCycleAnchor,
Instant endOfCurrentPeriod, boolean active, boolean cancelAtPeriodEnd,
SubscriptionStatus status, PaymentMethod paymentMethod, boolean paymentProcessing,
@Nullable ChargeFailure chargeFailure) {
}
record SubscriptionPrice(String currency, BigDecimal amount) {
}
record LevelAndCurrency(long level, String currency) {
}
/**
* Cancel all active subscriptions for this key within the payment provider.
*
* @param key An identifier for the subscriber within the payment provider, corresponds to the customerId field in the
* subscriptions table
* @return A stage that completes when all subscriptions associated with the key are cancelled
*/
CompletableFuture<Void> cancelAllActiveSubscriptions(String key);
CompletableFuture<SubscriptionInformation> getSubscriptionInformation(final String subscriptionId);
}

View File

@@ -0,0 +1,9 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.subscriptions;
import java.math.BigDecimal;
public record SubscriptionPrice(String currency, BigDecimal amount) {}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.subscriptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public enum SubscriptionStatus {
/**
* The subscription is in good standing and the most recent payment was successful.
*/
ACTIVE("active"),
/**
* Payment failed when creating the subscription, or the subscriptions start date is in the future.
*/
INCOMPLETE("incomplete"),
/**
* Payment on the latest renewal failed but there are processor retries left, or payment wasn't attempted.
*/
PAST_DUE("past_due"),
/**
* The subscription has been canceled.
*/
CANCELED("canceled"),
/**
* The latest renewal hasn't been paid but the subscription remains in place.
*/
UNPAID("unpaid"),
/**
* The status from the downstream processor is unknown.
*/
UNKNOWN("unknown");
private final String apiValue;
SubscriptionStatus(String apiValue) {
this.apiValue = apiValue;
}
public static SubscriptionStatus forApiValue(String status) {
return switch (status) {
case "active" -> ACTIVE;
case "canceled", "incomplete_expired" -> CANCELED;
case "unpaid" -> UNPAID;
case "past_due" -> PAST_DUE;
case "incomplete" -> INCOMPLETE;
case "trialing" -> {
final Logger logger = LoggerFactory.getLogger(CustomerAwareSubscriptionPaymentProcessor.class);
logger.error("Subscription has status that should never happen: {}", status);
yield UNKNOWN;
}
default -> {
final Logger logger = LoggerFactory.getLogger(CustomerAwareSubscriptionPaymentProcessor.class);
logger.error("Subscription has unknown status: {}", status);
yield UNKNOWN;
}
};
}
public String getApiValue() {
return apiValue;
}
}