mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 00:19:22 +01:00
Add playbilling endpoint to /v1/subscriptions
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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 subscription’s current level and lower-case currency code
|
||||
*/
|
||||
CompletableFuture<LevelAndCurrency> getLevelAndCurrencyForSubscription(Object subscription);
|
||||
|
||||
record SubscriptionId(String id) {
|
||||
|
||||
}
|
||||
|
||||
record LevelAndCurrency(long level, String currency) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
@@ -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 subscription’s 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 subscription’s 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);
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
@@ -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 subscription’s 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user