diff --git a/src/main/java/com/stripe/mpp/methods/stripe/StripeApi.java b/src/main/java/com/stripe/mpp/methods/stripe/StripeApi.java index 7576ae4..4346519 100644 --- a/src/main/java/com/stripe/mpp/methods/stripe/StripeApi.java +++ b/src/main/java/com/stripe/mpp/methods/stripe/StripeApi.java @@ -3,6 +3,7 @@ import com.stripe.StripeClient; import com.stripe.exception.StripeException; import com.stripe.model.PaymentIntent; +import com.stripe.net.RequestOptions; import com.stripe.param.PaymentIntentCreateParams; import com.stripe.mpp.error.VerificationFailedException; @@ -54,7 +55,8 @@ Result createAndConfirm( String currency, String spt, List paymentMethodTypes, - Map metadata + Map metadata, + String challengeId ) { try { StripeClient client = new StripeClient(secretKey); @@ -70,11 +72,19 @@ Result createAndConfirm( builder.putAllMetadata(metadata); } - PaymentIntent pi = client.paymentIntents().create(builder.build()); + RequestOptions options = RequestOptions.builder() + .setIdempotencyKey(buildIdempotencyKey(challengeId, spt)) + .build(); + + PaymentIntent pi = client.paymentIntents().create(builder.build(), options); return new Result(pi.getId(), pi.getStatus()); } catch (StripeException e) { throw new VerificationFailedException(e.getMessage()); } } + + static String buildIdempotencyKey(String challengeId, String spt) { + return "mpp-java_" + challengeId + "_" + spt; + } } diff --git a/src/main/java/com/stripe/mpp/methods/stripe/StripeChargeIntent.java b/src/main/java/com/stripe/mpp/methods/stripe/StripeChargeIntent.java index ce482fb..d51f00a 100644 --- a/src/main/java/com/stripe/mpp/methods/stripe/StripeChargeIntent.java +++ b/src/main/java/com/stripe/mpp/methods/stripe/StripeChargeIntent.java @@ -99,8 +99,9 @@ public Receipt verify(Credential credential, Map request) { Map methodDetails = methodDetails(request); List paymentMethodTypes = paymentMethodTypes(methodDetails); Map metadata = extractMetadata(methodDetails); + String challengeId = credential.challenge().id(); StripeApi.Result result = stripeApi.createAndConfirm( - secretKey, amountMinorUnits, currency, spt, paymentMethodTypes, metadata); + secretKey, amountMinorUnits, currency, spt, paymentMethodTypes, metadata, challengeId); if ("requires_action".equals(result.status())) { throw new com.stripe.mpp.error.PaymentActionRequiredException( diff --git a/src/test/java/com/stripe/mpp/methods/stripe/StripeChargeIntentTest.java b/src/test/java/com/stripe/mpp/methods/stripe/StripeChargeIntentTest.java index 8ea4897..d5af5b2 100644 --- a/src/test/java/com/stripe/mpp/methods/stripe/StripeChargeIntentTest.java +++ b/src/test/java/com/stripe/mpp/methods/stripe/StripeChargeIntentTest.java @@ -58,6 +58,7 @@ static class StubStripeApi extends StripeApi { String lastCurrency; List lastPaymentMethodTypes; Map lastMetadata; + String lastChallengeId; StubStripeApi(StripeApi.Result result) { this.result = result; @@ -66,13 +67,15 @@ static class StubStripeApi extends StripeApi { @Override StripeApi.Result createAndConfirm( String secretKey, long amountMinorUnits, String currency, - String spt, List paymentMethodTypes, Map metadata + String spt, List paymentMethodTypes, Map metadata, + String challengeId ) { this.lastSpt = spt; this.lastAmount = amountMinorUnits; this.lastCurrency = currency; this.lastPaymentMethodTypes = paymentMethodTypes; this.lastMetadata = metadata; + this.lastChallengeId = challengeId; return result; } } @@ -188,7 +191,8 @@ void stripeApiExceptionBecomesVerificationFailed() { @Override StripeApi.Result createAndConfirm( String secretKey, long amountMinorUnits, String currency, - String spt, List paymentMethodTypes, Map metadata + String spt, List paymentMethodTypes, Map metadata, + String challengeId ) { throw new VerificationFailedException("card_declined"); } @@ -208,6 +212,41 @@ void metadataPassedToStripeApi() { assertThat(api.lastMetadata).containsEntry("orderId", "order-42"); } + @Test + void challengeIdPassedForIdempotency() { + StubStripeApi api = new StubStripeApi(new StripeApi.Result("pi_abc123", "succeeded")); + intent(api).verify(credential("spt_xxx"), REQUEST); + + assertThat(api.lastChallengeId).isEqualTo("chal-id"); + } + + @Test + void idempotencyKeyDerivedFromChallengeIdAndSpt() { + String key = StripeApi.buildIdempotencyKey("chal-id", "spt_xxx"); + assertThat(key).isEqualTo("mpp-java_chal-id_spt_xxx"); + } + + @Test + void idempotencyKeyDeterministic() { + String key1 = StripeApi.buildIdempotencyKey("chal-abc", "spt_123"); + String key2 = StripeApi.buildIdempotencyKey("chal-abc", "spt_123"); + assertThat(key1).isEqualTo(key2); + } + + @Test + void idempotencyKeyDiffersForDifferentChallenges() { + String key1 = StripeApi.buildIdempotencyKey("chal-abc", "spt_123"); + String key2 = StripeApi.buildIdempotencyKey("chal-def", "spt_123"); + assertThat(key1).isNotEqualTo(key2); + } + + @Test + void idempotencyKeyDiffersForDifferentSpts() { + String key1 = StripeApi.buildIdempotencyKey("chal-abc", "spt_123"); + String key2 = StripeApi.buildIdempotencyKey("chal-abc", "spt_456"); + assertThat(key1).isNotEqualTo(key2); + } + @Test void missingPaymentMethodTypesThrows() { StubStripeApi api = new StubStripeApi(new StripeApi.Result("pi_abc123", "succeeded"));