Add charge failure details to `/v1/subscription/boost/receipt_credential` 402 response

This commit is contained in:
Katherine 2023-10-19 10:21:26 -07:00 committed by GitHub
parent bc35278684
commit 5990a100db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 86 additions and 17 deletions

View File

@ -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();
}); });
}); });

View File

@ -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) {

View File

@ -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

View File

@ -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) {
} }

View File

@ -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() {