diff --git a/src/main/java/com/gocardless/errors/ApiErrorResponse.java b/src/main/java/com/gocardless/errors/ApiErrorResponse.java index 006b791f..dbfac8cb 100644 --- a/src/main/java/com/gocardless/errors/ApiErrorResponse.java +++ b/src/main/java/com/gocardless/errors/ApiErrorResponse.java @@ -23,6 +23,10 @@ private ApiErrorResponse(String message, ErrorType type, String documentationUrl this.errors = errors; } + public static ApiErrorResponse fromMessage(String message, int code) { + return new ApiErrorResponse(message, ErrorType.GOCARDLESS, null, null, code, null); + } + String getMessage() { return message; } diff --git a/src/main/java/com/gocardless/http/HttpClient.java b/src/main/java/com/gocardless/http/HttpClient.java index 80163912..7f26128e 100644 --- a/src/main/java/com/gocardless/http/HttpClient.java +++ b/src/main/java/com/gocardless/http/HttpClient.java @@ -177,7 +177,7 @@ private T parseResponseBody(ApiRequest request, Response response) { private GoCardlessException handleErrorResponse(Response response) { try { String responseBody = response.body().string(); - return responseParser.parseError(responseBody); + return responseParser.parseError(responseBody, response.code()); } catch (IOException e) { throw new GoCardlessNetworkException("Failed to read response body", e); } diff --git a/src/main/java/com/gocardless/http/ResponseParser.java b/src/main/java/com/gocardless/http/ResponseParser.java index 504a56ec..494af8f3 100644 --- a/src/main/java/com/gocardless/http/ResponseParser.java +++ b/src/main/java/com/gocardless/http/ResponseParser.java @@ -51,8 +51,20 @@ ListResponse parsePage(String responseBody, String envelope, TypeToken
  • (ImmutableList.copyOf(items), meta, linked); } - GoCardlessApiException parseError(String responseBody) { - ApiErrorResponse error = parseSingle(responseBody, "error", ApiErrorResponse.class); + GoCardlessApiException parseError(String responseBody, int statusCode) { + JsonElement json; + try { + json = new JsonParser().parse(responseBody); + } catch (JsonSyntaxException e) { + throw new MalformedResponseException(responseBody); + } + JsonElement errorElement = json.getAsJsonObject().get("error"); + if (errorElement.isJsonPrimitive()) { + ApiErrorResponse error = + ApiErrorResponse.fromMessage(errorElement.getAsString(), statusCode); + return GoCardlessErrorMapper.toException(error); + } + ApiErrorResponse error = gson.fromJson(errorElement, ApiErrorResponse.class); return GoCardlessErrorMapper.toException(error); } } diff --git a/src/main/java/com/gocardless/resources/ScenarioSimulator.java b/src/main/java/com/gocardless/resources/ScenarioSimulator.java index 81f200e0..23176c2f 100644 --- a/src/main/java/com/gocardless/resources/ScenarioSimulator.java +++ b/src/main/java/com/gocardless/resources/ScenarioSimulator.java @@ -81,9 +81,8 @@ private ScenarioSimulator() { * `pending_submission` state. Not compatible with Autogiro, BECS NZ, and PAD mandates, which do * not expire.
  • *
  • `mandate_transferred`: Transitions a mandate through to `transferred`, having been - * submitted to the banks, set up successfully and then moved to a new bank account due to the - * customer using the UK's Current Account Switching Service (CASS). It must start in the - * `pending_submission` state. Only compatible with Bacs mandates.
  • + * submitted to the banks, set up successfully and then moved to a new bank account due. It must + * start in the `pending_submission` state. Only compatible with Bacs and SEPA mandates. *
  • `mandate_transferred_with_resubmission`: Transitions a mandate through `transferred` and * resubmits it to the banks, can be caused be the UK's Current Account Switching Service (CASS) * or when a customer contacts GoCardless to change their bank details. It must start in the diff --git a/src/main/java/com/gocardless/services/BillingRequestTemplateService.java b/src/main/java/com/gocardless/services/BillingRequestTemplateService.java index 53261738..5125d10c 100644 --- a/src/main/java/com/gocardless/services/BillingRequestTemplateService.java +++ b/src/main/java/com/gocardless/services/BillingRequestTemplateService.java @@ -57,7 +57,7 @@ public BillingRequestTemplateGetRequest get(String identity) { } /** - * + * */ public BillingRequestTemplateCreateRequest create() { return new BillingRequestTemplateCreateRequest(httpClient); diff --git a/src/main/java/com/gocardless/services/CustomerService.java b/src/main/java/com/gocardless/services/CustomerService.java index fb500697..28c2c3fa 100644 --- a/src/main/java/com/gocardless/services/CustomerService.java +++ b/src/main/java/com/gocardless/services/CustomerService.java @@ -302,11 +302,20 @@ protected boolean hasBody() { * Returns a [cursor-paginated](#api-usage-cursor-pagination) list of your customers. */ public static final class CustomerListRequest extends ListRequest { + private ActionRequired actionRequired; private CreatedAt createdAt; private Currency currency; private SortDirection sortDirection; private SortField sortField; + /** + * Boolean indicating whether the customer has any actions required. + */ + public CustomerListRequest withActionRequired(ActionRequired actionRequired) { + this.actionRequired = actionRequired; + return this; + } + /** * Cursor pointing to the start of the desired set. */ @@ -428,6 +437,9 @@ public CustomerListRequest withHeader(String headerName, String headerValue) protected Map getQueryParams() { ImmutableMap.Builder params = ImmutableMap.builder(); params.putAll(super.getQueryParams()); + if (actionRequired != null) { + params.put("action_required", actionRequired); + } if (createdAt != null) { params.putAll(createdAt.getQueryParams()); } @@ -458,6 +470,18 @@ protected TypeToken> getTypeToken() { return new TypeToken>() {}; } + public enum ActionRequired { + @SerializedName("true") + TRUE, @SerializedName("false") + FALSE, @SerializedName("unknown") + UNKNOWN; + + @Override + public String toString() { + return name().toLowerCase(); + } + } + public enum Currency { @SerializedName("AUD") AUD, @SerializedName("CAD") diff --git a/src/main/java/com/gocardless/services/MandateImportEntryService.java b/src/main/java/com/gocardless/services/MandateImportEntryService.java index cf79aa06..b5fed44f 100644 --- a/src/main/java/com/gocardless/services/MandateImportEntryService.java +++ b/src/main/java/com/gocardless/services/MandateImportEntryService.java @@ -473,6 +473,25 @@ public MandateImportEntryCreateRequest withMandate(Mandate mandate) { return this; } + /** + * This field is ACH specific, sometimes referred to as [SEC + * code](https://www.moderntreasury.com/learn/sec-codes). + * + * This is the way that the payer gives authorisation to the merchant. web: Authorisation is + * Internet Initiated or via Mobile Entry (maps to SEC code: WEB) telephone: Authorisation + * is provided orally over telephone (maps to SEC code: TEL) paper: Authorisation is + * provided in writing and signed, or similarly authenticated (maps to SEC code: PPD) + * + */ + public MandateImportEntryCreateRequest withMandateAuthorisationSource( + Mandate.AuthorisationSource authorisationSource) { + if (mandate == null) { + mandate = new Mandate(); + } + mandate.withAuthorisationSource(authorisationSource); + return this; + } + /** * Key-value store of custom data. Up to 3 keys are permitted, with key names up to 50 * characters and values up to 500 characters. @@ -856,9 +875,26 @@ public Links withMandateImport(String mandateImport) { } public static class Mandate { + private AuthorisationSource authorisationSource; private Map metadata; private String reference; + /** + * This field is ACH specific, sometimes referred to as [SEC + * code](https://www.moderntreasury.com/learn/sec-codes). + * + * This is the way that the payer gives authorisation to the merchant. web: + * Authorisation is Internet Initiated or via Mobile Entry (maps to SEC code: WEB) + * telephone: Authorisation is provided orally over telephone (maps to SEC code: TEL) + * paper: Authorisation is provided in writing and signed, or similarly authenticated + * (maps to SEC code: PPD) + * + */ + public Mandate withAuthorisationSource(AuthorisationSource authorisationSource) { + this.authorisationSource = authorisationSource; + return this; + } + /** * Key-value store of custom data. Up to 3 keys are permitted, with key names up to 50 * characters and values up to 500 characters. @@ -877,6 +913,19 @@ public Mandate withReference(String reference) { this.reference = reference; return this; } + + public enum AuthorisationSource { + @SerializedName("web") + WEB, @SerializedName("telephone") + TELEPHONE, @SerializedName("paper") + PAPER, @SerializedName("unknown") + UNKNOWN; + + @Override + public String toString() { + return name().toLowerCase(); + } + } } } diff --git a/src/main/java/com/gocardless/services/OutboundPaymentService.java b/src/main/java/com/gocardless/services/OutboundPaymentService.java index f62957cb..a756f2d9 100644 --- a/src/main/java/com/gocardless/services/OutboundPaymentService.java +++ b/src/main/java/com/gocardless/services/OutboundPaymentService.java @@ -37,7 +37,7 @@ public OutboundPaymentService(HttpClient httpClient) { } /** - * + * */ public OutboundPaymentCreateRequest create() { return new OutboundPaymentCreateRequest(httpClient); diff --git a/src/test/java/com/gocardless/http/ResponseParserTest.java b/src/test/java/com/gocardless/http/ResponseParserTest.java index 2254768c..f2bd1dee 100644 --- a/src/test/java/com/gocardless/http/ResponseParserTest.java +++ b/src/test/java/com/gocardless/http/ResponseParserTest.java @@ -69,7 +69,7 @@ public void shouldParsePage() throws IOException { public void shouldParseInvalidApiUsageError() throws IOException { URL resource = Resources.getResource("fixtures/invalid_api_usage.json"); String responseBody = Resources.toString(resource, UTF_8); - GoCardlessApiException exception = parser.parseError(responseBody); + GoCardlessApiException exception = parser.parseError(responseBody, 400); assertThat(exception).isInstanceOf(InvalidApiUsageException.class); assertThat(exception.getType()).isEqualTo(INVALID_API_USAGE); assertThat(exception.getMessage()).isEqualTo("Invalid document structure"); @@ -88,7 +88,7 @@ public void shouldParseInvalidApiUsageError() throws IOException { public void shouldParseInvalidStateError() throws IOException { URL resource = Resources.getResource("fixtures/invalid_state.json"); String responseBody = Resources.toString(resource, UTF_8); - GoCardlessApiException exception = parser.parseError(responseBody); + GoCardlessApiException exception = parser.parseError(responseBody, 409); assertThat(exception).isInstanceOf(InvalidStateException.class); assertThat(exception.getType()).isEqualTo(INVALID_STATE); assertThat(exception.getMessage()).isEqualTo("Bank account already exists"); @@ -108,7 +108,7 @@ public void shouldParseInvalidStateError() throws IOException { public void shouldParseValidationFailedError() throws IOException { URL resource = Resources.getResource("fixtures/validation_failed.json"); String responseBody = Resources.toString(resource, UTF_8); - GoCardlessApiException exception = parser.parseError(responseBody); + GoCardlessApiException exception = parser.parseError(responseBody, 422); assertThat(exception).isInstanceOf(ValidationFailedException.class); assertThat(exception.getType()).isEqualTo(VALIDATION_FAILED); assertThat(exception.getMessage()).isEqualTo( @@ -133,7 +133,7 @@ public void shouldParseValidationFailedError() throws IOException { public void shouldParseInternalError() throws IOException { URL resource = Resources.getResource("fixtures/internal_error.json"); String responseBody = Resources.toString(resource, UTF_8); - GoCardlessApiException exception = parser.parseError(responseBody); + GoCardlessApiException exception = parser.parseError(responseBody, 500); assertThat(exception).isInstanceOf(GoCardlessInternalException.class); assertThat(exception.getType()).isEqualTo(GOCARDLESS); assertThat(exception.getMessage()).isEqualTo("THE BEES THEY'RE IN MY EYES"); @@ -145,19 +145,32 @@ public void shouldParseInternalError() throws IOException { assertThat(exception.getErrors()).isEmpty(); } + @Test + public void shouldParseStringError() throws IOException { + URL resource = Resources.getResource("fixtures/string_error.json"); + String responseBody = Resources.toString(resource, UTF_8); + GoCardlessApiException exception = parser.parseError(responseBody, 400); + assertThat(exception).isInstanceOf(GoCardlessInternalException.class); + assertThat(exception.getType()).isEqualTo(GOCARDLESS); + assertThat(exception.getMessage()).isEqualTo("bank_authorisation_expired"); + assertThat(exception.getErrorMessage()).isEqualTo("bank_authorisation_expired"); + assertThat(exception.getCode()).isEqualTo(400); + assertThat(exception.getErrors()).isEmpty(); + } + @Test public void shouldHandleNonJsonResponse() throws IOException { exception.expect(MalformedResponseException.class); URL resource = Resources.getResource("fixtures/non_json_response.html"); String responseBody = Resources.toString(resource, UTF_8); - parser.parseError(responseBody); + parser.parseError(responseBody, 500); } @Test public void shouldHandleAuthenticationError() throws IOException { URL resource = Resources.getResource("fixtures/unauthorized_error.json"); String responseBody = Resources.toString(resource, UTF_8); - GoCardlessApiException exception = parser.parseError(responseBody); + GoCardlessApiException exception = parser.parseError(responseBody, 401); assertThat(exception).isInstanceOf(AuthenticationException.class); assertThat(exception.getMessage()).isEqualTo("Unauthorized"); assertThat(exception.getDocumentationUrl()) @@ -174,7 +187,7 @@ public void shouldHandleAuthenticationError() throws IOException { public void shouldHandleRateLimitError() throws IOException { URL resource = Resources.getResource("fixtures/rate_limit_exceeded.json"); String responseBody = Resources.toString(resource, UTF_8); - GoCardlessApiException exception = parser.parseError(responseBody); + GoCardlessApiException exception = parser.parseError(responseBody, 429); assertThat(exception).isInstanceOf(RateLimitException.class); assertThat(exception.getMessage()).isEqualTo("Rate limit exceeded"); assertThat(exception.getDocumentationUrl()) @@ -191,7 +204,7 @@ public void shouldHandleRateLimitError() throws IOException { public void shouldHandlePermissionException() throws IOException { URL resource = Resources.getResource("fixtures/insufficient_permission.json"); String responseBody = Resources.toString(resource, UTF_8); - GoCardlessApiException exception = parser.parseError(responseBody); + GoCardlessApiException exception = parser.parseError(responseBody, 403); assertThat(exception).isInstanceOf(PermissionException.class); assertThat(exception.getMessage()).isEqualTo("Insufficient permissions"); assertThat(exception.getDocumentationUrl()).isEqualTo( diff --git a/src/test/resources/fixtures/string_error.json b/src/test/resources/fixtures/string_error.json new file mode 100644 index 00000000..70870dd1 --- /dev/null +++ b/src/test/resources/fixtures/string_error.json @@ -0,0 +1 @@ +{"error": "bank_authorisation_expired"} \ No newline at end of file