diff --git a/src/main/java/com/stripe/mpp/error/MalformedCredentialException.java b/src/main/java/com/stripe/mpp/error/MalformedCredentialException.java index c6ad280..804ed64 100644 --- a/src/main/java/com/stripe/mpp/error/MalformedCredentialException.java +++ b/src/main/java/com/stripe/mpp/error/MalformedCredentialException.java @@ -3,7 +3,7 @@ public class MalformedCredentialException extends PaymentException { public MalformedCredentialException(String reason) { super(reason != null ? "Credential is malformed: " + reason + "." : "Credential is malformed.", - 402, BASE_URI + "/malformed-credential", "Malformed Credential"); + 402, BASE_URI + "/malformed-credential", "Malformed Credential", MALFORMED_CREDENTIAL_HINT); } public MalformedCredentialException() { diff --git a/src/main/java/com/stripe/mpp/error/PaymentException.java b/src/main/java/com/stripe/mpp/error/PaymentException.java index b16b5b2..7022f02 100644 --- a/src/main/java/com/stripe/mpp/error/PaymentException.java +++ b/src/main/java/com/stripe/mpp/error/PaymentException.java @@ -5,16 +5,28 @@ public class PaymentException extends RuntimeException { public static final String BASE_URI = "https://paymentauth.org/problems"; + public static final String PAYMENT_REQUIRED_HINT = + "Use a supported wallet to pay for this resource using one of the supported " + + "payment methods returned in the WWW-Authenticate header. See https://mpp.dev/tools/wallet.md"; + public static final String MALFORMED_CREDENTIAL_HINT = + "Use a supported wallet to construct valid credentials for one of the supported " + + "payment methods returned in the WWW-Authenticate header. See https://mpp.dev/tools/wallet.md"; private final int httpStatus; private final String type; private final String title; + private final String hint; public PaymentException(String message, int httpStatus, String type, String title) { + this(message, httpStatus, type, title, null); + } + + public PaymentException(String message, int httpStatus, String type, String title, String hint) { super(message); this.httpStatus = httpStatus; this.type = type; this.title = title; + this.hint = hint; } public PaymentException(String message) { @@ -24,6 +36,7 @@ public PaymentException(String message) { public int getHttpStatus() { return httpStatus; } public String getType() { return type; } public String getTitle() { return title; } + public String getHint() { return hint; } public Map toProblemDetails() { return toProblemDetails(null); @@ -35,6 +48,9 @@ public Map toProblemDetails(String challengeId) { details.put("title", title); details.put("status", httpStatus); details.put("detail", getMessage()); + if (hint != null) { + details.put("hint", hint); + } if (challengeId != null) { details.put("challengeId", challengeId); } diff --git a/src/main/java/com/stripe/mpp/error/PaymentMethodUnsupportedException.java b/src/main/java/com/stripe/mpp/error/PaymentMethodUnsupportedException.java index 6ea3f65..44545d9 100644 --- a/src/main/java/com/stripe/mpp/error/PaymentMethodUnsupportedException.java +++ b/src/main/java/com/stripe/mpp/error/PaymentMethodUnsupportedException.java @@ -3,7 +3,7 @@ public class PaymentMethodUnsupportedException extends PaymentException { public PaymentMethodUnsupportedException(String method) { super(method != null ? "Payment method \"" + method + "\" is not supported." : "Payment method is not supported.", - 400, BASE_URI + "/method-unsupported", "Method Unsupported"); + 400, BASE_URI + "/method-unsupported", "Method Unsupported", PAYMENT_REQUIRED_HINT); } public PaymentMethodUnsupportedException() { diff --git a/src/main/java/com/stripe/mpp/error/PaymentRequiredException.java b/src/main/java/com/stripe/mpp/error/PaymentRequiredException.java index 76cea31..7433d18 100644 --- a/src/main/java/com/stripe/mpp/error/PaymentRequiredException.java +++ b/src/main/java/com/stripe/mpp/error/PaymentRequiredException.java @@ -3,7 +3,7 @@ public class PaymentRequiredException extends PaymentException { public PaymentRequiredException(String realm, String description) { super(buildMessage(realm, description), 402, - BASE_URI + "/payment-required", "Payment Required"); + BASE_URI + "/payment-required", "Payment Required", PAYMENT_REQUIRED_HINT); } public PaymentRequiredException() { diff --git a/src/test/java/com/stripe/mpp/error/PaymentExceptionTest.java b/src/test/java/com/stripe/mpp/error/PaymentExceptionTest.java new file mode 100644 index 0000000..3b7aa13 --- /dev/null +++ b/src/test/java/com/stripe/mpp/error/PaymentExceptionTest.java @@ -0,0 +1,50 @@ +package com.stripe.mpp.error; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class PaymentExceptionTest { + @Test + void paymentRequiredProblemDetailsIncludeHint() { + PaymentException error = new PaymentRequiredException("api.example.com", "Paid endpoint"); + + Map details = error.toProblemDetails("ch_123"); + + assertThat(error.getHint()).isEqualTo(PaymentException.PAYMENT_REQUIRED_HINT); + assertThat(details).containsEntry("hint", PaymentException.PAYMENT_REQUIRED_HINT); + assertThat(details).containsEntry("challengeId", "ch_123"); + } + + @Test + void malformedCredentialProblemDetailsIncludeHint() { + PaymentException error = new MalformedCredentialException("bad base64"); + + Map details = error.toProblemDetails(); + + assertThat(error.getHint()).isEqualTo(PaymentException.MALFORMED_CREDENTIAL_HINT); + assertThat(details).containsEntry("hint", PaymentException.MALFORMED_CREDENTIAL_HINT); + } + + @Test + void methodUnsupportedProblemDetailsIncludePaymentRequiredHint() { + PaymentException error = new PaymentMethodUnsupportedException("unknown"); + + Map details = error.toProblemDetails(); + + assertThat(error.getHint()).isEqualTo(PaymentException.PAYMENT_REQUIRED_HINT); + assertThat(details).containsEntry("hint", PaymentException.PAYMENT_REQUIRED_HINT); + } + + @Test + void problemDetailsOmitHintWhenUnset() { + PaymentException error = new InvalidChallengeException("ch_123", "expired"); + + Map details = error.toProblemDetails(); + + assertThat(error.getHint()).isNull(); + assertThat(details).doesNotContainKey("hint"); + } +}