Add charge failure details to `/v1/subscription/boost/receipt_credential` 402 response
This commit is contained in:
		
							parent
							
								
									bc35278684
								
							
						
					
					
						commit
						5990a100db
					
				|  | @ -802,9 +802,11 @@ public class SubscriptionController { | ||||||
|     public SubscriptionProcessor processor = SubscriptionProcessor.STRIPE; |     public SubscriptionProcessor processor = SubscriptionProcessor.STRIPE; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public record CreateBoostReceiptCredentialsResponse(byte[] receiptCredentialResponse) { |   public record CreateBoostReceiptCredentialsSuccessResponse(byte[] receiptCredentialResponse) { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public record CreateBoostReceiptCredentialsErrorResponse(@JsonInclude(Include.NON_NULL) ChargeFailure chargeFailure) {} | ||||||
|  | 
 | ||||||
|   @POST |   @POST | ||||||
|   @Path("/boost/receipt_credentials") |   @Path("/boost/receipt_credentials") | ||||||
|   @Consumes(MediaType.APPLICATION_JSON) |   @Consumes(MediaType.APPLICATION_JSON) | ||||||
|  | @ -824,7 +826,8 @@ public class SubscriptionController { | ||||||
|             case PROCESSING -> throw new WebApplicationException(Status.NO_CONTENT); |             case PROCESSING -> throw new WebApplicationException(Status.NO_CONTENT); | ||||||
|             case SUCCEEDED -> { |             case SUCCEEDED -> { | ||||||
|             } |             } | ||||||
|             default -> throw new WebApplicationException(Status.PAYMENT_REQUIRED); |             default -> throw new WebApplicationException(Response.status(Status.PAYMENT_REQUIRED) | ||||||
|  |                 .entity(new CreateBoostReceiptCredentialsErrorResponse(paymentDetails.chargeFailure())).build()); | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           long level = oneTimeDonationConfiguration.boost().level(); |           long level = oneTimeDonationConfiguration.boost().level(); | ||||||
|  | @ -875,7 +878,7 @@ public class SubscriptionController { | ||||||
|                             Tag.of(TYPE_TAG_NAME, "boost"), |                             Tag.of(TYPE_TAG_NAME, "boost"), | ||||||
|                             UserAgentTagUtil.getPlatformTag(userAgent))) |                             UserAgentTagUtil.getPlatformTag(userAgent))) | ||||||
|                     .increment(); |                     .increment(); | ||||||
|                 return Response.ok(new CreateBoostReceiptCredentialsResponse(receiptCredentialResponse.serialize())) |                 return Response.ok(new CreateBoostReceiptCredentialsSuccessResponse(receiptCredentialResponse.serialize())) | ||||||
|                     .build(); |                     .build(); | ||||||
|               }); |               }); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  | @ -117,11 +117,15 @@ public class BraintreeManager implements SubscriptionProcessorManager { | ||||||
|     return CompletableFuture.supplyAsync(() -> { |     return CompletableFuture.supplyAsync(() -> { | ||||||
|       try { |       try { | ||||||
|         final Transaction transaction = braintreeGateway.transaction().find(paymentId); |         final Transaction transaction = braintreeGateway.transaction().find(paymentId); | ||||||
| 
 |         ChargeFailure chargeFailure = null; | ||||||
|  |         if (!getPaymentStatus(transaction.getStatus()).equals(PaymentStatus.SUCCEEDED)) { | ||||||
|  |           chargeFailure = createChargeFailure(transaction); | ||||||
|  |         } | ||||||
|         return new PaymentDetails(transaction.getGraphQLId(), |         return new PaymentDetails(transaction.getGraphQLId(), | ||||||
|             transaction.getCustomFields(), |             transaction.getCustomFields(), | ||||||
|             getPaymentStatus(transaction.getStatus()), |             getPaymentStatus(transaction.getStatus()), | ||||||
|             transaction.getCreatedAt().toInstant()); |             transaction.getCreatedAt().toInstant(), | ||||||
|  |             chargeFailure); | ||||||
| 
 | 
 | ||||||
|       } catch (final NotFoundException e) { |       } catch (final NotFoundException e) { | ||||||
|         return null; |         return null; | ||||||
|  | @ -433,7 +437,7 @@ public class BraintreeManager implements SubscriptionProcessorManager { | ||||||
| 
 | 
 | ||||||
|       if (latestTransaction.isPresent()){ |       if (latestTransaction.isPresent()){ | ||||||
|         paymentProcessing = isPaymentProcessing(latestTransaction.get().getStatus()); |         paymentProcessing = isPaymentProcessing(latestTransaction.get().getStatus()); | ||||||
|         if (!getPaymentStatus(latestTransaction.get().getStatus()).equals(PaymentStatus.SUCCEEDED)) { |         if (getPaymentStatus(latestTransaction.get().getStatus()) != PaymentStatus.SUCCEEDED) { | ||||||
|           chargeFailure = createChargeFailure(latestTransaction.get()); |           chargeFailure = createChargeFailure(latestTransaction.get()); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  | @ -470,7 +474,10 @@ public class BraintreeManager implements SubscriptionProcessorManager { | ||||||
| 
 | 
 | ||||||
|     final String code; |     final String code; | ||||||
|     final String message; |     final String message; | ||||||
|     if (transaction.getProcessorResponseCode() != null) { |     if (transaction.getStatus() == Transaction.Status.VOIDED) { | ||||||
|  |       code = "voided"; | ||||||
|  |       message = "voided"; | ||||||
|  |     } else if (transaction.getProcessorResponseCode() != null) { | ||||||
|       code = transaction.getProcessorResponseCode(); |       code = transaction.getProcessorResponseCode(); | ||||||
|       message = transaction.getProcessorResponseText(); |       message = transaction.getProcessorResponseText(); | ||||||
|     } else if (transaction.getGatewayRejectionReason() != null) { |     } else if (transaction.getGatewayRejectionReason() != null) { | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ import com.stripe.param.CustomerUpdateParams; | ||||||
| import com.stripe.param.CustomerUpdateParams.InvoiceSettings; | import com.stripe.param.CustomerUpdateParams.InvoiceSettings; | ||||||
| import com.stripe.param.InvoiceListParams; | import com.stripe.param.InvoiceListParams; | ||||||
| import com.stripe.param.PaymentIntentCreateParams; | import com.stripe.param.PaymentIntentCreateParams; | ||||||
|  | import com.stripe.param.PaymentIntentRetrieveParams; | ||||||
| import com.stripe.param.PriceRetrieveParams; | import com.stripe.param.PriceRetrieveParams; | ||||||
| import com.stripe.param.SetupIntentCreateParams; | import com.stripe.param.SetupIntentCreateParams; | ||||||
| import com.stripe.param.SubscriptionCancelParams; | import com.stripe.param.SubscriptionCancelParams; | ||||||
|  | @ -216,12 +217,23 @@ public class StripeManager implements SubscriptionProcessorManager { | ||||||
|   public CompletableFuture<PaymentDetails> getPaymentDetails(String paymentIntentId) { |   public CompletableFuture<PaymentDetails> getPaymentDetails(String paymentIntentId) { | ||||||
|     return CompletableFuture.supplyAsync(() -> { |     return CompletableFuture.supplyAsync(() -> { | ||||||
|       try { |       try { | ||||||
|         final PaymentIntent paymentIntent = stripeClient.paymentIntents().retrieve(paymentIntentId, commonOptions()); |         final PaymentIntentRetrieveParams params = PaymentIntentRetrieveParams.builder() | ||||||
|  |             .addExpand("latest_charge").build(); | ||||||
|  |         final PaymentIntent paymentIntent = stripeClient.paymentIntents().retrieve(paymentIntentId, params, commonOptions()); | ||||||
|  | 
 | ||||||
|  |         ChargeFailure chargeFailure = null; | ||||||
|  |         if (paymentIntent.getLatestChargeObject() != null) { | ||||||
|  |           final Charge charge = paymentIntent.getLatestChargeObject(); | ||||||
|  |           if (charge.getFailureCode() != null || charge.getFailureMessage() != null) { | ||||||
|  |             chargeFailure = createChargeFailure(charge); | ||||||
|  |           } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         return new PaymentDetails(paymentIntent.getId(), |         return new PaymentDetails(paymentIntent.getId(), | ||||||
|             paymentIntent.getMetadata() == null ? Collections.emptyMap() : paymentIntent.getMetadata(), |             paymentIntent.getMetadata() == null ? Collections.emptyMap() : paymentIntent.getMetadata(), | ||||||
|             getPaymentStatusForStatus(paymentIntent.getStatus()), |             getPaymentStatusForStatus(paymentIntent.getStatus()), | ||||||
|             Instant.ofEpochSecond(paymentIntent.getCreated())); |             Instant.ofEpochSecond(paymentIntent.getCreated()), | ||||||
|  |             chargeFailure); | ||||||
|       } catch (StripeException e) { |       } catch (StripeException e) { | ||||||
|         if (e.getStatusCode() == 404) { |         if (e.getStatusCode() == 404) { | ||||||
|           return null; |           return null; | ||||||
|  | @ -479,6 +491,16 @@ public class StripeManager implements SubscriptionProcessorManager { | ||||||
|     }, executor); |     }, executor); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private static ChargeFailure createChargeFailure(final Charge charge) { | ||||||
|  |     Charge.Outcome outcome = charge.getOutcome(); | ||||||
|  |     return new ChargeFailure( | ||||||
|  |         charge.getFailureCode(), | ||||||
|  |         charge.getFailureMessage(), | ||||||
|  |         outcome != null ? outcome.getNetworkStatus() : null, | ||||||
|  |         outcome != null ? outcome.getReason() : null, | ||||||
|  |         outcome != null ? outcome.getType() : null); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   @Override |   @Override | ||||||
|   public CompletableFuture<SubscriptionInformation> getSubscriptionInformation(Object subscriptionObj) { |   public CompletableFuture<SubscriptionInformation> getSubscriptionInformation(Object subscriptionObj) { | ||||||
| 
 | 
 | ||||||
|  | @ -497,13 +519,7 @@ public class StripeManager implements SubscriptionProcessorManager { | ||||||
|                 if (invoice.getChargeObject() != null) { |                 if (invoice.getChargeObject() != null) { | ||||||
|                   final Charge charge = invoice.getChargeObject(); |                   final Charge charge = invoice.getChargeObject(); | ||||||
|                   if (charge.getFailureCode() != null || charge.getFailureMessage() != null) { |                   if (charge.getFailureCode() != null || charge.getFailureMessage() != null) { | ||||||
|                     Charge.Outcome outcome = charge.getOutcome(); |                     chargeFailure = createChargeFailure(charge); | ||||||
|                     chargeFailure = new ChargeFailure( |  | ||||||
|                         charge.getFailureCode(), |  | ||||||
|                         charge.getFailureMessage(), |  | ||||||
|                         outcome != null ? outcome.getNetworkStatus() : null, |  | ||||||
|                         outcome != null ? outcome.getReason() : null, |  | ||||||
|                         outcome != null ? outcome.getType() : null); |  | ||||||
|                   } |                   } | ||||||
| 
 | 
 | ||||||
|                   if (charge.getPaymentMethodDetails() != null |                   if (charge.getPaymentMethodDetails() != null | ||||||
|  |  | ||||||
|  | @ -60,7 +60,8 @@ public interface SubscriptionProcessorManager { | ||||||
|   record PaymentDetails(String id, |   record PaymentDetails(String id, | ||||||
|                         Map<String, String> customMetadata, |                         Map<String, String> customMetadata, | ||||||
|                         PaymentStatus status, |                         PaymentStatus status, | ||||||
|                         Instant created) { |                         Instant created, | ||||||
|  |                         @Nullable ChargeFailure chargeFailure) { | ||||||
| 
 | 
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -31,6 +31,7 @@ import java.time.Clock; | ||||||
| import java.time.Instant; | import java.time.Instant; | ||||||
| import java.util.Arrays; | import java.util.Arrays; | ||||||
| import java.util.Base64; | import java.util.Base64; | ||||||
|  | import java.util.Collections; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | @ -267,6 +268,47 @@ class SubscriptionControllerTest { | ||||||
|     assertThat(response.getStatus()).isEqualTo(422); |     assertThat(response.getStatus()).isEqualTo(422); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   @ParameterizedTest | ||||||
|  |   @MethodSource | ||||||
|  |   void createBoostReceiptPaymentRequired(final ChargeFailure chargeFailure, boolean expectChargeFailure) { | ||||||
|  |     when(STRIPE_MANAGER.getPaymentDetails(any())).thenReturn(CompletableFuture.completedFuture(new SubscriptionProcessorManager.PaymentDetails( | ||||||
|  |         "id", | ||||||
|  |         Collections.emptyMap(), | ||||||
|  |         SubscriptionProcessorManager.PaymentStatus.FAILED, | ||||||
|  |         Instant.now(), | ||||||
|  |         chargeFailure) | ||||||
|  |     )); | ||||||
|  |     Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/receipt_credentials") | ||||||
|  |         .request() | ||||||
|  |         .post(Entity.json(""" | ||||||
|  |             { | ||||||
|  |               "paymentIntentId": "foo", | ||||||
|  |               "receiptCredentialRequest": "abcd", | ||||||
|  |               "processor": "STRIPE" | ||||||
|  |             } | ||||||
|  |           """)); | ||||||
|  |     assertThat(response.getStatus()).isEqualTo(402); | ||||||
|  | 
 | ||||||
|  |     if (expectChargeFailure) { | ||||||
|  |       assertThat(response.readEntity(SubscriptionController.CreateBoostReceiptCredentialsErrorResponse.class).chargeFailure()).isEqualTo(chargeFailure); | ||||||
|  |     } else { | ||||||
|  |       assertThat(response.readEntity(String.class)).isEqualTo("{}"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static Stream<Arguments> createBoostReceiptPaymentRequired() { | ||||||
|  |     return Stream.of( | ||||||
|  |         Arguments.of(new ChargeFailure( | ||||||
|  |             "generic_decline", | ||||||
|  |             "some failure message", | ||||||
|  |             null, | ||||||
|  |             null, | ||||||
|  |             null | ||||||
|  |         ), true), | ||||||
|  |         Arguments.of(null, false) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   @Test |   @Test | ||||||
|   void confirmPaypalBoostProcessorError() { |   void confirmPaypalBoostProcessorError() { | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 Katherine
						Katherine