From ab03e27a60ff14c0acf2de821157def79a74a8fc Mon Sep 17 00:00:00 2001 From: javorosas Date: Wed, 9 Sep 2026 13:13:49 +0200 Subject: [PATCH 1/5] fix: serialize nested query map values with bracket notation A date range passed as a nested map (Map.of("date", Map.of("gte", ..., "lt", ...))) was stringified with Map.toString(), producing garbage like date={gte=2026-01-01, lt=2026-02-01}. Recurse into Map values and expand them into bracket keys (date[gte]=...&date[lt]=...), matching the v2 API contract. Iterable/array values keep expanding to repeated keys. Bump to 2.1.1 and add a regression test. --- pom.xml | 2 +- .../facturapi/http/FacturapiHttpClient.java | 37 ++++++++++--------- .../io/facturapi/FacturapiHttpClientTest.java | 26 +++++++++++++ 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/pom.xml b/pom.xml index de17cc0..309e450 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.facturapi facturapi-java - 2.1.0 + 2.1.1 facturapi-java Official Java SDK for Facturapi https://github.com/facturapi/facturapi-java diff --git a/src/main/java/io/facturapi/http/FacturapiHttpClient.java b/src/main/java/io/facturapi/http/FacturapiHttpClient.java index a25c3b4..30b2c48 100644 --- a/src/main/java/io/facturapi/http/FacturapiHttpClient.java +++ b/src/main/java/io/facturapi/http/FacturapiHttpClient.java @@ -345,23 +345,7 @@ private static String buildQuery(Map queryParams) { List parts = new ArrayList<>(); for (Map.Entry 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); @@ -371,6 +355,25 @@ private static void appendQueryPart(List 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); diff --git a/src/test/java/io/facturapi/FacturapiHttpClientTest.java b/src/test/java/io/facturapi/FacturapiHttpClientTest.java index e8d6eac..fb45de9 100644 --- a/src/test/java/io/facturapi/FacturapiHttpClientTest.java +++ b/src/test/java/io/facturapi/FacturapiHttpClientTest.java @@ -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; @@ -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(); From 41c39b6d18dbbd95f09cbcdc0e1a5d752f7bc6e9 Mon Sep 17 00:00:00 2001 From: javorosas Date: Wed, 9 Sep 2026 14:42:44 +0200 Subject: [PATCH 2/5] feat(models): expose cursor and capped-total metadata on search results SearchResult gains totalsAreCapped, nextCursor, and previousCursor so callers can follow cursor pagination and detect capped totals once the capping wave is live (page totals capped; cursor mode returns totals only on the first page). --- .../io/facturapi/models/SearchResult.java | 27 +++++++++++++++++++ .../io/facturapi/FacturapiResourcesTest.java | 24 +++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/main/java/io/facturapi/models/SearchResult.java b/src/main/java/io/facturapi/models/SearchResult.java index 2701e72..429ed41 100644 --- a/src/main/java/io/facturapi/models/SearchResult.java +++ b/src/main/java/io/facturapi/models/SearchResult.java @@ -12,7 +12,10 @@ public class SearchResult { private Integer totalPages; @JsonAlias({"total"}) private Integer totalResults; + private Boolean totalsAreCapped; private Boolean hasMore; + private String nextCursor; + private String previousCursor; private List data = new ArrayList<>(); public Integer getPage() { @@ -39,6 +42,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; } @@ -47,6 +58,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 getData() { return data; } diff --git a/src/test/java/io/facturapi/FacturapiResourcesTest.java b/src/test/java/io/facturapi/FacturapiResourcesTest.java index e425076..7656160 100644 --- a/src/test/java/io/facturapi/FacturapiResourcesTest.java +++ b/src/test/java/io/facturapi/FacturapiResourcesTest.java @@ -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; @@ -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(); From 3a172df8bc805a3e36b318bf656ff134c27e44ac Mon Sep 17 00:00:00 2001 From: javorosas Date: Wed, 9 Sep 2026 15:28:06 +0200 Subject: [PATCH 3/5] chore: bump to 2.2.0 (minor) The nested query serialization fix is a patch, but the new cursor and capped-total fields on SearchResult are additive public API, so the release is a minor per semver, consistent with the node SDK bump. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 309e450..2a1fa3f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.facturapi facturapi-java - 2.1.1 + 2.2.0 facturapi-java Official Java SDK for Facturapi https://github.com/facturapi/facturapi-java From b4a5f43688768ffd2b4fdf8423f725b2058bd99c Mon Sep 17 00:00:00 2001 From: javorosas Date: Wed, 9 Sep 2026 15:49:22 +0200 Subject: [PATCH 4/5] docs(changelog): add 2.2.0 entry SearchResult cursor/capped metadata under Added; nested query map serialization fix under Fixed. --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a7267..61cd353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 597f49d96504d961004a3292e57c72b10d486ef1 Mon Sep 17 00:00:00 2001 From: javorosas Date: Wed, 9 Sep 2026 22:13:20 +0200 Subject: [PATCH 5/5] chore: drop unused JsonProperty import in SearchResult --- src/main/java/io/facturapi/models/SearchResult.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/io/facturapi/models/SearchResult.java b/src/main/java/io/facturapi/models/SearchResult.java index 429ed41..b36b57b 100644 --- a/src/main/java/io/facturapi/models/SearchResult.java +++ b/src/main/java/io/facturapi/models/SearchResult.java @@ -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;