Skip to content

Commit 3aebcad

Browse files
feat!: publicar métodos ZIP, borradores y correcciones de tipos (#10)
* feat(retentions): agregar métodos para borradores (#7) * chore(retentions): add new retentions method for support drafts * chore: keep retention draft methods unreleased --------- Co-authored-by: javorosas <javier.rosasb@gmail.com> * feat(invoices): agregar métodos ZIP (#11) * Add invoice ZIP request methods * fix: align ZIP download method naming --------- Co-authored-by: javorosas <javier.rosasb@gmail.com> * fix!: return Java API error codes as strings * fix: only normalize numeric API error codes * fix: model property tax accounts as arrays * docs: update Java dependency snippets for 2.0.0 * docs: complete Java 2.0.0 changelog --------- Co-authored-by: Raúl García <141654545+raul-facturapi@users.noreply.github.com>
1 parent efd9436 commit 3aebcad

10 files changed

Lines changed: 213 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,22 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## Unreleased
8+
## [2.0.0] - 2026-08-24
99

1010
### Added
1111

1212
- Add draft support methods for retentions: `retentions.updateDraft(String id, Map<String, Object> data)`, `retentions.copyToDraft(String id)`, `retentions.stampDraft(String id)`, and `retentions.cancel(String id)`.
1313

14+
- Add `invoices.createZipRequest(Map<String, Object> data)` for `POST /invoices/zip-requests`.
15+
- Add `invoices.listZipRequests(Map<String, ?> params)` for `GET /invoices/zip-requests`.
16+
- Add `invoices.retrieveZipRequest(String id)` for `GET /invoices/zip-requests/{id}`.
17+
- Add `invoices.downloadZipRequestStream(String id)` for streaming `GET /invoices/zip-requests/{id}/zip`.
18+
19+
### Changed
20+
21+
- Change `FacturapiException.getErrorCode()` to return strings, including converted legacy numeric codes.
22+
- Change `InvoiceItem.property_tax_account` to a list of property tax account numbers.
23+
1424
## [1.3.0] - 2026-06-07
1525

1626
### Added

README.es.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ Maven:
2929
<dependency>
3030
<groupId>io.facturapi</groupId>
3131
<artifactId>facturapi-java</artifactId>
32-
<version>1.3.0</version>
32+
<version>2.0.0</version>
3333
</dependency>
3434
```
3535

3636
Gradle:
3737

3838
```gradle
39-
implementation("io.facturapi:facturapi-java:1.3.0")
39+
implementation("io.facturapi:facturapi-java:2.0.0")
4040
```
4141

4242
## Inicio rápido

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ Maven:
2929
<dependency>
3030
<groupId>io.facturapi</groupId>
3131
<artifactId>facturapi-java</artifactId>
32-
<version>1.3.0</version>
32+
<version>2.0.0</version>
3333
</dependency>
3434
```
3535

3636
Gradle:
3737

3838
```gradle
39-
implementation("io.facturapi:facturapi-java:1.3.0")
39+
implementation("io.facturapi:facturapi-java:2.0.0")
4040
```
4141

4242
## Quickstart

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>io.facturapi</groupId>
77
<artifactId>facturapi-java</artifactId>
8-
<version>1.3.0</version>
8+
<version>2.0.0</version>
99
<name>facturapi-java</name>
1010
<description>Official Java SDK for Facturapi</description>
1111
<url>https://github.com/facturapi/facturapi-java</url>

src/main/java/io/facturapi/FacturapiException.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
public class FacturapiException extends RuntimeException {
99
private final int statusCode;
10-
private final Object errorCode;
10+
private final String errorCode;
1111
private final String errorPath;
1212
private final String errorLocation;
1313
private final JsonNode errors;
@@ -29,14 +29,14 @@ public FacturapiException(String message, Throwable cause) {
2929
this.headers = Collections.emptyMap();
3030
}
3131

32-
public FacturapiException(String message, int statusCode, Object errorCode, String errorPath) {
32+
public FacturapiException(String message, int statusCode, String errorCode, String errorPath) {
3333
this(message, statusCode, errorCode, errorPath, null, null, null, Collections.emptyMap());
3434
}
3535

3636
public FacturapiException(
3737
String message,
3838
int statusCode,
39-
Object errorCode,
39+
String errorCode,
4040
String errorPath,
4141
String errorLocation,
4242
JsonNode errors,
@@ -57,7 +57,7 @@ public int getStatusCode() {
5757
return statusCode;
5858
}
5959

60-
public Object getErrorCode() {
60+
public String getErrorCode() {
6161
return errorCode;
6262
}
6363

src/main/java/io/facturapi/http/FacturapiHttpClient.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ private static JsonNode firstDefined(JsonNode node, String... keys) {
210210
private FacturapiException buildApiException(String bodyText, Response response) {
211211
int statusCode = response.code();
212212
int resolvedStatus = statusCode;
213-
Object errorCode = null;
213+
String errorCode = null;
214214
String errorPath = null;
215215
String errorLocation = null;
216216
JsonNode errors = null;
@@ -245,18 +245,8 @@ private FacturapiException buildApiException(String bodyText, Response response)
245245
}
246246

247247
JsonNode codeNode = firstDefined(root, "code");
248-
if (codeNode != null && !codeNode.isNull()) {
249-
if (codeNode.isTextual()) {
250-
errorCode = codeNode.asText();
251-
} else if (codeNode.isIntegralNumber()) {
252-
errorCode = codeNode.intValue();
253-
} else if (codeNode.isNumber()) {
254-
errorCode = codeNode.numberValue();
255-
} else if (codeNode.isBoolean()) {
256-
errorCode = codeNode.asBoolean();
257-
} else {
258-
errorCode = codeNode.toString();
259-
}
248+
if (codeNode != null && (codeNode.isTextual() || codeNode.isNumber())) {
249+
errorCode = codeNode.asText();
260250
}
261251

262252
JsonNode pathNode = firstDefined(root, "path");

src/main/java/io/facturapi/models/InvoiceItem.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public class InvoiceItem {
1414
private InvoiceItemThirdParty thirdParty;
1515
private String complement;
1616
private List<InvoiceItemPart> parts = new ArrayList<>();
17-
private String propertyTaxAccount;
17+
@JsonProperty("property_tax_account")
18+
private List<String> propertyTaxAccounts = new ArrayList<>();
1819

1920
public Double getQuantity() { return quantity; }
2021
public void setQuantity(Double quantity) { this.quantity = quantity; }
@@ -30,6 +31,8 @@ public class InvoiceItem {
3031
public void setComplement(String complement) { this.complement = complement; }
3132
public List<InvoiceItemPart> getParts() { return parts; }
3233
public void setParts(List<InvoiceItemPart> parts) { this.parts = parts; }
33-
public String getPropertyTaxAccount() { return propertyTaxAccount; }
34-
public void setPropertyTaxAccount(String propertyTaxAccount) { this.propertyTaxAccount = propertyTaxAccount; }
34+
@JsonProperty("property_tax_account")
35+
public List<String> getPropertyTaxAccounts() { return propertyTaxAccounts; }
36+
@JsonProperty("property_tax_account")
37+
public void setPropertyTaxAccounts(List<String> propertyTaxAccounts) { this.propertyTaxAccounts = propertyTaxAccounts; }
3538
}

src/main/java/io/facturapi/resources/InvoicesResource.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,63 @@ public InputStream downloadZipStream(String id) {
142142
return client.getStream("/invoices/" + id + "/zip");
143143
}
144144

145+
/**
146+
* Creates a ZIP request or returns the existing request for the same criteria.
147+
*
148+
* @param data ZIP request criteria.
149+
* @return ZIP request data.
150+
* @see <a href="https://docs.facturapi.io/api#tag/Invoices">API reference</a>
151+
*/
152+
public Map<String, Object> createZipRequest(Map<String, Object> data) {
153+
return post(
154+
"/invoices/zip-requests",
155+
data,
156+
null,
157+
new TypeReference<Map<String, Object>>() {}
158+
);
159+
}
160+
161+
/**
162+
* Gets a paginated list of ZIP requests.
163+
*
164+
* @param params Search and pagination parameters.
165+
* @return Paginated ZIP request result.
166+
* @see <a href="https://docs.facturapi.io/api#tag/Invoices">API reference</a>
167+
*/
168+
public SearchResult<Map<String, Object>> listZipRequests(Map<String, ?> params) {
169+
return get(
170+
"/invoices/zip-requests",
171+
params,
172+
new TypeReference<SearchResult<Map<String, Object>>>() {}
173+
);
174+
}
175+
176+
/**
177+
* Gets a ZIP request by id.
178+
*
179+
* @param id ZIP request id.
180+
* @return ZIP request data.
181+
* @see <a href="https://docs.facturapi.io/api#tag/Invoices">API reference</a>
182+
*/
183+
public Map<String, Object> retrieveZipRequest(String id) {
184+
return get(
185+
"/invoices/zip-requests/" + id,
186+
null,
187+
new TypeReference<Map<String, Object>>() {}
188+
);
189+
}
190+
191+
/**
192+
* Downloads the ZIP generated for a request.
193+
*
194+
* @param id ZIP request id.
195+
* @return ZIP stream. Caller owns closing it.
196+
* @see <a href="https://docs.facturapi.io/api#tag/Invoices">API reference</a>
197+
*/
198+
public InputStream downloadZipRequestStream(String id) {
199+
return client.getStream("/invoices/zip-requests/" + id + "/zip");
200+
}
201+
145202
/**
146203
* Downloads the cancellation receipt XML file.
147204
*

src/test/java/io/facturapi/FacturapiHttpClientTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,23 @@ void throwsFacturapiExceptionWithApiMessage() {
7777
assertEquals("3", ex.getHeaders().get("Retry-After").get(0));
7878
assertEquals("log_123", ex.getHeaders().get("x-facturapi-log-id").get(0));
7979
}
80+
81+
@Test
82+
void convertsNumericApiErrorCodesToStrings() {
83+
StubHttpClient httpClient = new StubHttpClient();
84+
httpClient.enqueueJson(400, "{\"message\":\"Invalid customer\",\"code\":400}");
85+
86+
FacturapiHttpClient client = new FacturapiHttpClient(
87+
FacturapiConfig.builder("sk_test_123")
88+
.httpClient(httpClient.client())
89+
.build()
90+
);
91+
92+
FacturapiException ex = assertThrows(
93+
FacturapiException.class,
94+
() -> client.get("/customers/cus_1", null, GenericResponse.class)
95+
);
96+
97+
assertEquals("400", ex.getErrorCode());
98+
}
8099
}

src/test/java/io/facturapi/FacturapiResourcesTest.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
import io.facturapi.enums.Taxability;
1717
import io.facturapi.http.FacturapiConfig;
1818
import io.facturapi.models.Customer;
19+
import io.facturapi.models.InvoiceItem;
1920
import java.io.InputStream;
2021
import java.nio.charset.StandardCharsets;
2122
import java.time.Instant;
2223
import java.time.LocalDate;
24+
import java.util.List;
2325
import java.util.Map;
2426
import org.junit.jupiter.api.Test;
2527

@@ -206,6 +208,98 @@ void retentionDraftCancelAndListUseExpectedPaths() {
206208
assertEquals("/v2/retentions?status=draft", listRequest.uri().getPath() + "?" + listRequest.uri().getQuery());
207209
}
208210

211+
@Test
212+
void invoiceZipRequestCanBeCreated() {
213+
StubHttpClient httpClient = new StubHttpClient();
214+
httpClient.enqueueJson(200, "{\"id\":\"zip_1\",\"status\":\"pending\"}");
215+
216+
Facturapi sdk = new Facturapi(
217+
FacturapiConfig.builder("sk_test")
218+
.httpClient(httpClient.client())
219+
.build()
220+
);
221+
222+
var response = sdk.invoices().createZipRequest(
223+
Map.of(
224+
"year", 2025,
225+
"month", 3,
226+
"issuer_type", "issuing",
227+
"invoice_types", java.util.List.of("I", "E")
228+
)
229+
);
230+
231+
assertEquals("zip_1", response.get("id"));
232+
var request = httpClient.requests().get(0);
233+
assertEquals("POST", request.method());
234+
assertEquals("/v2/invoices/zip-requests", request.uri().getPath());
235+
assertTrue(request.bodyUtf8().contains("\"issuer_type\":\"issuing\""));
236+
}
237+
238+
@Test
239+
void invoiceZipRequestsCanBeListed() {
240+
StubHttpClient httpClient = new StubHttpClient();
241+
httpClient.enqueueJson(
242+
200,
243+
"{\"page\":1,\"total_pages\":1,\"total_results\":1,\"data\":[{\"id\":\"zip_1\"}]}"
244+
);
245+
246+
Facturapi sdk = new Facturapi(
247+
FacturapiConfig.builder("sk_test")
248+
.httpClient(httpClient.client())
249+
.build()
250+
);
251+
252+
var response = sdk.invoices().listZipRequests(
253+
Map.of("year", 2025, "month", 3, "status", "finished", "limit", 20, "page", 1)
254+
);
255+
256+
assertEquals("zip_1", response.getData().get(0).get("id"));
257+
var request = httpClient.requests().get(0);
258+
assertEquals("GET", request.method());
259+
assertEquals("/v2/invoices/zip-requests", request.uri().getPath());
260+
assertTrue(request.uri().getQuery().contains("year=2025"));
261+
assertTrue(request.uri().getQuery().contains("status=finished"));
262+
}
263+
264+
@Test
265+
void invoiceZipRequestCanBeRetrieved() {
266+
StubHttpClient httpClient = new StubHttpClient();
267+
httpClient.enqueueJson(200, "{\"id\":\"zip_1\",\"status\":\"finished\"}");
268+
269+
Facturapi sdk = new Facturapi(
270+
FacturapiConfig.builder("sk_test")
271+
.httpClient(httpClient.client())
272+
.build()
273+
);
274+
275+
var response = sdk.invoices().retrieveZipRequest("zip_1");
276+
277+
assertEquals("finished", response.get("status"));
278+
var request = httpClient.requests().get(0);
279+
assertEquals("GET", request.method());
280+
assertEquals("/v2/invoices/zip-requests/zip_1", request.uri().getPath());
281+
}
282+
283+
@Test
284+
void invoiceZipRequestCanBeDownloaded() throws Exception {
285+
StubHttpClient httpClient = new StubHttpClient();
286+
httpClient.enqueueBinary(200, "ZIP-CONTENT".getBytes(StandardCharsets.UTF_8), "application/zip");
287+
288+
Facturapi sdk = new Facturapi(
289+
FacturapiConfig.builder("sk_test")
290+
.httpClient(httpClient.client())
291+
.build()
292+
);
293+
294+
try (InputStream stream = sdk.invoices().downloadZipRequestStream("zip_1")) {
295+
assertEquals("ZIP-CONTENT", new String(stream.readAllBytes(), StandardCharsets.UTF_8));
296+
}
297+
298+
var request = httpClient.requests().get(0);
299+
assertEquals("GET", request.method());
300+
assertEquals("/v2/invoices/zip-requests/zip_1/zip", request.uri().getPath());
301+
}
302+
209303
@Test
210304
void organizationUploadsAcceptBytes() {
211305
StubHttpClient httpClient = new StubHttpClient();
@@ -414,4 +508,18 @@ void objectMapperDeserializesCodeEnums() throws Exception {
414508
assertEquals(TaxType.IEPS, tax.getType());
415509
assertEquals(TaxFactor.EXENTO, tax.getFactor());
416510
}
511+
512+
@Test
513+
void objectMapperDeserializesPropertyTaxAccountsAsArrays() throws Exception {
514+
var mapper = FacturapiConfig.builder("sk_test").build().getObjectMapper();
515+
516+
var empty = mapper.readValue("{\"property_tax_account\":[]}", InvoiceItem.class);
517+
var accounts = mapper.readValue(
518+
"{\"property_tax_account\":[\"0102030405\"]}",
519+
InvoiceItem.class
520+
);
521+
522+
assertEquals(List.of(), empty.getPropertyTaxAccounts());
523+
assertEquals(List.of("0102030405"), accounts.getPropertyTaxAccounts());
524+
}
417525
}

0 commit comments

Comments
 (0)