Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.2.0] - 2026-09-09

### Added

- Expose cursor pagination and capped-total metadata on `SearchResult`: `totalsAreCapped`, `nextCursor`, and `previousCursor` so callers can follow cursor pagination and detect capped totals (page totals capped; cursor mode returns totals only on the first page).

### Fixed

- Serialize nested query map values with bracket notation. A date range passed as a nested `Map` (for example `date -> gte/lt`) was stringified with `Map.toString()`; values now expand to bracket keys (`date[gte]=...&date[lt]=...`).

## [2.1.0] - 2026-09-04

### Added
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>io.facturapi</groupId>
<artifactId>facturapi-java</artifactId>
<version>2.1.0</version>
<version>2.2.0</version>
Comment thread
javorosas marked this conversation as resolved.
<name>facturapi-java</name>
<description>Official Java SDK for Facturapi</description>
<url>https://github.com/facturapi/facturapi-java</url>
Expand Down
37 changes: 20 additions & 17 deletions src/main/java/io/facturapi/http/FacturapiHttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -345,23 +345,7 @@ private static String buildQuery(Map<String, ?> queryParams) {

List<String> parts = new ArrayList<>();
for (Map.Entry<String, ?> entry : queryParams.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value == null) {
continue;
}
if (value instanceof Iterable<?>) {
for (Object v : (Iterable<?>) value) {
appendQueryPart(parts, key, v);
}
} else if (value.getClass().isArray()) {
int length = java.lang.reflect.Array.getLength(value);
for (int i = 0; i < length; i++) {
appendQueryPart(parts, key, java.lang.reflect.Array.get(value, i));
}
} else {
appendQueryPart(parts, key, value);
}
appendQueryPart(parts, entry.getKey(), entry.getValue());
}

return parts.isEmpty() ? "" : "?" + String.join("&", parts);
Expand All @@ -371,6 +355,25 @@ private static void appendQueryPart(List<String> parts, String key, Object value
if (value == null) {
return;
}
if (value instanceof Map<?, ?>) {
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
appendQueryPart(parts, key + "[" + entry.getKey() + "]", entry.getValue());
}
return;
}
if (value instanceof Iterable<?>) {
for (Object item : (Iterable<?>) value) {
appendQueryPart(parts, key, item);
}
return;
}
if (value.getClass().isArray()) {
int length = java.lang.reflect.Array.getLength(value);
for (int i = 0; i < length; i++) {
appendQueryPart(parts, key, java.lang.reflect.Array.get(value, i));
}
return;
}
String encodedKey = URLEncoder.encode(Objects.toString(key), StandardCharsets.UTF_8);
String encodedValue = URLEncoder.encode(Objects.toString(value), StandardCharsets.UTF_8);
parts.add(encodedKey + "=" + encodedValue);
Expand Down
28 changes: 27 additions & 1 deletion src/main/java/io/facturapi/models/SearchResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -12,7 +11,10 @@ public class SearchResult<T> {
private Integer totalPages;
@JsonAlias({"total"})
private Integer totalResults;
private Boolean totalsAreCapped;
private Boolean hasMore;
private String nextCursor;
private String previousCursor;
Comment thread
javorosas marked this conversation as resolved.
private List<T> data = new ArrayList<>();

public Integer getPage() {
Expand All @@ -39,6 +41,14 @@ public void setTotalResults(Integer totalResults) {
this.totalResults = totalResults;
}

public Boolean getTotalsAreCapped() {
return totalsAreCapped;
}

public void setTotalsAreCapped(Boolean totalsAreCapped) {
this.totalsAreCapped = totalsAreCapped;
}

public Boolean getHasMore() {
return hasMore;
}
Expand All @@ -47,6 +57,22 @@ public void setHasMore(Boolean hasMore) {
this.hasMore = hasMore;
}

public String getNextCursor() {
return nextCursor;
}

public void setNextCursor(String nextCursor) {
this.nextCursor = nextCursor;
}

public String getPreviousCursor() {
return previousCursor;
}

public void setPreviousCursor(String previousCursor) {
this.previousCursor = previousCursor;
}

public List<T> getData() {
return data;
}
Expand Down
26 changes: 26 additions & 0 deletions src/test/java/io/facturapi/FacturapiHttpClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -78,6 +79,31 @@ void throwsFacturapiExceptionWithApiMessage() {
assertEquals("log_123", ex.getHeaders().get("x-facturapi-log-id").get(0));
}

@Test
void serializesNestedDateRangeQueryWithBracketNotation() throws Exception {
StubHttpClient httpClient = new StubHttpClient();
httpClient.enqueueJson(200, "{\"data\":[]}");

FacturapiHttpClient client = new FacturapiHttpClient(
FacturapiConfig.builder("sk_test_123")
.httpClient(httpClient.client())
.build()
);

client.get(
"/invoices",
Map.of("limit", 100, "date", Map.of("gte", "2026-01-01", "lt", "2026-02-01")),
GenericResponse.class
);

var request = httpClient.requests().get(0);
String query = request.uri().getQuery();
assertTrue(query.contains("limit=100"));
assertTrue(query.contains("date[gte]=2026-01-01"));
assertTrue(query.contains("date[lt]=2026-02-01"));
assertFalse(query.contains("{gte"));
}

@Test
void convertsNumericApiErrorCodesToStrings() {
StubHttpClient httpClient = new StubHttpClient();
Expand Down
24 changes: 24 additions & 0 deletions src/test/java/io/facturapi/FacturapiResourcesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.facturapi.enums.CancellationStatus;
Expand Down Expand Up @@ -539,6 +540,29 @@ void objectMapperDeserializesCodeEnums() throws Exception {
assertEquals(TaxFactor.EXENTO, tax.getFactor());
}

@Test
void invoiceListDeserializesSearchResultPaginationMetadata() throws Exception {
StubHttpClient httpClient = new StubHttpClient();
httpClient.enqueueJson(
200,
"{\"data\":[],\"total_results\":3000,\"totals_are_capped\":true,\"next_cursor\":\"next-1\",\"previous_cursor\":null}"
);

Facturapi sdk = new Facturapi(
FacturapiConfig.builder("sk_test")
.httpClient(httpClient.client())
.build()
);

var result = sdk.invoices().list(Map.of());

assertTrue(result.getTotalsAreCapped());
assertEquals(3000, result.getTotalResults());
assertEquals("next-1", result.getNextCursor());
assertNull(result.getPreviousCursor());
assertTrue(result.getData().isEmpty());
}

@Test
void objectMapperDeserializesPropertyTaxAccountsAsArrays() throws Exception {
var mapper = FacturapiConfig.builder("sk_test").build().getObjectMapper();
Expand Down
Loading