mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 02:08:04 +01:00
Simplify SubscriptionExceptions
This commit is contained in:
@@ -72,7 +72,7 @@ import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriberCredentials;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionException;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Subscriptions;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.AppleAppStoreManager;
|
||||
@@ -86,6 +86,10 @@ import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentProvider;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionInvalidArgumentsException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionInvalidLevelException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionPaymentRequiresActionException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionReceiptRequestedForOpenPaymentException;
|
||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
@@ -415,19 +419,19 @@ public class SubscriptionController {
|
||||
subscriptionManager.updateSubscriptionLevelForCustomer(subscriberCredentials, record, manager, level,
|
||||
currency, idempotencyKey, subscriptionTemplateId, this::subscriptionsAreSameType);
|
||||
return new SetSubscriptionLevelSuccessResponse(level);
|
||||
} catch (SubscriptionException.InvalidLevel e) {
|
||||
} catch (SubscriptionInvalidLevelException e) {
|
||||
throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new SubscriptionController.SetSubscriptionLevelErrorResponse(List.of(
|
||||
new SubscriptionController.SetSubscriptionLevelErrorResponse.Error(
|
||||
SubscriptionController.SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_LEVEL,
|
||||
null))))
|
||||
.build());
|
||||
} catch (SubscriptionException.PaymentRequiresAction e) {
|
||||
} catch (SubscriptionPaymentRequiresActionException e) {
|
||||
throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new SetSubscriptionLevelErrorResponse(List.of(new SetSubscriptionLevelErrorResponse.Error(
|
||||
SetSubscriptionLevelErrorResponse.Error.Type.PAYMENT_REQUIRES_ACTION, null))))
|
||||
.build());
|
||||
} catch (SubscriptionException.InvalidArguments e) {
|
||||
} catch (SubscriptionInvalidArgumentsException e) {
|
||||
throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new SetSubscriptionLevelErrorResponse(List.of(new SetSubscriptionLevelErrorResponse.Error(
|
||||
SetSubscriptionLevelErrorResponse.Error.Type.INVALID_ARGUMENTS, e.getMessage()))))
|
||||
@@ -768,7 +772,7 @@ public class SubscriptionController {
|
||||
UserAgentTagUtil.getPlatformTag(userAgent)))
|
||||
.increment();
|
||||
return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize())).build();
|
||||
} catch (SubscriptionException.ReceiptRequestedForOpenPayment e) {
|
||||
} catch (SubscriptionReceiptRequestedForOpenPaymentException e) {
|
||||
return Response.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -802,7 +806,7 @@ public class SubscriptionController {
|
||||
|
||||
manager
|
||||
.setDefaultPaymentMethodForCustomer(processorCustomer.customerId(), paymentMethodId, record.subscriptionId);
|
||||
} catch (SubscriptionException.InvalidArguments e) {
|
||||
} catch (SubscriptionInvalidArgumentsException e) {
|
||||
// Here, invalid arguments must mean that the client has made requests out of order, and needs to finish
|
||||
// setting up the paymentMethod first
|
||||
throw new ClientErrorException(Status.CONFLICT);
|
||||
|
||||
@@ -12,8 +12,16 @@ import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
import java.util.Map;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionChargeFailurePaymentRequiredException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionForbiddenException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionInvalidAmountException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionInvalidArgumentsException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionNotFoundException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionPaymentRequiredException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorConflictException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorException;
|
||||
|
||||
public class SubscriptionExceptionMapper implements ExceptionMapper<SubscriptionException> {
|
||||
@VisibleForTesting
|
||||
@@ -25,20 +33,20 @@ public class SubscriptionExceptionMapper implements ExceptionMapper<Subscription
|
||||
public Response toResponse(final SubscriptionException exception) {
|
||||
|
||||
// Some exceptions have specific error body formats
|
||||
if (exception instanceof SubscriptionException.InvalidAmount e) {
|
||||
if (exception instanceof SubscriptionInvalidAmountException e) {
|
||||
return Response
|
||||
.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", e.getErrorCode()))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.build();
|
||||
}
|
||||
if (exception instanceof SubscriptionException.ProcessorException e) {
|
||||
if (exception instanceof SubscriptionProcessorException e) {
|
||||
return Response.status(PROCESSOR_ERROR_STATUS_CODE)
|
||||
.entity(new ChargeFailureResponse(e.getProcessor().name(), e.getChargeFailure()))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.build();
|
||||
}
|
||||
if (exception instanceof SubscriptionException.ChargeFailurePaymentRequired e) {
|
||||
if (exception instanceof SubscriptionChargeFailurePaymentRequiredException e) {
|
||||
return Response
|
||||
.status(Response.Status.PAYMENT_REQUIRED)
|
||||
.entity(new ChargeFailureResponse(e.getProcessor().name(), e.getChargeFailure()))
|
||||
@@ -48,11 +56,11 @@ public class SubscriptionExceptionMapper implements ExceptionMapper<Subscription
|
||||
|
||||
// Otherwise, we'll return a generic error message WebApplicationException, with a detailed error if one is provided
|
||||
final Response.Status status = (switch (exception) {
|
||||
case SubscriptionException.NotFound e -> Response.Status.NOT_FOUND;
|
||||
case SubscriptionException.Forbidden e -> Response.Status.FORBIDDEN;
|
||||
case SubscriptionException.InvalidArguments e -> Response.Status.BAD_REQUEST;
|
||||
case SubscriptionException.ProcessorConflict e -> Response.Status.CONFLICT;
|
||||
case SubscriptionException.PaymentRequired e -> Response.Status.PAYMENT_REQUIRED;
|
||||
case SubscriptionNotFoundException e -> Response.Status.NOT_FOUND;
|
||||
case SubscriptionForbiddenException e -> Response.Status.FORBIDDEN;
|
||||
case SubscriptionInvalidArgumentsException e -> Response.Status.BAD_REQUEST;
|
||||
case SubscriptionProcessorConflictException e -> Response.Status.CONFLICT;
|
||||
case SubscriptionPaymentRequiredException e -> Response.Status.PAYMENT_REQUIRED;
|
||||
default -> Response.Status.INTERNAL_SERVER_ERROR;
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ import javax.annotation.Nonnull;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionForbiddenException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionNotFoundException;
|
||||
|
||||
public record SubscriberCredentials(@Nonnull byte[] subscriberBytes,
|
||||
@Nonnull byte[] subscriberUser,
|
||||
@@ -25,10 +28,10 @@ public record SubscriberCredentials(@Nonnull byte[] subscriberBytes,
|
||||
public static SubscriberCredentials process(
|
||||
Optional<AuthenticatedDevice> authenticatedAccount,
|
||||
String subscriberId,
|
||||
Clock clock) throws SubscriptionException{
|
||||
Clock clock) throws SubscriptionException {
|
||||
Instant now = clock.instant();
|
||||
if (authenticatedAccount.isPresent()) {
|
||||
throw new SubscriptionException.Forbidden("must not use authenticated connection for subscriber operations");
|
||||
throw new SubscriptionForbiddenException("must not use authenticated connection for subscriber operations");
|
||||
}
|
||||
byte[] subscriberBytes = convertSubscriberIdStringToBytes(subscriberId);
|
||||
byte[] subscriberUser = getUser(subscriberBytes);
|
||||
@@ -37,15 +40,15 @@ public record SubscriberCredentials(@Nonnull byte[] subscriberBytes,
|
||||
return new SubscriberCredentials(subscriberBytes, subscriberUser, subscriberKey, hmac, now);
|
||||
}
|
||||
|
||||
private static byte[] convertSubscriberIdStringToBytes(String subscriberId) throws SubscriptionException.NotFound {
|
||||
private static byte[] convertSubscriberIdStringToBytes(String subscriberId) throws SubscriptionNotFoundException {
|
||||
try {
|
||||
byte[] bytes = Base64.getUrlDecoder().decode(subscriberId);
|
||||
if (bytes.length != 32) {
|
||||
throw new SubscriptionException.NotFound();
|
||||
throw new SubscriptionNotFoundException();
|
||||
}
|
||||
return bytes;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new SubscriptionException.NotFound(e);
|
||||
throw new SubscriptionNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentProvider;
|
||||
|
||||
public class SubscriptionException extends Exception {
|
||||
|
||||
private @Nullable String errorDetail;
|
||||
|
||||
public SubscriptionException(Exception cause) {
|
||||
this(cause, null);
|
||||
}
|
||||
|
||||
SubscriptionException(Exception cause, String errorDetail) {
|
||||
super(cause);
|
||||
this.errorDetail = errorDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An error message suitable to include in a client response
|
||||
*/
|
||||
public Optional<String> errorDetail() {
|
||||
return Optional.ofNullable(errorDetail);
|
||||
}
|
||||
|
||||
public static class NotFound extends SubscriptionException {
|
||||
|
||||
public NotFound() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
public NotFound(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Forbidden extends SubscriptionException {
|
||||
|
||||
public Forbidden(final String message) {
|
||||
super(null, message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidArguments extends SubscriptionException {
|
||||
|
||||
public InvalidArguments(final String message, final Exception cause) {
|
||||
super(cause, message);
|
||||
}
|
||||
|
||||
public InvalidArguments(final String message) {
|
||||
this(message, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidLevel extends InvalidArguments {
|
||||
|
||||
public InvalidLevel() {
|
||||
super(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidAmount extends InvalidArguments {
|
||||
private String errorCode;
|
||||
|
||||
public InvalidAmount(String errorCode) {
|
||||
super(null, null);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PaymentRequiresAction extends InvalidArguments {
|
||||
|
||||
public PaymentRequiresAction(String message) {
|
||||
super(message, null);
|
||||
}
|
||||
|
||||
public PaymentRequiresAction() {
|
||||
super(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PaymentRequired extends SubscriptionException {
|
||||
|
||||
public PaymentRequired() {
|
||||
super(null, null);
|
||||
}
|
||||
|
||||
public PaymentRequired(String message) {
|
||||
super(null, message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChargeFailurePaymentRequired extends SubscriptionException {
|
||||
|
||||
private final PaymentProvider processor;
|
||||
private final ChargeFailure chargeFailure;
|
||||
|
||||
public ChargeFailurePaymentRequired(final PaymentProvider processor, final ChargeFailure chargeFailure) {
|
||||
super(null, null);
|
||||
this.processor = processor;
|
||||
this.chargeFailure = chargeFailure;
|
||||
}
|
||||
|
||||
public PaymentProvider getProcessor() {
|
||||
return processor;
|
||||
}
|
||||
|
||||
public ChargeFailure getChargeFailure() {
|
||||
return chargeFailure;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class ProcessorException extends SubscriptionException {
|
||||
|
||||
private final PaymentProvider processor;
|
||||
private final ChargeFailure chargeFailure;
|
||||
|
||||
public ProcessorException(final PaymentProvider processor, final ChargeFailure chargeFailure) {
|
||||
super(null, null);
|
||||
this.processor = processor;
|
||||
this.chargeFailure = chargeFailure;
|
||||
}
|
||||
|
||||
public PaymentProvider getProcessor() {
|
||||
return processor;
|
||||
}
|
||||
|
||||
public ChargeFailure getChargeFailure() {
|
||||
return chargeFailure;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempted to retrieve a receipt for a subscription that hasn't yet been charged or the invoice is in the open
|
||||
* state
|
||||
*/
|
||||
public static class ReceiptRequestedForOpenPayment extends SubscriptionException {
|
||||
|
||||
public ReceiptRequestedForOpenPayment() {
|
||||
super(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ProcessorConflict extends SubscriptionException {
|
||||
public ProcessorConflict() {
|
||||
super(null, null);
|
||||
}
|
||||
|
||||
public ProcessorConflict(final String message) {
|
||||
super(null, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.stripe.exception.StripeException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Instant;
|
||||
@@ -31,7 +30,14 @@ import org.whispersystems.textsecuregcm.subscriptions.PaymentProvider;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionInformation;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionPaymentProcessor;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionForbiddenException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionInvalidArgumentsException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionInvalidLevelException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionNotFoundException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionPaymentRequiredException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorConflictException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorException;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionReceiptRequestedForOpenPaymentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
|
||||
/**
|
||||
@@ -65,18 +71,18 @@ public class SubscriptionManager {
|
||||
* Cancel a subscription with the upstream payment provider and remove the subscription from the table
|
||||
*
|
||||
* @param subscriberCredentials Subscriber credentials derived from the subscriberId
|
||||
* @throws RateLimitExceededException if rate-limited
|
||||
* @throws SubscriptionException.NotFound if the provided credentials are incorrect or the subscriber does not
|
||||
* exist
|
||||
* @throws SubscriptionException.InvalidArguments if a precondition for cancellation was not met
|
||||
* @throws RateLimitExceededException if rate-limited
|
||||
* @throws SubscriptionNotFoundException if the provided credentials are incorrect or the subscriber does not
|
||||
* exist
|
||||
* @throws SubscriptionInvalidArgumentsException if a precondition for cancellation was not met
|
||||
*/
|
||||
public void deleteSubscriber(final SubscriberCredentials subscriberCredentials)
|
||||
throws SubscriptionException.NotFound, SubscriptionException.InvalidArguments, RateLimitExceededException {
|
||||
throws SubscriptionNotFoundException, SubscriptionInvalidArgumentsException, RateLimitExceededException {
|
||||
final Subscriptions.GetResult getResult =
|
||||
subscriptions.get(subscriberCredentials.subscriberUser(), subscriberCredentials.hmac()).join();
|
||||
if (getResult == Subscriptions.GetResult.NOT_STORED
|
||||
|| getResult == Subscriptions.GetResult.PASSWORD_MISMATCH) {
|
||||
throw new SubscriptionException.NotFound();
|
||||
throw new SubscriptionNotFoundException();
|
||||
}
|
||||
|
||||
// a missing customer ID is OK; it means the subscriber never started to add a payment method, so we can skip cancelling
|
||||
@@ -94,22 +100,22 @@ public class SubscriptionManager {
|
||||
* already exists, its last access time will be updated.
|
||||
*
|
||||
* @param subscriberCredentials Subscriber credentials derived from the subscriberId
|
||||
* @throws SubscriptionException.Forbidden if the subscriber credentials were incorrect
|
||||
* @throws SubscriptionForbiddenException if the subscriber credentials were incorrect
|
||||
*/
|
||||
public void updateSubscriber(final SubscriberCredentials subscriberCredentials)
|
||||
throws SubscriptionException.Forbidden {
|
||||
throws SubscriptionForbiddenException {
|
||||
final Subscriptions.GetResult getResult =
|
||||
subscriptions.get(subscriberCredentials.subscriberUser(), subscriberCredentials.hmac()).join();
|
||||
|
||||
if (getResult == Subscriptions.GetResult.PASSWORD_MISMATCH) {
|
||||
throw new SubscriptionException.Forbidden("subscriberId mismatch");
|
||||
throw new SubscriptionForbiddenException("subscriberId mismatch");
|
||||
} else if (getResult == Subscriptions.GetResult.NOT_STORED) {
|
||||
// create a customer and write it to ddb
|
||||
final Subscriptions.Record updatedRecord = subscriptions.create(subscriberCredentials.subscriberUser(),
|
||||
subscriberCredentials.hmac(),
|
||||
subscriberCredentials.now()).join();
|
||||
if (updatedRecord == null) {
|
||||
throw new SubscriptionException.Forbidden("subscriberId mismatch");
|
||||
throw new SubscriptionForbiddenException("subscriberId mismatch");
|
||||
}
|
||||
} else {
|
||||
// already exists so just touch access time and return
|
||||
@@ -119,7 +125,7 @@ public class SubscriptionManager {
|
||||
|
||||
public Optional<SubscriptionInformation> getSubscriptionInformation(
|
||||
final SubscriberCredentials subscriberCredentials)
|
||||
throws SubscriptionException.Forbidden, SubscriptionException.NotFound, SubscriptionException.InvalidArguments, RateLimitExceededException {
|
||||
throws SubscriptionForbiddenException, SubscriptionNotFoundException, RateLimitExceededException {
|
||||
final Subscriptions.Record record = getSubscriber(subscriberCredentials);
|
||||
if (record.subscriptionId == null) {
|
||||
return Optional.empty();
|
||||
@@ -132,17 +138,17 @@ public class SubscriptionManager {
|
||||
* Get the subscriber record
|
||||
*
|
||||
* @param subscriberCredentials Subscriber credentials derived from the subscriberId
|
||||
* @throws SubscriptionException.Forbidden if the subscriber credentials were incorrect
|
||||
* @throws SubscriptionException.NotFound if the subscriber did not exist
|
||||
* @throws SubscriptionForbiddenException if the subscriber credentials were incorrect
|
||||
* @throws SubscriptionNotFoundException if the subscriber did not exist
|
||||
*/
|
||||
public Subscriptions.Record getSubscriber(final SubscriberCredentials subscriberCredentials)
|
||||
throws SubscriptionException.Forbidden, SubscriptionException.NotFound {
|
||||
throws SubscriptionForbiddenException, SubscriptionNotFoundException {
|
||||
final Subscriptions.GetResult getResult =
|
||||
subscriptions.get(subscriberCredentials.subscriberUser(), subscriberCredentials.hmac()).join();
|
||||
if (getResult == Subscriptions.GetResult.PASSWORD_MISMATCH) {
|
||||
throw new SubscriptionException.Forbidden("subscriberId mismatch");
|
||||
throw new SubscriptionForbiddenException("subscriberId mismatch");
|
||||
} else if (getResult == Subscriptions.GetResult.NOT_STORED) {
|
||||
throw new SubscriptionException.NotFound();
|
||||
throw new SubscriptionNotFoundException();
|
||||
} else {
|
||||
return getResult.record;
|
||||
}
|
||||
@@ -161,34 +167,31 @@ public class SubscriptionManager {
|
||||
* @param expiration A function that takes a {@link CustomerAwareSubscriptionPaymentProcessor.ReceiptItem}
|
||||
* and returns the expiration time of the receipt
|
||||
* @return the requested ZK receipt credential
|
||||
* @throws SubscriptionException.Forbidden if the subscriber credentials were incorrect
|
||||
* @throws SubscriptionException.NotFound if the subscriber did not exist or did not have a
|
||||
* subscription attached
|
||||
* @throws SubscriptionException.InvalidArguments if the receipt credential request failed verification
|
||||
* @throws SubscriptionException.PaymentRequired if the subscription is in a state does not grant the
|
||||
* user an entitlement
|
||||
* @throws SubscriptionException.ChargeFailurePaymentRequired if the subscription is in a state does not grant the
|
||||
* user an entitlement because a charge failed to go
|
||||
* through
|
||||
* @throws SubscriptionException.ReceiptRequestedForOpenPayment if a receipt was requested while a payment transaction
|
||||
* was still open
|
||||
* @throws RateLimitExceededException if rate-limited
|
||||
* @throws SubscriptionForbiddenException if the subscriber credentials were incorrect
|
||||
* @throws SubscriptionNotFoundException if the subscriber did not exist or did not have a
|
||||
* subscription attached
|
||||
* @throws SubscriptionInvalidArgumentsException if the receipt credential request failed verification
|
||||
* @throws SubscriptionPaymentRequiredException if the subscription is in a state does not grant the
|
||||
* user an entitlement
|
||||
* @throws SubscriptionReceiptRequestedForOpenPaymentException if a receipt was requested while a payment transaction
|
||||
* was still open
|
||||
* @throws RateLimitExceededException if rate-limited
|
||||
*/
|
||||
public ReceiptResult createReceiptCredentials(
|
||||
final SubscriberCredentials subscriberCredentials,
|
||||
final SubscriptionController.GetReceiptCredentialsRequest request,
|
||||
final Function<CustomerAwareSubscriptionPaymentProcessor.ReceiptItem, Instant> expiration)
|
||||
throws SubscriptionException.Forbidden, SubscriptionException.NotFound, SubscriptionException.InvalidArguments, SubscriptionException.ChargeFailurePaymentRequired, SubscriptionException.PaymentRequired, RateLimitExceededException, SubscriptionException.ReceiptRequestedForOpenPayment {
|
||||
throws SubscriptionForbiddenException, SubscriptionNotFoundException, SubscriptionInvalidArgumentsException, SubscriptionPaymentRequiredException, RateLimitExceededException, SubscriptionReceiptRequestedForOpenPaymentException {
|
||||
final Subscriptions.Record record = getSubscriber(subscriberCredentials);
|
||||
if (record.subscriptionId == null) {
|
||||
throw new SubscriptionException.NotFound();
|
||||
throw new SubscriptionNotFoundException();
|
||||
}
|
||||
|
||||
ReceiptCredentialRequest receiptCredentialRequest;
|
||||
try {
|
||||
receiptCredentialRequest = new ReceiptCredentialRequest(request.receiptCredentialRequest());
|
||||
} catch (InvalidInputException e) {
|
||||
throw new SubscriptionException.InvalidArguments("invalid receipt credential request", e);
|
||||
throw new SubscriptionInvalidArgumentsException("invalid receipt credential request", e);
|
||||
}
|
||||
|
||||
final PaymentProvider processor = record.getProcessorCustomer().orElseThrow().processor();
|
||||
@@ -204,7 +207,7 @@ public class SubscriptionManager {
|
||||
expiration.apply(receipt).getEpochSecond(),
|
||||
receipt.level());
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new SubscriptionException.InvalidArguments("receipt credential request failed verification", e);
|
||||
throw new SubscriptionInvalidArgumentsException("receipt credential request failed verification", e);
|
||||
}
|
||||
return new ReceiptResult(receiptCredentialResponse, receipt, processor);
|
||||
}
|
||||
@@ -228,18 +231,18 @@ public class SubscriptionManager {
|
||||
* @param <R> The return type of the paymentSetupFunction, which should be used by a client
|
||||
* to configure the newly created payment method
|
||||
* @return The return value of the paymentSetupFunction
|
||||
* @throws SubscriptionException.Forbidden if the subscriber credentials were incorrect
|
||||
* @throws SubscriptionException.NotFound if the subscriber did not exist or did not have a subscription
|
||||
* attached
|
||||
* @throws SubscriptionException.ProcessorConflict if the new payment processor the existing processor associated with
|
||||
* the subscriberId
|
||||
* @throws SubscriptionForbiddenException if the subscriber credentials were incorrect
|
||||
* @throws SubscriptionNotFoundException if the subscriber did not exist or did not have a subscription
|
||||
* attached
|
||||
* @throws SubscriptionProcessorConflictException if the new payment processor the existing processor associated with
|
||||
* the subscriberId
|
||||
*/
|
||||
public <T extends CustomerAwareSubscriptionPaymentProcessor, R> R addPaymentMethodToCustomer(
|
||||
final SubscriberCredentials subscriberCredentials,
|
||||
final T subscriptionPaymentProcessor,
|
||||
final ClientPlatform clientPlatform,
|
||||
final BiFunction<T, String, R> paymentSetupFunction)
|
||||
throws SubscriptionException.Forbidden, SubscriptionException.NotFound, SubscriptionException.ProcessorConflict {
|
||||
throws SubscriptionForbiddenException, SubscriptionNotFoundException, SubscriptionProcessorConflictException {
|
||||
|
||||
Subscriptions.Record record = this.getSubscriber(subscriberCredentials);
|
||||
if (record.getProcessorCustomer().isEmpty()) {
|
||||
@@ -253,7 +256,7 @@ public class SubscriptionManager {
|
||||
.orElseThrow(() -> new UncheckedIOException(new IOException("processor must now exist")));
|
||||
|
||||
if (processorCustomer.processor() != subscriptionPaymentProcessor.getProvider()) {
|
||||
throw new SubscriptionException.ProcessorConflict("existing processor does not match");
|
||||
throw new SubscriptionProcessorConflictException("existing processor does not match");
|
||||
}
|
||||
return paymentSetupFunction.apply(subscriptionPaymentProcessor, processorCustomer.customerId());
|
||||
}
|
||||
@@ -288,13 +291,13 @@ public class SubscriptionManager {
|
||||
* @param subscriptionTemplateId Specifies the product associated with the provided level within the payment
|
||||
* processor
|
||||
* @param transitionValidator A function that checks if the level update is valid
|
||||
* @throws SubscriptionException.InvalidArguments if the transitionValidator failed for the level transition, or the
|
||||
* subscription could not be created because the payment provider
|
||||
* requires additional action, or there was a failure because an
|
||||
* idempotency key was reused on a * modified request
|
||||
* @throws SubscriptionException.ProcessorConflict if the new payment processor the existing processor associated
|
||||
* with the subscriber
|
||||
* @throws SubscriptionException.ProcessorException if there was no payment method on the customer
|
||||
* @throws SubscriptionInvalidArgumentsException if the transitionValidator failed for the level transition, or the
|
||||
* subscription could not be created because the payment provider
|
||||
* requires additional action, or there was a failure because an
|
||||
* idempotency key was reused on a * modified request
|
||||
* @throws SubscriptionProcessorConflictException if the new payment processor the existing processor associated with
|
||||
* the subscriber
|
||||
* @throws SubscriptionProcessorException if there was no payment method on the customer
|
||||
*/
|
||||
public void updateSubscriptionLevelForCustomer(
|
||||
final SubscriberCredentials subscriberCredentials,
|
||||
@@ -305,7 +308,7 @@ public class SubscriptionManager {
|
||||
final String idempotencyKey,
|
||||
final String subscriptionTemplateId,
|
||||
final LevelTransitionValidator transitionValidator)
|
||||
throws SubscriptionException.InvalidArguments, SubscriptionException.ProcessorConflict, SubscriptionException.ProcessorException {
|
||||
throws SubscriptionInvalidArgumentsException, SubscriptionProcessorConflictException, SubscriptionProcessorException {
|
||||
|
||||
if (record.subscriptionId != null) {
|
||||
// we already have a subscription in our records so let's check the level and currency,
|
||||
@@ -319,7 +322,7 @@ public class SubscriptionManager {
|
||||
return;
|
||||
}
|
||||
if (!transitionValidator.isTransitionValid(existingLevelAndCurrency.level(), level)) {
|
||||
throw new SubscriptionException.InvalidLevel();
|
||||
throw new SubscriptionInvalidLevelException();
|
||||
}
|
||||
final CustomerAwareSubscriptionPaymentProcessor.SubscriptionId updatedSubscriptionId =
|
||||
processor.updateSubscription(subscription, subscriptionTemplateId, level, idempotencyKey);
|
||||
@@ -353,20 +356,20 @@ public class SubscriptionManager {
|
||||
* @param purchaseToken The client provided purchaseToken that represents a purchased subscription in the
|
||||
* play store
|
||||
* @return the subscription level for the accepted subscription
|
||||
* @throws SubscriptionException.Forbidden if the subscriber credentials were incorrect
|
||||
* @throws SubscriptionException.NotFound if the subscriber did not exist or did not have a subscription
|
||||
* attached
|
||||
* @throws SubscriptionException.ProcessorConflict if the new payment processor the existing processor associated with
|
||||
* the subscriberId
|
||||
* @throws SubscriptionException.PaymentRequired if the subscription is not in a state that grants the user an
|
||||
* entitlement
|
||||
* @throws RateLimitExceededException if rate-limited
|
||||
* @throws SubscriptionForbiddenException if the subscriber credentials were incorrect
|
||||
* @throws SubscriptionNotFoundException if the subscriber did not exist or did not have a subscription
|
||||
* attached
|
||||
* @throws SubscriptionProcessorConflictException if the new payment processor the existing processor associated with
|
||||
* the subscriberId
|
||||
* @throws SubscriptionPaymentRequiredException if the subscription is not in a state that grants the user an
|
||||
* entitlement
|
||||
* @throws RateLimitExceededException if rate-limited
|
||||
*/
|
||||
public long updatePlayBillingPurchaseToken(
|
||||
final SubscriberCredentials subscriberCredentials,
|
||||
final GooglePlayBillingManager googlePlayBillingManager,
|
||||
final String purchaseToken)
|
||||
throws SubscriptionException.ProcessorConflict, SubscriptionException.Forbidden, SubscriptionException.NotFound, RateLimitExceededException, SubscriptionException.PaymentRequired {
|
||||
throws SubscriptionProcessorConflictException, SubscriptionForbiddenException, SubscriptionNotFoundException, RateLimitExceededException, SubscriptionPaymentRequiredException {
|
||||
|
||||
// For IAP providers, the subscriptionId and the customerId are both just the purchaseToken. Changes to the
|
||||
// subscription always just result in a new purchaseToken
|
||||
@@ -377,7 +380,7 @@ public class SubscriptionManager {
|
||||
// Check the record for an existing subscription
|
||||
if (record.processorCustomer != null
|
||||
&& record.processorCustomer.processor() != PaymentProvider.GOOGLE_PLAY_BILLING) {
|
||||
throw new SubscriptionException.ProcessorConflict("existing processor does not match");
|
||||
throw new SubscriptionProcessorConflictException("existing processor does not match");
|
||||
}
|
||||
|
||||
// If we're replacing an existing purchaseToken, cancel it first
|
||||
@@ -406,26 +409,26 @@ public class SubscriptionManager {
|
||||
* @param originalTransactionId The client provided originalTransactionId that represents a purchased subscription in
|
||||
* the app store
|
||||
* @return the subscription level for the accepted subscription
|
||||
* @throws SubscriptionException.Forbidden if the subscriber credentials are incorrect
|
||||
* @throws SubscriptionException.NotFound if the originalTransactionId does not exist
|
||||
* @throws SubscriptionException.ProcessorConflict if the new payment processor the existing processor associated with
|
||||
* the subscriber
|
||||
* @throws SubscriptionException.InvalidArguments if the originalTransactionId is malformed or does not represent a
|
||||
* valid subscription
|
||||
* @throws SubscriptionException.PaymentRequired if the subscription is not in a state that grants the user an
|
||||
* entitlement
|
||||
* @throws RateLimitExceededException if rate-limited
|
||||
* @throws SubscriptionForbiddenException if the subscriber credentials are incorrect
|
||||
* @throws SubscriptionNotFoundException if the originalTransactionId does not exist
|
||||
* @throws SubscriptionProcessorConflictException if the new payment processor the existing processor associated with
|
||||
* the subscriber
|
||||
* @throws SubscriptionInvalidArgumentsException if the originalTransactionId is malformed or does not represent a
|
||||
* valid subscription
|
||||
* @throws SubscriptionPaymentRequiredException if the subscription is not in a state that grants the user an
|
||||
* entitlement
|
||||
* @throws RateLimitExceededException if rate-limited
|
||||
*/
|
||||
public long updateAppStoreTransactionId(
|
||||
final SubscriberCredentials subscriberCredentials,
|
||||
final AppleAppStoreManager appleAppStoreManager,
|
||||
final String originalTransactionId)
|
||||
throws SubscriptionException.Forbidden, SubscriptionException.NotFound, SubscriptionException.ProcessorConflict, SubscriptionException.InvalidArguments, SubscriptionException.PaymentRequired, RateLimitExceededException {
|
||||
throws SubscriptionForbiddenException, SubscriptionNotFoundException, SubscriptionProcessorConflictException, SubscriptionInvalidArgumentsException, SubscriptionPaymentRequiredException, RateLimitExceededException {
|
||||
|
||||
final Subscriptions.Record record = getSubscriber(subscriberCredentials);
|
||||
if (record.processorCustomer != null
|
||||
&& record.processorCustomer.processor() != PaymentProvider.APPLE_APP_STORE) {
|
||||
throw new SubscriptionException.ProcessorConflict("existing processor does not match");
|
||||
throw new SubscriptionProcessorConflictException("existing processor does not match");
|
||||
}
|
||||
|
||||
// For IAP providers, the subscriptionId and the customerId are both just the identifier for the subscription in
|
||||
|
||||
@@ -41,7 +41,6 @@ import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||
import org.whispersystems.textsecuregcm.util.ResilienceUtil;
|
||||
|
||||
/**
|
||||
@@ -114,16 +113,16 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor {
|
||||
* @param originalTransactionId The originalTransactionId associated with the subscription
|
||||
* @return the subscription level of the valid transaction.
|
||||
* @throws RateLimitExceededException If rate-limited
|
||||
* @throws SubscriptionException.NotFound If the provided originalTransactionId was not found
|
||||
* @throws SubscriptionException.PaymentRequired If the originalTransactionId exists but is in a state that does not
|
||||
* @throws SubscriptionNotFoundException If the provided originalTransactionId was not found
|
||||
* @throws SubscriptionPaymentRequiredException If the originalTransactionId exists but is in a state that does not
|
||||
* grant the user an entitlement
|
||||
* @throws SubscriptionException.InvalidArguments If the transaction is valid but does not contain a subscription
|
||||
* @throws SubscriptionInvalidArgumentsException If the transaction is valid but does not contain a subscription
|
||||
*/
|
||||
public Long validateTransaction(final String originalTransactionId)
|
||||
throws SubscriptionException.InvalidArguments, RateLimitExceededException, SubscriptionException.NotFound, SubscriptionException.PaymentRequired {
|
||||
final DecodedTransaction tx = lookup(originalTransactionId);
|
||||
throws SubscriptionInvalidArgumentsException, RateLimitExceededException, SubscriptionNotFoundException, SubscriptionPaymentRequiredException {
|
||||
final DecodedTransaction tx = lookupAndValidateTransaction(originalTransactionId);
|
||||
if (!isSubscriptionActive(tx)) {
|
||||
throw new SubscriptionException.PaymentRequired();
|
||||
throw new SubscriptionPaymentRequiredException();
|
||||
}
|
||||
return getLevel(tx);
|
||||
}
|
||||
@@ -137,25 +136,29 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor {
|
||||
* this method.
|
||||
*
|
||||
* @param originalTransactionId The originalTransactionId associated with the subscription
|
||||
* @throws SubscriptionException.NotFound If the provided originalTransactionId was not found
|
||||
* @throws SubscriptionException.InvalidArguments If the transaction is valid but does not contain a subscription, or
|
||||
* @throws RateLimitExceededException If rate-limited
|
||||
* @throws SubscriptionInvalidArgumentsException If the transaction is valid but does not contain a subscription, or
|
||||
* the transaction has not already been cancelled with storekit
|
||||
*/
|
||||
@Override
|
||||
public void cancelAllActiveSubscriptions(String originalTransactionId)
|
||||
throws SubscriptionException.InvalidArguments, RateLimitExceededException, SubscriptionException.NotFound {
|
||||
final DecodedTransaction tx = lookup(originalTransactionId);
|
||||
if (tx.signedTransaction.getStatus() != Status.EXPIRED &&
|
||||
tx.signedTransaction.getStatus() != Status.REVOKED &&
|
||||
tx.renewalInfo.getAutoRenewStatus() != AutoRenewStatus.OFF) {
|
||||
throw new SubscriptionException.InvalidArguments("must cancel subscription with storekit before deleting");
|
||||
throws SubscriptionInvalidArgumentsException, RateLimitExceededException {
|
||||
try {
|
||||
final DecodedTransaction tx = lookup(originalTransactionId);
|
||||
if (tx.signedTransaction.getStatus() != Status.EXPIRED &&
|
||||
tx.signedTransaction.getStatus() != Status.REVOKED &&
|
||||
tx.renewalInfo.getAutoRenewStatus() != AutoRenewStatus.OFF) {
|
||||
throw new SubscriptionInvalidArgumentsException("must cancel subscription with storekit before deleting");
|
||||
}
|
||||
} catch (SubscriptionNotFoundException _) {
|
||||
// If the subscription is not found there is no need to do anything, so we can squash it
|
||||
}
|
||||
// The subscription will not auto-renew, so we can stop tracking it
|
||||
// The subscription will not auto-renew, so we can stop tracking it
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubscriptionInformation getSubscriptionInformation(final String originalTransactionId)
|
||||
throws SubscriptionException.InvalidArguments, RateLimitExceededException, SubscriptionException.NotFound {
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException {
|
||||
final DecodedTransaction tx = lookup(originalTransactionId);
|
||||
final SubscriptionStatus status = switch (tx.signedTransaction.getStatus()) {
|
||||
case ACTIVE -> SubscriptionStatus.ACTIVE;
|
||||
@@ -181,10 +184,10 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor {
|
||||
|
||||
@Override
|
||||
public ReceiptItem getReceiptItem(String originalTransactionId)
|
||||
throws SubscriptionException.InvalidArguments, RateLimitExceededException, SubscriptionException.NotFound, SubscriptionException.PaymentRequired {
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException, SubscriptionPaymentRequiredException {
|
||||
final DecodedTransaction tx = lookup(originalTransactionId);
|
||||
if (!isSubscriptionActive(tx)) {
|
||||
throw new SubscriptionException.PaymentRequired();
|
||||
throw new SubscriptionPaymentRequiredException();
|
||||
}
|
||||
|
||||
// A new transactionId might be generated if you restore a subscription on a new device. webOrderLineItemId is
|
||||
@@ -198,11 +201,21 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor {
|
||||
}
|
||||
|
||||
private DecodedTransaction lookup(final String originalTransactionId)
|
||||
throws SubscriptionException.InvalidArguments, RateLimitExceededException, SubscriptionException.NotFound {
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException {
|
||||
try {
|
||||
return lookupAndValidateTransaction(originalTransactionId);
|
||||
} catch (SubscriptionInvalidArgumentsException e) {
|
||||
// Shouldn't happen because we previously validated this transactionId before storing it
|
||||
throw new UncheckedIOException(new IOException(e));
|
||||
}
|
||||
}
|
||||
|
||||
private DecodedTransaction lookupAndValidateTransaction(final String originalTransactionId)
|
||||
throws SubscriptionInvalidArgumentsException, RateLimitExceededException, SubscriptionNotFoundException {
|
||||
final StatusResponse statuses = getAllSubscriptions(originalTransactionId);
|
||||
final SubscriptionGroupIdentifierItem item = statuses.getData().stream()
|
||||
.filter(s -> subscriptionGroupId.equals(s.getSubscriptionGroupIdentifier())).findFirst()
|
||||
.orElseThrow(() -> new SubscriptionException.InvalidArguments("transaction did not contain a backup subscription", null));
|
||||
.orElseThrow(() -> new SubscriptionInvalidArgumentsException("transaction did not contain a backup subscription", null));
|
||||
|
||||
final List<DecodedTransaction> txs = item.getLastTransactions().stream()
|
||||
.map(this::decode)
|
||||
@@ -210,7 +223,7 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor {
|
||||
.toList();
|
||||
|
||||
if (txs.isEmpty()) {
|
||||
throw new SubscriptionException.InvalidArguments("transactionId did not include a paid subscription", null);
|
||||
throw new SubscriptionInvalidArgumentsException("transactionId did not include a paid subscription", null);
|
||||
}
|
||||
|
||||
if (txs.size() > 1) {
|
||||
@@ -222,13 +235,13 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor {
|
||||
// Get All Subscriptions only requires that the transaction be some transaction associated with the
|
||||
// subscription. This is too flexible, since we'd like to key on the originalTransactionId in the
|
||||
// SubscriptionManager.
|
||||
throw new SubscriptionException.InvalidArguments("transactionId was not the transaction's originalTransactionId", null);
|
||||
throw new SubscriptionInvalidArgumentsException("transactionId was not the transaction's originalTransactionId", null);
|
||||
}
|
||||
return txs.getFirst();
|
||||
}
|
||||
|
||||
private StatusResponse getAllSubscriptions(final String originalTransactionId)
|
||||
throws SubscriptionException.NotFound, SubscriptionException.InvalidArguments, RateLimitExceededException {
|
||||
throws SubscriptionNotFoundException, SubscriptionInvalidArgumentsException, RateLimitExceededException {
|
||||
try {
|
||||
return retry.executeCallable(() -> {
|
||||
try {
|
||||
@@ -236,9 +249,9 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor {
|
||||
} catch (final APIException e) {
|
||||
Metrics.counter(GET_SUBSCRIPTION_ERROR_COUNTER_NAME, "reason", e.getApiError().name()).increment();
|
||||
throw switch (e.getApiError()) {
|
||||
case ORIGINAL_TRANSACTION_ID_NOT_FOUND, TRANSACTION_ID_NOT_FOUND -> new SubscriptionException.NotFound();
|
||||
case ORIGINAL_TRANSACTION_ID_NOT_FOUND, TRANSACTION_ID_NOT_FOUND -> new SubscriptionNotFoundException();
|
||||
case RATE_LIMIT_EXCEEDED -> new RateLimitExceededException(null);
|
||||
case INVALID_ORIGINAL_TRANSACTION_ID -> new SubscriptionException.InvalidArguments(e.getApiErrorMessage());
|
||||
case INVALID_ORIGINAL_TRANSACTION_ID -> new SubscriptionInvalidArgumentsException(e.getApiErrorMessage());
|
||||
default -> e;
|
||||
};
|
||||
} catch (final IOException e) {
|
||||
@@ -246,7 +259,7 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
} catch (SubscriptionException.NotFound | SubscriptionException.InvalidArguments | RateLimitExceededException e) {
|
||||
} catch (SubscriptionNotFoundException | SubscriptionInvalidArgumentsException | RateLimitExceededException e) {
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
@@ -290,11 +303,11 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor {
|
||||
SubscriptionCurrencyUtil.convertConfiguredAmountToApiAmount(tx.transaction.getCurrency(), amount));
|
||||
}
|
||||
|
||||
private long getLevel(final DecodedTransaction tx) throws SubscriptionException.InvalidArguments {
|
||||
private long getLevel(final DecodedTransaction tx) {
|
||||
final Long level = productIdToLevel.get(tx.transaction.getProductId());
|
||||
if (level == null) {
|
||||
throw new SubscriptionException.InvalidArguments(
|
||||
"Transaction for unknown productId " + tx.transaction.getProductId());
|
||||
throw new UncheckedIOException(new IOException(
|
||||
"Transaction for unknown productId " + tx.transaction.getProductId()));
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import javax.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -46,7 +45,6 @@ import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
import org.whispersystems.textsecuregcm.util.ExecutorUtil;
|
||||
import org.whispersystems.textsecuregcm.util.GoogleApiUtil;
|
||||
@@ -207,7 +205,7 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||
return switch (unsuccessfulTx.getProcessorResponseCode()) {
|
||||
case GENERIC_DECLINED_PROCESSOR_CODE, PAYPAL_FUNDING_INSTRUMENT_DECLINED_PROCESSOR_CODE ->
|
||||
CompletableFuture.failedFuture(
|
||||
new SubscriptionException.ProcessorException(getProvider(), createChargeFailure(unsuccessfulTx)));
|
||||
new SubscriptionProcessorException(getProvider(), createChargeFailure(unsuccessfulTx)));
|
||||
|
||||
default -> {
|
||||
logger.info("PayPal charge unexpectedly failed: {}", unsuccessfulTx.getProcessorResponseCode());
|
||||
@@ -362,11 +360,11 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||
@Override
|
||||
public SubscriptionId createSubscription(String customerId, String planId, long level,
|
||||
long lastSubscriptionCreatedAt)
|
||||
throws SubscriptionException.ProcessorConflict, SubscriptionException.ProcessorException {
|
||||
throws SubscriptionProcessorConflictException, SubscriptionProcessorException {
|
||||
|
||||
final com.braintreegateway.PaymentMethod paymentMethod = getDefaultPaymentMethod(customerId);
|
||||
if (paymentMethod == null) {
|
||||
throw new SubscriptionException.ProcessorConflict();
|
||||
throw new SubscriptionProcessorConflictException();
|
||||
}
|
||||
|
||||
final Optional<Subscription> maybeExistingSubscription = paymentMethod.getSubscriptions().stream()
|
||||
@@ -400,7 +398,7 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||
throw Optional
|
||||
.ofNullable(result.getTarget())
|
||||
.flatMap(subscription -> subscription.getTransactions().stream().findFirst())
|
||||
.map(transaction -> new SubscriptionException.ProcessorException(getProvider(),
|
||||
.map(transaction -> new SubscriptionProcessorException(getProvider(),
|
||||
createChargeFailure(transaction)))
|
||||
.orElseThrow(() -> new BraintreeException(result.getMessage()));
|
||||
}
|
||||
@@ -415,7 +413,7 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||
|
||||
@Override
|
||||
public CustomerAwareSubscriptionPaymentProcessor.SubscriptionId updateSubscription(Object subscriptionObj, String planId, long level,
|
||||
String idempotencyKey) throws SubscriptionException.ProcessorConflict, SubscriptionException.ProcessorException {
|
||||
String idempotencyKey) throws SubscriptionProcessorConflictException, SubscriptionProcessorException {
|
||||
|
||||
if (!(subscriptionObj instanceof final Subscription subscription)) {
|
||||
throw new IllegalArgumentException("invalid subscription object: " + subscriptionObj.getClass().getName());
|
||||
@@ -427,7 +425,7 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||
endSubscription(subscription);
|
||||
|
||||
final Transaction transaction = getLatestTransactionForSubscription(subscription)
|
||||
.orElseThrow(() -> ExceptionUtils.wrap(new SubscriptionException.ProcessorConflict()));
|
||||
.orElseThrow(() -> ExceptionUtils.wrap(new SubscriptionProcessorConflictException()));
|
||||
|
||||
final Customer customer = transaction.getCustomer();
|
||||
|
||||
@@ -579,17 +577,17 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
|
||||
|
||||
@Override
|
||||
public ReceiptItem getReceiptItem(String subscriptionId)
|
||||
throws SubscriptionException.ReceiptRequestedForOpenPayment, SubscriptionException.ChargeFailurePaymentRequired {
|
||||
throws SubscriptionReceiptRequestedForOpenPaymentException, SubscriptionChargeFailurePaymentRequiredException {
|
||||
final Subscription subscription = getSubscription(getSubscription(subscriptionId));
|
||||
final Transaction transaction = getLatestTransactionForSubscription(subscription)
|
||||
.orElseThrow(SubscriptionException.ReceiptRequestedForOpenPayment::new);
|
||||
.orElseThrow(SubscriptionReceiptRequestedForOpenPaymentException::new);
|
||||
if (!getPaymentStatus(transaction.getStatus()).equals(PaymentStatus.SUCCEEDED)) {
|
||||
final SubscriptionStatus subscriptionStatus = getSubscriptionStatus(subscription.getStatus(), true);
|
||||
if (subscriptionStatus.equals(SubscriptionStatus.ACTIVE) || subscriptionStatus.equals(
|
||||
SubscriptionStatus.PAST_DUE)) {
|
||||
throw new SubscriptionException.ReceiptRequestedForOpenPayment();
|
||||
throw new SubscriptionReceiptRequestedForOpenPaymentException();
|
||||
}
|
||||
throw new SubscriptionException.ChargeFailurePaymentRequired(getProvider(), createChargeFailure(transaction));
|
||||
throw new SubscriptionChargeFailurePaymentRequiredException(getProvider(), createChargeFailure(transaction));
|
||||
}
|
||||
|
||||
final Instant paidAt = transaction.getSubscriptionDetails().getBillingPeriodStartDate().toInstant();
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
|
||||
/**
|
||||
@@ -42,11 +40,11 @@ public interface CustomerAwareSubscriptionPaymentProcessor extends SubscriptionP
|
||||
* @param paymentMethodToken a processor-specific token previously acquired at
|
||||
* {@link #createPaymentMethodSetupToken}
|
||||
* @param currentSubscriptionId (nullable) an active subscription ID, in case it needs an explicit update
|
||||
* @throws SubscriptionException.InvalidArguments If the paymentMethodToken is invalid or the payment method has not
|
||||
* @throws SubscriptionInvalidArgumentsException If the paymentMethodToken is invalid or the payment method has not
|
||||
* finished being set up
|
||||
*/
|
||||
void setDefaultPaymentMethodForCustomer(String customerId, String paymentMethodToken,
|
||||
@Nullable String currentSubscriptionId) throws SubscriptionException.InvalidArguments;
|
||||
@Nullable String currentSubscriptionId) throws SubscriptionInvalidArgumentsException;
|
||||
|
||||
Object getSubscription(String subscriptionId);
|
||||
|
||||
@@ -58,14 +56,14 @@ public interface CustomerAwareSubscriptionPaymentProcessor extends SubscriptionP
|
||||
* @param level The level of the subscription
|
||||
* @param lastSubscriptionCreatedAt The timestamp of the last successfully created subscription
|
||||
* @return A subscription identifier
|
||||
* @throws SubscriptionException.ProcessorException If there was a failure processing the charge
|
||||
* @throws SubscriptionException.InvalidArguments If there was a failure because an idempotency key was reused on a
|
||||
* @throws SubscriptionProcessorException If there was a failure processing the charge
|
||||
* @throws SubscriptionInvalidArgumentsException If there was a failure because an idempotency key was reused on a
|
||||
* modified request, or if the payment requires additional steps
|
||||
* before charging
|
||||
* @throws SubscriptionException.ProcessorConflict If there was no payment method on the customer
|
||||
* @throws SubscriptionProcessorConflictException If there was no payment method on the customer
|
||||
*/
|
||||
SubscriptionId createSubscription(String customerId, String templateId, long level, long lastSubscriptionCreatedAt)
|
||||
throws SubscriptionException.ProcessorException, SubscriptionException.InvalidArguments, SubscriptionException.ProcessorConflict;
|
||||
throws SubscriptionProcessorException, SubscriptionInvalidArgumentsException, SubscriptionProcessorConflictException;
|
||||
|
||||
/**
|
||||
* Update an existing subscription on a customer
|
||||
@@ -75,14 +73,14 @@ public interface CustomerAwareSubscriptionPaymentProcessor extends SubscriptionP
|
||||
* @param level The target level of the subscription
|
||||
* @param idempotencyKey An idempotency key to prevent retries of successful requests
|
||||
* @return A subscription identifier
|
||||
* @throws SubscriptionException.ProcessorException If there was a failure processing the charge
|
||||
* @throws SubscriptionException.InvalidArguments If there was a failure because an idempotency key was reused on a
|
||||
* @throws SubscriptionProcessorException If there was a failure processing the charge
|
||||
* @throws SubscriptionInvalidArgumentsException If there was a failure because an idempotency key was reused on a
|
||||
* modified request, or if the payment requires additional steps
|
||||
* before charging
|
||||
* @throws SubscriptionException.ProcessorConflict If there was no payment method on the customer
|
||||
* @throws SubscriptionProcessorConflictException If there was no payment method on the customer
|
||||
*/
|
||||
SubscriptionId updateSubscription(Object subscription, String templateId, long level, String idempotencyKey)
|
||||
throws SubscriptionException.InvalidArguments, SubscriptionException.ProcessorException, SubscriptionException.ProcessorConflict;
|
||||
throws SubscriptionInvalidArgumentsException, SubscriptionProcessorException, SubscriptionProcessorConflictException;
|
||||
|
||||
/**
|
||||
* @param subscription
|
||||
|
||||
@@ -46,7 +46,6 @@ import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||
|
||||
/**
|
||||
* Manages subscriptions made with the Play Billing API
|
||||
@@ -134,7 +133,7 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor {
|
||||
* @return A stage that completes when the purchase has been successfully acknowledged
|
||||
*/
|
||||
public void acknowledgePurchase()
|
||||
throws RateLimitExceededException, SubscriptionException.NotFound {
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException {
|
||||
if (!requiresAck) {
|
||||
// We've already acknowledged this purchase on a previous attempt, nothing to do
|
||||
return;
|
||||
@@ -155,12 +154,12 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor {
|
||||
* @param purchaseToken The play store billing purchaseToken that represents a subscription purchase
|
||||
* @return A {@link ValidatedToken} that can be acknowledged
|
||||
* @throws RateLimitExceededException If rate-limited by play-billing
|
||||
* @throws SubscriptionException.NotFound If the provided purchaseToken was not found in play-billing
|
||||
* @throws SubscriptionException.PaymentRequired If the purchaseToken exists but is in a state that does not grant the
|
||||
* @throws SubscriptionNotFoundException If the provided purchaseToken was not found in play-billing
|
||||
* @throws SubscriptionPaymentRequiredException If the purchaseToken exists but is in a state that does not grant the
|
||||
* user an entitlement
|
||||
*/
|
||||
public ValidatedToken validateToken(String purchaseToken)
|
||||
throws RateLimitExceededException, SubscriptionException.NotFound, SubscriptionException.PaymentRequired {
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException, SubscriptionPaymentRequiredException {
|
||||
final SubscriptionPurchaseV2 subscription = lookupSubscription(purchaseToken);
|
||||
final SubscriptionState state = SubscriptionState
|
||||
.fromString(subscription.getSubscriptionState())
|
||||
@@ -175,7 +174,7 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor {
|
||||
if (state != SubscriptionState.ACTIVE
|
||||
&& state != SubscriptionState.IN_GRACE_PERIOD
|
||||
&& state != SubscriptionState.CANCELED) {
|
||||
throw new SubscriptionException.PaymentRequired(
|
||||
throw new SubscriptionPaymentRequiredException(
|
||||
"Cannot acknowledge purchase for subscription in state " + subscription.getSubscriptionState());
|
||||
}
|
||||
|
||||
@@ -221,14 +220,14 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor {
|
||||
|
||||
executeTokenOperation(pub ->
|
||||
pub.purchases().subscriptions().cancel(packageName, purchase.getProductId(), purchaseToken));
|
||||
} catch (SubscriptionException.NotFound e) {
|
||||
// If the subscription is not found, no need to do anything so we can squash it
|
||||
} catch (SubscriptionNotFoundException e) {
|
||||
// If the subscription is not found there is no need to do anything, so we can squash it
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubscriptionInformation getSubscriptionInformation(final String purchaseToken)
|
||||
throws RateLimitExceededException, SubscriptionException.NotFound {
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException {
|
||||
|
||||
final SubscriptionPurchaseV2 subscription = lookupSubscription(purchaseToken);
|
||||
final SubscriptionPrice price = getSubscriptionPrice(subscription);
|
||||
@@ -300,7 +299,7 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor {
|
||||
|
||||
@Override
|
||||
public ReceiptItem getReceiptItem(String purchaseToken)
|
||||
throws RateLimitExceededException, SubscriptionException.NotFound, SubscriptionException.PaymentRequired {
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException, SubscriptionPaymentRequiredException {
|
||||
final SubscriptionPurchaseV2 subscription = lookupSubscription(purchaseToken);
|
||||
final AcknowledgementState acknowledgementState = AcknowledgementState
|
||||
.fromString(subscription.getAcknowledgementState())
|
||||
@@ -321,7 +320,7 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor {
|
||||
// We don't need to check any state at this point, just whether the subscription is currently valid. If the
|
||||
// subscription is in a grace period, the expiration time will be dynamically extended, see
|
||||
// https://developer.android.com/google/play/billing/lifecycle/subscriptions#grace-period
|
||||
throw new SubscriptionException.PaymentRequired();
|
||||
throw new SubscriptionPaymentRequiredException();
|
||||
}
|
||||
|
||||
return new ReceiptItem(
|
||||
@@ -345,13 +344,13 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor {
|
||||
* @return A stage that completes with the result of the API call
|
||||
*/
|
||||
private <R> R executeTokenOperation(final ApiCall<R> apiCall)
|
||||
throws RateLimitExceededException, SubscriptionException.NotFound {
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException {
|
||||
try {
|
||||
return apiCall.req(androidPublisher).execute();
|
||||
} catch (HttpResponseException e) {
|
||||
if (e.getStatusCode() == Response.Status.NOT_FOUND.getStatusCode()
|
||||
|| e.getStatusCode() == Response.Status.GONE.getStatusCode()) {
|
||||
throw new SubscriptionException.NotFound();
|
||||
throw new SubscriptionNotFoundException();
|
||||
}
|
||||
if (e.getStatusCode() == Response.Status.TOO_MANY_REQUESTS.getStatusCode()) {
|
||||
throw new RateLimitExceededException(null);
|
||||
@@ -368,7 +367,7 @@ public class GooglePlayBillingManager implements SubscriptionPaymentProcessor {
|
||||
}
|
||||
|
||||
private SubscriptionPurchaseV2 lookupSubscription(final String purchaseToken)
|
||||
throws RateLimitExceededException, SubscriptionException.NotFound {
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException {
|
||||
return executeTokenOperation(publisher -> publisher.purchases().subscriptionsv2().get(packageName, purchaseToken));
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -76,7 +75,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.WhisperServerVersion;
|
||||
import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
import org.whispersystems.textsecuregcm.util.ExecutorUtil;
|
||||
@@ -175,7 +173,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||
|
||||
@Override
|
||||
public void setDefaultPaymentMethodForCustomer(String customerId, String paymentMethodId,
|
||||
@Nullable String currentSubscriptionId) throws SubscriptionException.InvalidArguments {
|
||||
@Nullable String currentSubscriptionId) throws SubscriptionInvalidArgumentsException {
|
||||
CustomerUpdateParams params = CustomerUpdateParams.builder()
|
||||
.setInvoiceSettings(InvoiceSettings.builder()
|
||||
.setDefaultPaymentMethod(paymentMethodId)
|
||||
@@ -185,7 +183,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||
stripeClient.customers().update(customerId, params, commonOptions());
|
||||
} catch (InvalidRequestException e) {
|
||||
// Could happen if the paymentMethodId was bunk or the client didn't actually finish setting it up
|
||||
throw new SubscriptionException.InvalidArguments(e.getMessage());
|
||||
throw new SubscriptionInvalidArgumentsException(e.getMessage());
|
||||
} catch (StripeException e) {
|
||||
throw new UncheckedIOException(new IOException(e));
|
||||
}
|
||||
@@ -209,7 +207,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a payment intent. May throw a {@link SubscriptionException.InvalidAmount} if stripe rejects the
|
||||
* Creates a payment intent. May throw a {@link SubscriptionInvalidAmountException} if stripe rejects the
|
||||
* attempt if the amount is too large or too small
|
||||
*/
|
||||
public CompletableFuture<PaymentIntent> createPaymentIntent(final String currency,
|
||||
@@ -234,7 +232,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||
final String errorCode = StringUtils.lowerCase(e.getCode(), Locale.ROOT);
|
||||
switch (errorCode) {
|
||||
case "amount_too_small","amount_too_large" ->
|
||||
throw ExceptionUtils.wrap(new SubscriptionException.InvalidAmount(errorCode));
|
||||
throw ExceptionUtils.wrap(new SubscriptionInvalidAmountException(errorCode));
|
||||
default -> throw new CompletionException(e);
|
||||
}
|
||||
}
|
||||
@@ -286,7 +284,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||
@Override
|
||||
public SubscriptionId createSubscription(String customerId, String priceId, long level,
|
||||
long lastSubscriptionCreatedAt)
|
||||
throws SubscriptionException.ProcessorException, SubscriptionException.InvalidArguments {
|
||||
throws SubscriptionProcessorException, SubscriptionInvalidArgumentsException {
|
||||
// this relies on Stripe's idempotency key to avoid creating more than one subscription if the client
|
||||
// retries this request
|
||||
SubscriptionCreateParams params = SubscriptionCreateParams.builder()
|
||||
@@ -308,12 +306,12 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||
commonOptions(generateIdempotencyKeyForCreateSubscription(customerId, lastSubscriptionCreatedAt)));
|
||||
return new SubscriptionId(subscription.getId());
|
||||
} catch (IdempotencyException e) {
|
||||
throw new SubscriptionException.InvalidArguments(e.getStripeError().getMessage());
|
||||
throw new SubscriptionInvalidArgumentsException(e.getStripeError().getMessage());
|
||||
} catch (CardException e) {
|
||||
throw new SubscriptionException.ProcessorException(getProvider(), createChargeFailureFromCardException(e));
|
||||
throw new SubscriptionProcessorException(getProvider(), createChargeFailureFromCardException(e));
|
||||
} catch (StripeException e) {
|
||||
if ("subscription_payment_intent_requires_action".equals(e.getCode())) {
|
||||
throw new SubscriptionException.PaymentRequiresAction();
|
||||
throw new SubscriptionPaymentRequiresActionException();
|
||||
}
|
||||
throw new UncheckedIOException(new IOException(e));
|
||||
}
|
||||
@@ -321,7 +319,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||
|
||||
@Override
|
||||
public SubscriptionId updateSubscription(Object subscriptionObj, String priceId, long level, String idempotencyKey)
|
||||
throws SubscriptionException.InvalidArguments, SubscriptionException.ProcessorException {
|
||||
throws SubscriptionInvalidArgumentsException, SubscriptionProcessorException {
|
||||
|
||||
final Subscription subscription = getSubscription(subscriptionObj);
|
||||
|
||||
@@ -360,9 +358,9 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||
commonOptions(generateIdempotencyKeyForSubscriptionUpdate(subscription.getCustomer(), idempotencyKey)));
|
||||
return new SubscriptionId(subscription1.getId());
|
||||
} catch (IdempotencyException e) {
|
||||
throw new SubscriptionException.InvalidArguments(e.getStripeError().getMessage());
|
||||
throw new SubscriptionInvalidArgumentsException(e.getStripeError().getMessage());
|
||||
} catch (CardException e) {
|
||||
throw new SubscriptionException.ProcessorException(getProvider(), createChargeFailureFromCardException(e));
|
||||
throw new SubscriptionProcessorException(getProvider(), createChargeFailureFromCardException(e));
|
||||
} catch (StripeException e) {
|
||||
throw new UncheckedIOException(new IOException(e));
|
||||
}
|
||||
@@ -607,27 +605,27 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||
|
||||
@Override
|
||||
public ReceiptItem getReceiptItem(String subscriptionId)
|
||||
throws SubscriptionException.ChargeFailurePaymentRequired, SubscriptionException.PaymentRequired, SubscriptionException.ReceiptRequestedForOpenPayment {
|
||||
throws SubscriptionPaymentRequiredException, SubscriptionReceiptRequestedForOpenPaymentException {
|
||||
final Invoice invoice = getSubscription(getSubscription(subscriptionId)).getLatestInvoiceObject();
|
||||
return convertInvoiceToReceipt(invoice, subscriptionId);
|
||||
}
|
||||
|
||||
private ReceiptItem convertInvoiceToReceipt(Invoice latestSubscriptionInvoice, String subscriptionId)
|
||||
throws SubscriptionException.ReceiptRequestedForOpenPayment, SubscriptionException.ChargeFailurePaymentRequired, SubscriptionException.PaymentRequired {
|
||||
throws SubscriptionReceiptRequestedForOpenPaymentException, SubscriptionPaymentRequiredException {
|
||||
if (latestSubscriptionInvoice == null) {
|
||||
throw new SubscriptionException.ReceiptRequestedForOpenPayment();
|
||||
throw new SubscriptionReceiptRequestedForOpenPaymentException();
|
||||
}
|
||||
if (StringUtils.equalsIgnoreCase("open", latestSubscriptionInvoice.getStatus())) {
|
||||
throw new SubscriptionException.ReceiptRequestedForOpenPayment();
|
||||
throw new SubscriptionReceiptRequestedForOpenPaymentException();
|
||||
}
|
||||
if (!StringUtils.equalsIgnoreCase("paid", latestSubscriptionInvoice.getStatus())) {
|
||||
final Charge charge = latestSubscriptionInvoice.getChargeObject();
|
||||
if (charge != null && (charge.getFailureCode() != null || charge.getFailureMessage() != null)) {
|
||||
// If the charge object has a failure reason we can present to the user, create a detailed exception
|
||||
throw new SubscriptionException.ChargeFailurePaymentRequired(getProvider(), createChargeFailure(charge));
|
||||
throw new SubscriptionChargeFailurePaymentRequiredException(getProvider(), createChargeFailure(charge));
|
||||
} else {
|
||||
// Otherwise, return a generic payment required error
|
||||
throw new SubscriptionException.PaymentRequired();
|
||||
throw new SubscriptionPaymentRequiredException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,12 +687,12 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
|
||||
// This usually indicates that the client has made requests out of order, either by not confirming
|
||||
// the SetupIntent or not having the user authorize the transaction.
|
||||
logger.debug("setupIntent {} missing expected fields", setupIntentId);
|
||||
throw ExceptionUtils.wrap(new SubscriptionException.ProcessorConflict());
|
||||
throw ExceptionUtils.wrap(new SubscriptionProcessorConflictException());
|
||||
}
|
||||
return setupIntent.getLatestAttemptObject().getPaymentMethodDetails().getIdeal().getGeneratedSepaDebit();
|
||||
} catch (StripeException e) {
|
||||
if (e.getStatusCode() == 404) {
|
||||
throw ExceptionUtils.wrap(new SubscriptionException.NotFound());
|
||||
throw ExceptionUtils.wrap(new SubscriptionNotFoundException());
|
||||
}
|
||||
logger.error("unexpected error from Stripe when retrieving setupIntent {}", setupIntentId, e);
|
||||
throw ExceptionUtils.wrap(e);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionChargeFailurePaymentRequiredException extends SubscriptionPaymentRequiredException {
|
||||
|
||||
private final PaymentProvider processor;
|
||||
private final ChargeFailure chargeFailure;
|
||||
|
||||
public SubscriptionChargeFailurePaymentRequiredException(final PaymentProvider processor,
|
||||
final ChargeFailure chargeFailure) {
|
||||
super();
|
||||
this.processor = processor;
|
||||
this.chargeFailure = chargeFailure;
|
||||
}
|
||||
|
||||
public PaymentProvider getProcessor() {
|
||||
return processor;
|
||||
}
|
||||
|
||||
public ChargeFailure getChargeFailure() {
|
||||
return chargeFailure;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class SubscriptionException extends Exception {
|
||||
|
||||
private @Nullable String errorDetail;
|
||||
|
||||
public SubscriptionException(Exception cause) {
|
||||
this(cause, null);
|
||||
}
|
||||
|
||||
SubscriptionException(Exception cause, String errorDetail) {
|
||||
super(cause);
|
||||
this.errorDetail = errorDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An error message suitable to include in a client response
|
||||
*/
|
||||
public Optional<String> errorDetail() {
|
||||
return Optional.ofNullable(errorDetail);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionForbiddenException extends SubscriptionException {
|
||||
|
||||
public SubscriptionForbiddenException(final String message) {
|
||||
super(null, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionInvalidAmountException extends SubscriptionInvalidArgumentsException {
|
||||
|
||||
private String errorCode;
|
||||
|
||||
public SubscriptionInvalidAmountException(String errorCode) {
|
||||
super(null, null);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionInvalidArgumentsException extends SubscriptionException {
|
||||
|
||||
public SubscriptionInvalidArgumentsException(final String message, final Exception cause) {
|
||||
super(cause, message);
|
||||
}
|
||||
|
||||
public SubscriptionInvalidArgumentsException(final String message) {
|
||||
this(message, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionInvalidLevelException extends SubscriptionInvalidArgumentsException {
|
||||
|
||||
public SubscriptionInvalidLevelException() {
|
||||
super(null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionNotFoundException extends SubscriptionException {
|
||||
|
||||
public SubscriptionNotFoundException() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
public SubscriptionNotFoundException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,6 @@ package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.storage.PaymentTime;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionException;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface SubscriptionPaymentProcessor {
|
||||
|
||||
@@ -29,22 +26,16 @@ public interface SubscriptionPaymentProcessor {
|
||||
*
|
||||
* @param subscriptionId A subscriptionId that potentially corresponds to a valid subscription
|
||||
* @return A {@link ReceiptItem} if the subscription is valid
|
||||
*
|
||||
* @throws RateLimitExceededException If rate-limited
|
||||
* @throws SubscriptionException.NotFound If the provided subscriptionId could not be found with
|
||||
* the provider
|
||||
* @throws SubscriptionException.InvalidArguments If the subscriptionId locates a subscription that
|
||||
* cannot be used to generate a receipt
|
||||
* @throws SubscriptionException.PaymentRequired If the subscription is in a state does not grant the
|
||||
* user an entitlement
|
||||
* @throws SubscriptionException.ChargeFailurePaymentRequired If the subscription is in a state does not grant the
|
||||
* user an entitlement because a charge failed to go
|
||||
* through
|
||||
* @throws SubscriptionException.ReceiptRequestedForOpenPayment If a receipt was requested while a payment transaction
|
||||
* was still open
|
||||
* @throws RateLimitExceededException If rate-limited
|
||||
* @throws SubscriptionNotFoundException If the provided subscriptionId could not be found with
|
||||
* the provider
|
||||
* @throws SubscriptionPaymentRequiredException If the subscription is in a state does not grant the
|
||||
* user an entitlement
|
||||
* @throws SubscriptionReceiptRequestedForOpenPaymentException If a receipt was requested while a payment transaction
|
||||
* was still open
|
||||
*/
|
||||
ReceiptItem getReceiptItem(String subscriptionId)
|
||||
throws SubscriptionException.InvalidArguments, RateLimitExceededException, SubscriptionException.NotFound, SubscriptionException.ChargeFailurePaymentRequired, SubscriptionException.PaymentRequired, SubscriptionException.ReceiptRequestedForOpenPayment;
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException, SubscriptionChargeFailurePaymentRequiredException, SubscriptionPaymentRequiredException, SubscriptionReceiptRequestedForOpenPaymentException;
|
||||
|
||||
/**
|
||||
* Cancel all active subscriptions for this key within the payment processor.
|
||||
@@ -52,22 +43,19 @@ public interface SubscriptionPaymentProcessor {
|
||||
* @param key An identifier for the subscriber within the payment provider, corresponds to the customerId field in the
|
||||
* subscriptions table
|
||||
* @throws RateLimitExceededException If rate-limited
|
||||
* @throws SubscriptionException.NotFound If the provided key was not found with the provider
|
||||
* @throws SubscriptionException.InvalidArguments If a precondition for cancellation was not met
|
||||
* @throws SubscriptionInvalidArgumentsException If a precondition for cancellation was not met
|
||||
*/
|
||||
void cancelAllActiveSubscriptions(String key)
|
||||
throws SubscriptionException.InvalidArguments, RateLimitExceededException, SubscriptionException.NotFound;
|
||||
throws SubscriptionInvalidArgumentsException, RateLimitExceededException;
|
||||
|
||||
/**
|
||||
* Retrieve subscription information from the processor
|
||||
*
|
||||
* @param subscriptionId The identifier with the processor to retrieve information for
|
||||
* @return {@link SubscriptionInformation} from the provider
|
||||
* @throws RateLimitExceededException If rate-limited
|
||||
* @throws SubscriptionException.NotFound If the provided key was not found with the provider
|
||||
* @throws SubscriptionException.InvalidArguments If the subscription exists on the provider but does not represent a
|
||||
* valid subscription
|
||||
* @throws RateLimitExceededException If rate-limited
|
||||
* @throws SubscriptionNotFoundException If the provided key was not found with the provider
|
||||
*/
|
||||
SubscriptionInformation getSubscriptionInformation(final String subscriptionId)
|
||||
throws SubscriptionException.InvalidArguments, RateLimitExceededException, SubscriptionException.NotFound;
|
||||
throws RateLimitExceededException, SubscriptionNotFoundException;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionPaymentRequiredException extends SubscriptionException {
|
||||
|
||||
public SubscriptionPaymentRequiredException() {
|
||||
super(null, null);
|
||||
}
|
||||
|
||||
public SubscriptionPaymentRequiredException(String message) {
|
||||
super(null, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionPaymentRequiresActionException extends SubscriptionInvalidArgumentsException {
|
||||
|
||||
public SubscriptionPaymentRequiresActionException(String message) {
|
||||
super(message, null);
|
||||
}
|
||||
|
||||
public SubscriptionPaymentRequiresActionException() {
|
||||
super(null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionProcessorConflictException extends SubscriptionException {
|
||||
|
||||
public SubscriptionProcessorConflictException() {
|
||||
super(null, null);
|
||||
}
|
||||
|
||||
public SubscriptionProcessorConflictException(final String message) {
|
||||
super(null, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
public class SubscriptionProcessorException extends SubscriptionException {
|
||||
|
||||
private final PaymentProvider processor;
|
||||
private final ChargeFailure chargeFailure;
|
||||
|
||||
public SubscriptionProcessorException(final PaymentProvider processor, final ChargeFailure chargeFailure) {
|
||||
super(null, null);
|
||||
this.processor = processor;
|
||||
this.chargeFailure = chargeFailure;
|
||||
}
|
||||
|
||||
public PaymentProvider getProcessor() {
|
||||
return processor;
|
||||
}
|
||||
|
||||
public ChargeFailure getChargeFailure() {
|
||||
return chargeFailure;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.subscriptions;
|
||||
|
||||
/**
|
||||
* Attempted to retrieve a receipt for a subscription that hasn't yet been charged or the invoice is in the open state
|
||||
*/
|
||||
public class SubscriptionReceiptRequestedForOpenPaymentException extends SubscriptionException {
|
||||
|
||||
public SubscriptionReceiptRequestedForOpenPaymentException() {
|
||||
super(null, null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user