Propagate certain subscription processor errors to client responses
This commit is contained in:
		
							parent
							
								
									2d187abf13
								
							
						
					
					
						commit
						b89e2e5355
					
				|  | @ -140,6 +140,7 @@ import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptio | |||
| import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; | ||||
| import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper; | ||||
| import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper; | ||||
| import org.whispersystems.textsecuregcm.mappers.SubscriptionProcessorExceptionMapper; | ||||
| import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener; | ||||
| import org.whispersystems.textsecuregcm.metrics.MetricsUtil; | ||||
| import org.whispersystems.textsecuregcm.metrics.ReportedMessageMetricsListener; | ||||
|  | @ -858,6 +859,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration | |||
|         new ImpossiblePhoneNumberExceptionMapper(), | ||||
|         new NonNormalizedPhoneNumberExceptionMapper(), | ||||
|         new RegistrationServiceSenderExceptionMapper(), | ||||
|         new SubscriptionProcessorExceptionMapper(), | ||||
|         new JsonMappingExceptionMapper() | ||||
|     ).forEach(exceptionMapper -> { | ||||
|       environment.jersey().register(exceptionMapper); | ||||
|  |  | |||
|  | @ -89,6 +89,7 @@ import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager; | |||
| import org.whispersystems.textsecuregcm.storage.SubscriptionManager; | ||||
| import org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.StripeManager; | ||||
|  | @ -1078,48 +1079,6 @@ public class SubscriptionController { | |||
|       } | ||||
|     } | ||||
| 
 | ||||
|     public static class ChargeFailure { | ||||
|       private final String code; | ||||
|       private final String message; | ||||
|       private final String outcomeNetworkStatus; | ||||
|       private final String outcomeReason; | ||||
|       private final String outcomeType; | ||||
| 
 | ||||
|       @JsonCreator | ||||
|       public ChargeFailure( | ||||
|           @JsonProperty("code") String code, | ||||
|           @JsonProperty("message") String message, | ||||
|           @JsonProperty("outcomeNetworkStatus") String outcomeNetworkStatus, | ||||
|           @JsonProperty("outcomeReason") String outcomeReason, | ||||
|           @JsonProperty("outcomeType") String outcomeType) { | ||||
|         this.code = code; | ||||
|         this.message = message; | ||||
|         this.outcomeNetworkStatus = outcomeNetworkStatus; | ||||
|         this.outcomeReason = outcomeReason; | ||||
|         this.outcomeType = outcomeType; | ||||
|       } | ||||
| 
 | ||||
|       public String getCode() { | ||||
|         return code; | ||||
|       } | ||||
| 
 | ||||
|       public String getMessage() { | ||||
|         return message; | ||||
|       } | ||||
| 
 | ||||
|       public String getOutcomeNetworkStatus() { | ||||
|         return outcomeNetworkStatus; | ||||
|       } | ||||
| 
 | ||||
|       public String getOutcomeReason() { | ||||
|         return outcomeReason; | ||||
|       } | ||||
| 
 | ||||
|       public String getOutcomeType() { | ||||
|         return outcomeType; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     private final Subscription subscription; | ||||
|     private final ChargeFailure chargeFailure; | ||||
| 
 | ||||
|  | @ -1158,17 +1117,7 @@ public class SubscriptionController { | |||
|             final SubscriptionProcessorManager manager = getManagerForProcessor(record.getProcessorCustomer().orElseThrow().processor()); | ||||
| 
 | ||||
|             return manager.getSubscription(record.subscriptionId).thenCompose(subscription -> | ||||
|                     manager.getSubscriptionInformation(subscription).thenApply(subscriptionInformation -> { | ||||
|                         final GetSubscriptionInformationResponse.ChargeFailure chargeFailure = Optional.ofNullable(subscriptionInformation.chargeFailure()) | ||||
|                                 .map(chargeFailure1 -> new GetSubscriptionInformationResponse.ChargeFailure( | ||||
|                                         subscriptionInformation.chargeFailure().code(), | ||||
|                                         subscriptionInformation.chargeFailure().message(), | ||||
|                                         subscriptionInformation.chargeFailure().outcomeNetworkStatus(), | ||||
|                                         subscriptionInformation.chargeFailure().outcomeReason(), | ||||
|                                         subscriptionInformation.chargeFailure().outcomeType() | ||||
|                                 )) | ||||
|                                 .orElse(null); | ||||
|                         return Response.ok( | ||||
|                 manager.getSubscriptionInformation(subscription).thenApply(subscriptionInformation -> Response.ok( | ||||
|                     new GetSubscriptionInformationResponse( | ||||
|                         new GetSubscriptionInformationResponse.Subscription( | ||||
|                             subscriptionInformation.level(), | ||||
|  | @ -1180,9 +1129,8 @@ public class SubscriptionController { | |||
|                             subscriptionInformation.price().amount(), | ||||
|                             subscriptionInformation.status().getApiValue(), | ||||
|                             manager.getProcessor()), | ||||
|                                     chargeFailure | ||||
|                                 )).build(); | ||||
|                     })); | ||||
|                         subscriptionInformation.chargeFailure() | ||||
|                     )).build())); | ||||
|         }); | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,26 @@ | |||
| /* | ||||
|  * Copyright 2023 Signal Messenger, LLC | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| package org.whispersystems.textsecuregcm.mappers; | ||||
| 
 | ||||
| import java.util.Map; | ||||
| import javax.ws.rs.core.Response; | ||||
| import javax.ws.rs.ext.ExceptionMapper; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorException; | ||||
| 
 | ||||
| public class SubscriptionProcessorExceptionMapper implements ExceptionMapper<SubscriptionProcessorException> { | ||||
| 
 | ||||
|   public static final int EXTERNAL_SERVICE_ERROR_STATUS_CODE = 440; | ||||
| 
 | ||||
|   @Override | ||||
|   public Response toResponse(final SubscriptionProcessorException exception) { | ||||
|     return Response.status(EXTERNAL_SERVICE_ERROR_STATUS_CODE) | ||||
|         .entity(Map.of( | ||||
|             "processor", exception.getProcessor().name(), | ||||
|             "chargeFailure", exception.getChargeFailure() | ||||
|         )) | ||||
|         .build(); | ||||
|   } | ||||
| } | ||||
|  | @ -49,6 +49,8 @@ public class BraintreeManager implements SubscriptionProcessorManager { | |||
| 
 | ||||
|   private static final Logger logger = LoggerFactory.getLogger(BraintreeManager.class); | ||||
| 
 | ||||
|   private static final String GENERIC_DECLINED_PROCESSOR_CODE = "2046"; | ||||
|   private static final String PAYPAL_FUNDING_INSTRUMENT_DECLINED_PROCESSOR_CODE = "2074"; | ||||
|   private static final String PAYPAL_PAYMENT_ALREADY_COMPLETED_PROCESSOR_CODE = "2094"; | ||||
|   private final BraintreeGateway braintreeGateway; | ||||
|   private final BraintreeGraphqlClient braintreeGraphqlClient; | ||||
|  | @ -184,11 +186,18 @@ public class BraintreeManager implements SubscriptionProcessorManager { | |||
|                     new PayPalChargeSuccessDetails(successfulTx.getGraphQLId())); | ||||
|               } | ||||
| 
 | ||||
|               return switch (unsuccessfulTx.getProcessorResponseCode()) { | ||||
|                 case GENERIC_DECLINED_PROCESSOR_CODE, PAYPAL_FUNDING_INSTRUMENT_DECLINED_PROCESSOR_CODE -> | ||||
|                     CompletableFuture.failedFuture( | ||||
|                         new SubscriptionProcessorException(getProcessor(), createChargeFailure(unsuccessfulTx))); | ||||
| 
 | ||||
|                 default -> { | ||||
|                   logger.info("PayPal charge unexpectedly failed: {}", unsuccessfulTx.getProcessorResponseCode()); | ||||
| 
 | ||||
|               return CompletableFuture.failedFuture( | ||||
|                   yield CompletableFuture.failedFuture( | ||||
|                       new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR)); | ||||
| 
 | ||||
|                 } | ||||
|               }; | ||||
|             }, executor)); | ||||
|   } | ||||
| 
 | ||||
|  | @ -240,12 +249,6 @@ public class BraintreeManager implements SubscriptionProcessorManager { | |||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   private void assertResultSuccess(Result<?> result) throws CompletionException { | ||||
|     if (!result.isSuccess()) { | ||||
|       throw new CompletionException(new BraintreeException(result.getMessage())); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @Override | ||||
|   public CompletableFuture<ProcessorCustomer> createCustomer(final byte[] subscriberUser) { | ||||
|     return CompletableFuture.supplyAsync(() -> { | ||||
|  | @ -258,7 +261,9 @@ public class BraintreeManager implements SubscriptionProcessorManager { | |||
|           } | ||||
|         }, executor) | ||||
|         .thenApply(result -> { | ||||
|           assertResultSuccess(result); | ||||
|           if (!result.isSuccess()) { | ||||
|             throw new CompletionException(new BraintreeException(result.getMessage())); | ||||
|           } | ||||
| 
 | ||||
|           return new ProcessorCustomer(result.getTarget().getId(), SubscriptionProcessor.BRAINTREE); | ||||
|         }); | ||||
|  | @ -336,7 +341,19 @@ public class BraintreeManager implements SubscriptionProcessorManager { | |||
|                     .done() | ||||
|                 ); | ||||
| 
 | ||||
|                 assertResultSuccess(result); | ||||
|                 if (!result.isSuccess()) { | ||||
|                   final CompletionException completionException; | ||||
|                   if (result.getTarget() != null) { | ||||
|                     completionException = result.getTarget().getTransactions().stream().findFirst() | ||||
|                         .map(transaction -> new CompletionException( | ||||
|                             new SubscriptionProcessorException(getProcessor(), createChargeFailure(transaction)))) | ||||
|                         .orElseGet(() -> new CompletionException(new BraintreeException(result.getMessage()))); | ||||
|                   } else { | ||||
|                     completionException = new CompletionException(new BraintreeException(result.getMessage())); | ||||
|                   } | ||||
| 
 | ||||
|                   throw completionException; | ||||
|                 } | ||||
| 
 | ||||
|                 return result.getTarget(); | ||||
|               })); | ||||
|  | @ -358,7 +375,7 @@ public class BraintreeManager implements SubscriptionProcessorManager { | |||
|     } | ||||
| 
 | ||||
|     // since badge redemption is untrackable by design and unrevokable, subscription changes must be immediate and | ||||
|     // and not prorated. Braintree subscriptions cannot change their next billing date, | ||||
|     // not prorated. Braintree subscriptions cannot change their next billing date, | ||||
|     // so we must end the existing one and create a new one | ||||
|     return cancelSubscriptionAtEndOfCurrentPeriod(subscription) | ||||
|         .thenCompose(ignored -> { | ||||
|  | @ -413,13 +430,28 @@ public class BraintreeManager implements SubscriptionProcessorManager { | |||
|       final Instant anchor = subscription.getFirstBillingDate().toInstant(); | ||||
|       final Instant endOfCurrentPeriod = subscription.getBillingPeriodEndDate().toInstant(); | ||||
| 
 | ||||
|       final Optional<Transaction> maybeTransaction = getLatestTransactionForSubscription(subscription); | ||||
| 
 | ||||
|       final ChargeFailure chargeFailure = maybeTransaction.map(transaction -> { | ||||
| 
 | ||||
|       final ChargeFailure chargeFailure = getLatestTransactionForSubscription(subscription).map(transaction -> { | ||||
|         if (getPaymentStatus(transaction.getStatus()).equals(PaymentStatus.SUCCEEDED)) { | ||||
|           return null; | ||||
|         } | ||||
|         return createChargeFailure(transaction); | ||||
|       }).orElse(null); | ||||
| 
 | ||||
|       return new SubscriptionInformation( | ||||
|           new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT), | ||||
|               SubscriptionCurrencyUtil.convertBraintreeAmountToApiAmount(plan.getCurrencyIsoCode(), plan.getPrice())), | ||||
|           level, | ||||
|           anchor, | ||||
|           endOfCurrentPeriod, | ||||
|           Subscription.Status.ACTIVE == subscription.getStatus(), | ||||
|           !subscription.neverExpires(), | ||||
|           getSubscriptionStatus(subscription.getStatus()), | ||||
|           chargeFailure | ||||
|       ); | ||||
|     }, executor); | ||||
|   } | ||||
| 
 | ||||
|   private ChargeFailure createChargeFailure(Transaction transaction) { | ||||
| 
 | ||||
|     final String code; | ||||
|     final String message; | ||||
|  | @ -440,22 +472,6 @@ public class BraintreeManager implements SubscriptionProcessorManager { | |||
|         null, | ||||
|         null, | ||||
|         null); | ||||
| 
 | ||||
|       }).orElse(null); | ||||
| 
 | ||||
| 
 | ||||
|       return new SubscriptionInformation( | ||||
|           new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT), | ||||
|               SubscriptionCurrencyUtil.convertBraintreeAmountToApiAmount(plan.getCurrencyIsoCode(), plan.getPrice())), | ||||
|           level, | ||||
|           anchor, | ||||
|           endOfCurrentPeriod, | ||||
|           Subscription.Status.ACTIVE == subscription.getStatus(), | ||||
|           !subscription.neverExpires(), | ||||
|           getSubscriptionStatus(subscription.getStatus()), | ||||
|           chargeFailure | ||||
|       ); | ||||
|     }, executor); | ||||
|   } | ||||
| 
 | ||||
|   @Override | ||||
|  |  | |||
|  | @ -0,0 +1,13 @@ | |||
| /* | ||||
|  * Copyright 2023 Signal Messenger, LLC | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| package org.whispersystems.textsecuregcm.subscriptions; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| 
 | ||||
| public record ChargeFailure(String code, String message, @Nullable String outcomeNetworkStatus, | ||||
|                             @Nullable String outcomeReason, @Nullable String outcomeType) { | ||||
| 
 | ||||
| } | ||||
|  | @ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.subscriptions; | |||
| import com.google.common.base.Strings; | ||||
| import com.google.common.collect.Lists; | ||||
| import com.stripe.StripeClient; | ||||
| import com.stripe.exception.CardException; | ||||
| import com.stripe.exception.StripeException; | ||||
| import com.stripe.model.Charge; | ||||
| import com.stripe.model.Customer; | ||||
|  | @ -268,6 +269,18 @@ public class StripeManager implements SubscriptionProcessorManager { | |||
|                 .create(params, commonOptions(generateIdempotencyKeyForCreateSubscription( | ||||
|                 customerId, lastSubscriptionCreatedAt))); | ||||
|           } catch (StripeException e) { | ||||
| 
 | ||||
|             if (e instanceof CardException ce) { | ||||
|               throw new CompletionException(new SubscriptionProcessorException(getProcessor(), | ||||
|                   new ChargeFailure( | ||||
|                       StringUtils.defaultIfBlank(ce.getDeclineCode(), ce.getCode()), | ||||
|                       e.getStripeError().getMessage(), | ||||
|                       null, | ||||
|                       null, | ||||
|                       null | ||||
|                   ))); | ||||
|             } | ||||
| 
 | ||||
|             throw new CompletionException(e); | ||||
|           } | ||||
|         }, executor) | ||||
|  |  | |||
|  | @ -0,0 +1,26 @@ | |||
| /* | ||||
|  * Copyright 2023 Signal Messenger, LLC | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| package org.whispersystems.textsecuregcm.subscriptions; | ||||
| 
 | ||||
| public class SubscriptionProcessorException extends Exception { | ||||
| 
 | ||||
|   private final SubscriptionProcessor processor; | ||||
|   private final ChargeFailure chargeFailure; | ||||
| 
 | ||||
|   public SubscriptionProcessorException(final SubscriptionProcessor processor, | ||||
|       final ChargeFailure chargeFailure) { | ||||
|     this.processor = processor; | ||||
|     this.chargeFailure = chargeFailure; | ||||
|   } | ||||
| 
 | ||||
|   public SubscriptionProcessor getProcessor() { | ||||
|     return processor; | ||||
|   } | ||||
| 
 | ||||
|   public ChargeFailure getChargeFailure() { | ||||
|     return chargeFailure; | ||||
|   } | ||||
| } | ||||
|  | @ -155,11 +155,6 @@ public interface SubscriptionProcessorManager { | |||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   record ChargeFailure(String code, String message, @Nullable String outcomeNetworkStatus, | ||||
|                        @Nullable String outcomeReason, @Nullable String outcomeType) { | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   record ReceiptItem(String itemId, Instant expiration, long level) { | ||||
| 
 | ||||
|   } | ||||
|  |  | |||
|  | @ -42,6 +42,7 @@ import java.util.function.Predicate; | |||
| import java.util.stream.Stream; | ||||
| import javax.ws.rs.client.Entity; | ||||
| import javax.ws.rs.core.Response; | ||||
| import org.assertj.core.api.InstanceOfAssertFactories; | ||||
| import org.glassfish.jersey.server.ServerProperties; | ||||
| import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
|  | @ -63,12 +64,15 @@ import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetSu | |||
| import org.whispersystems.textsecuregcm.entities.Badge; | ||||
| import org.whispersystems.textsecuregcm.entities.BadgeSvg; | ||||
| import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper; | ||||
| import org.whispersystems.textsecuregcm.mappers.SubscriptionProcessorExceptionMapper; | ||||
| import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager; | ||||
| import org.whispersystems.textsecuregcm.storage.SubscriptionManager; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.StripeManager; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorException; | ||||
| import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorManager; | ||||
| import org.whispersystems.textsecuregcm.tests.util.AuthHelper; | ||||
| import org.whispersystems.textsecuregcm.util.SystemMapper; | ||||
|  | @ -112,6 +116,7 @@ class SubscriptionControllerTest { | |||
|       .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) | ||||
|       .addProvider(AuthHelper.getAuthFilter()) | ||||
|       .addProvider(CompletionExceptionMapper.class) | ||||
|       .addProvider(SubscriptionProcessorExceptionMapper.class) | ||||
|       .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(Set.of( | ||||
|           AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) | ||||
|       .setMapper(SystemMapper.jsonMapper()) | ||||
|  | @ -189,6 +194,32 @@ class SubscriptionControllerTest { | |||
|     assertThat(response.getStatus()).isEqualTo(422); | ||||
|   } | ||||
| 
 | ||||
|   @Test | ||||
|   void confirmPaypalBoostProcessorError() { | ||||
| 
 | ||||
|     when(BRAINTREE_MANAGER.captureOneTimePayment(anyString(), anyString(), anyString(), anyString(), anyLong(), | ||||
|         anyLong())) | ||||
|         .thenReturn(CompletableFuture.failedFuture(new SubscriptionProcessorException(SubscriptionProcessor.BRAINTREE, | ||||
|             new ChargeFailure("2046", "Declined", null, null, null)))); | ||||
| 
 | ||||
|     final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/paypal/confirm") | ||||
|         .request() | ||||
|         .post(Entity.json(Map.of("payerId", "payer123", | ||||
|             "paymentId", "PAYID-456", | ||||
|             "paymentToken", "EC-789", | ||||
|             "currency", "usd", | ||||
|             "amount", 123))); | ||||
| 
 | ||||
|     assertThat(response.getStatus()).isEqualTo(SubscriptionProcessorExceptionMapper.EXTERNAL_SERVICE_ERROR_STATUS_CODE); | ||||
| 
 | ||||
|     final Map responseMap = response.readEntity(Map.class); | ||||
|     assertThat(responseMap.get("processor")).isEqualTo("BRAINTREE"); | ||||
|     assertThat(responseMap.get("chargeFailure")).asInstanceOf( | ||||
|             InstanceOfAssertFactories.map(String.class, Object.class)) | ||||
|         .extracting("code") | ||||
|         .isEqualTo("2046"); | ||||
|   } | ||||
| 
 | ||||
|   @Test | ||||
|   void createBoostReceiptNoRequest() { | ||||
|     final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/receipt_credentials") | ||||
|  | @ -230,7 +261,7 @@ class SubscriptionControllerTest { | |||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void success() { | ||||
|     void createSubscriptionSuccess() { | ||||
|       when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong())) | ||||
|           .thenReturn(CompletableFuture.completedFuture(mock(SubscriptionProcessorManager.SubscriptionId.class))); | ||||
| 
 | ||||
|  | @ -244,6 +275,30 @@ class SubscriptionControllerTest { | |||
|       assertThat(response.getStatus()).isEqualTo(200); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void createSubscriptionProcessorDeclined() { | ||||
|       when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong())) | ||||
|           .thenReturn(CompletableFuture.failedFuture(new SubscriptionProcessorException(SubscriptionProcessor.STRIPE, | ||||
|               new ChargeFailure("card_declined", "Insufficient funds", null, null, null)))); | ||||
| 
 | ||||
|       final String level = String.valueOf(levelId); | ||||
|       final String idempotencyKey = UUID.randomUUID().toString(); | ||||
|       final Response response = RESOURCE_EXTENSION.target( | ||||
|               String.format("/v1/subscription/%s/level/%s/%s/%s", subscriberId, level, currency, idempotencyKey)) | ||||
|           .request() | ||||
|           .put(Entity.json("")); | ||||
| 
 | ||||
|       assertThat(response.getStatus()).isEqualTo( | ||||
|           SubscriptionProcessorExceptionMapper.EXTERNAL_SERVICE_ERROR_STATUS_CODE); | ||||
| 
 | ||||
|       final Map responseMap = response.readEntity(Map.class); | ||||
|       assertThat(responseMap.get("processor")).isEqualTo("STRIPE"); | ||||
|       assertThat(responseMap.get("chargeFailure")).asInstanceOf( | ||||
|               InstanceOfAssertFactories.map(String.class, Object.class)) | ||||
|           .extracting("code") | ||||
|           .isEqualTo("card_declined"); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void missingCustomerId() { | ||||
|       final byte[] subscriberUserAndKey = new byte[32]; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Chris Eager
						Chris Eager