Skip to content

Commit

Permalink
✨ Support new API version error format
Browse files Browse the repository at this point in the history
While FreshBooks API versions are not yet documented, the recent versions (2022-10-31 and forward) feature a slightly different response format when some /accounting endpoints fail.

Update the accounting handlers to handle both formats.

Fixes #41
  • Loading branch information
Andrew McIntosh committed Aug 7, 2023
1 parent 462b045 commit caecf94
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ local.properties
# Editor config
.vscode/
.idea/
.settings/
lib/bin/

2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Handle new API version accounting errors

## 0.6.0

- Added Profit & Loss accounting report
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.amcintosh.freshbooks.models.api;

import com.google.api.client.util.Key;


public class AccountingErrorDetails {
@Key("@type")
public String type;

@Key
public String reason;
@Key
public String domain;
@Key
public AccountingErrorDetailsMetadata metadata;

public int getReason() {
return Integer.parseInt(reason);
}

public static class AccountingErrorDetailsMetadata {
@Key
public String object;
@Key
public String message;
@Key
public String field;
@Key
public String value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ public class AccountingListResponse {

@Key public AccountingListInnerResponse response;

@Key public int code;
@Key public String message;
@Key public ArrayList<AccountingErrorDetails> details;

public static class AccountingListInnerResponse {

@Key public AccountingListResult result;
Expand Down Expand Up @@ -37,6 +41,5 @@ public static class AccountingListResult {

@Key("other_income") public ArrayList<OtherIncome> otherIncomes;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ public class AccountingResponse {

@Key public AccountingInnerResponse response;

@Key public int code;
@Key public String message;
@Key public ArrayList<AccountingErrorDetails> details;

public static class AccountingInnerResponse {

@Key public AccountingResult result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import net.amcintosh.freshbooks.FreshBooksClient;
import net.amcintosh.freshbooks.FreshBooksException;
import net.amcintosh.freshbooks.models.api.AccountingError;
import net.amcintosh.freshbooks.models.api.AccountingErrorDetails;
import net.amcintosh.freshbooks.models.api.AccountingListResponse;
import net.amcintosh.freshbooks.models.api.AccountingResponse;
import net.amcintosh.freshbooks.models.builders.IncludesQueryBuilder;
Expand Down Expand Up @@ -64,6 +65,14 @@ protected AccountingResponse handleRequest(String method, String url) throws Fre
return this.handleRequest(method, url, null);
}

private boolean isNewAccountingError(AccountingResponse responseModel) {
return responseModel != null && responseModel.message != null;
}

private boolean isOldAccountingError(AccountingResponse responseModel) {
return responseModel != null && responseModel.response != null && responseModel.response.errors != null;
}

protected AccountingResponse handleRequest(String method, String url, Map<String, Object> content) throws FreshBooksException {
HttpResponse response;
AccountingResponse model = null;
Expand All @@ -83,11 +92,21 @@ protected AccountingResponse handleRequest(String method, String url, Map<String
throw new FreshBooksException("Returned an unexpected response", statusMessage, statusCode, e);
}

if (!response.isSuccessStatusCode() && model != null && model.response != null && model.response.errors != null) {
if (!response.isSuccessStatusCode() && this.isOldAccountingError(model)) {
AccountingError error = model.response.errors.get(0);
throw new FreshBooksException(error.message, statusMessage, statusCode,
error.errno, error.field, error.object, error.value);
}
if (!response.isSuccessStatusCode() && this.isNewAccountingError(model)) {
if (model.details.size() > 0) {
AccountingErrorDetails error = model.details.get(0);
if (error.type.equals("type.googleapis.com/google.rpc.ErrorInfo") && error.metadata != null) {
throw new FreshBooksException(error.metadata.message, statusMessage, statusCode,
error.getReason(), error.metadata.field, error.metadata.object, error.metadata.value);
}
}
throw new FreshBooksException(model.message, statusMessage, statusCode);
}

if (response.isSuccessStatusCode() && method.equals(HttpMethods.DELETE) && model != null && model.response != null ) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ public void getClient_includes() throws FreshBooksException, IOException {
}

@Test
public void getResource_notFound() throws IOException {
String jsonResponse = TestUtil.loadTestJson("fixtures/get_client_response__not_found.json");
public void getResource_notFoundOld() throws IOException {
String jsonResponse = TestUtil.loadTestJson("fixtures/get_client_response__not_found_old.json");
FreshBooksClient mockedFreshBooksClient = mock(FreshBooksClient.class);
HttpRequest mockRequest = TestUtil.buildMockHttpRequest(404, jsonResponse);
when(mockedFreshBooksClient.request(HttpMethods.GET,
Expand All @@ -65,6 +65,52 @@ public void getResource_notFound() throws IOException {
}
}

@Test
public void getResource_notFoundNew() throws IOException {
String jsonResponse = TestUtil.loadTestJson("fixtures/get_client_response__not_found_new.json");
FreshBooksClient mockedFreshBooksClient = mock(FreshBooksClient.class);
HttpRequest mockRequest = TestUtil.buildMockHttpRequest(404, jsonResponse);
when(mockedFreshBooksClient.request(HttpMethods.GET,
"/accounting/account/ABC123/users/clients/12345", null)).thenReturn(mockRequest);

long clientId = 12345;
Clients clients = new Clients(mockedFreshBooksClient);

try {
clients.get("ABC123", clientId);
} catch (FreshBooksException e) {
assertEquals(404, e.statusCode);
assertEquals("Client not found.", e.getMessage());
assertEquals(1012, e.errorNo);
assertEquals("userid", e.field);
assertEquals("client", e.object);
assertEquals("12345", e.value);
}
}

@Test
public void getResource_noAuth() throws IOException {
String jsonResponse = TestUtil.loadTestJson("fixtures/get_client_response__no_auth.json");
FreshBooksClient mockedFreshBooksClient = mock(FreshBooksClient.class);
HttpRequest mockRequest = TestUtil.buildMockHttpRequest(401, jsonResponse);
when(mockedFreshBooksClient.request(HttpMethods.GET,
"/accounting/account/ABC123/users/clients/12345", null)).thenReturn(mockRequest);

long clientId = 12345;
Clients clients = new Clients(mockedFreshBooksClient);

try {
clients.get("ABC123", clientId);
} catch (FreshBooksException e) {
assertEquals(401, e.statusCode);
assertEquals("The server could not verify that you are authorized to access the URL requested.", e.getMessage());
assertEquals(1003, e.errorNo);
assertEquals("", e.field);
assertEquals("", e.object);
assertEquals("", e.value);
}
}

@Test
public void getResource_badResponse() throws IOException {
String jsonResponse = "stuff";
Expand Down
17 changes: 17 additions & 0 deletions lib/src/test/resources/fixtures/get_client_response__no_auth.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"code": 16,
"message": "Request failed with status_code: 401",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "1003",
"domain": "accounting.api.freshbooks.com",
"metadata": {
"object": "",
"message": "The server could not verify that you are authorized to access the URL requested.",
"value": "",
"field": ""
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"code": 5,
"message": "Request failed with status_code: 404",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "1012",
"domain": "accounting.api.freshbooks.com",
"metadata": {
"object": "client",
"message": "Client not found.",
"value": "12345",
"field": "userid"
}
}
]
}

0 comments on commit caecf94

Please sign in to comment.