Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ 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).

## [6.7.0] - 2026-08-12
### Added
- Added invoice ZIP request methods: `CreateZipRequestAsync`, `ListZipRequestsAsync`, `RetrieveZipRequestAsync`, and `DownloadZipRequestAsync`.

Comment on lines +8 to +11
## [6.6.0] - 2026-07-01
### Added
- Added retention draft methods: `UpdateDraftAsync`, `StampDraftAsync`, and `CopyToDraftAsync`.
Expand Down
94 changes: 94 additions & 0 deletions FacturapiTest/WrapperBehaviorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,100 @@ public async Task InvoiceDownloadPdfAsync_ReturnsSeekableStreamAtPositionZero()
Assert.Equal("pdf-bytes", text);
}

[Fact]
public async Task InvoiceCreateZipRequestAsync_UsesZipRequestsPostRoute()
{
var handler = new RecordingHandler(async (request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Post, request.Method);
Assert.NotNull(request.RequestUri);
Assert.Equal("/v2/invoices/zip-requests", request.RequestUri.PathAndQuery);
Assert.NotNull(request.Content);
var body = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Contains("\"year\":2025", body);
Assert.Contains("\"issuer_type\":\"issuing\"", body);
Assert.Contains("\"invoice_types\":[\"I\",\"E\"]", body);
return JsonResponse("{\"id\":\"zip_1\",\"status\":\"pending\"}");
});

var wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler));
var result = await wrapper.CreateZipRequestAsync(new Dictionary<string, object>
{
["year"] = 2025,
["month"] = 3,
["issuer_type"] = "issuing",
["invoice_types"] = new[] { "I", "E" }
});

Assert.Equal("zip_1", result["id"]?.ToString());
}

[Fact]
public async Task InvoiceListZipRequestsAsync_UsesZipRequestsQueryRoute()
{
var handler = new RecordingHandler((request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Get, request.Method);
Assert.NotNull(request.RequestUri);
Assert.Equal("/v2/invoices/zip-requests?year=2025&month=3&status=finished&limit=20&page=1", request.RequestUri.PathAndQuery);
return Task.FromResult(JsonResponse("{\"page\":1,\"total_pages\":1,\"total_results\":1,\"data\":[{\"id\":\"zip_1\"}]}"));
Comment on lines +525 to +528
});

var wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler));
var result = await wrapper.ListZipRequestsAsync(new Dictionary<string, object>
{
["year"] = 2025,
["month"] = 3,
["status"] = "finished",
["limit"] = 20,
["page"] = 1
});

Assert.Single(result.Data);
Assert.Equal("zip_1", result.Data[0]["id"]?.ToString());
}

[Fact]
public async Task InvoiceRetrieveZipRequestAsync_UsesZipRequestRoute()
{
var handler = new RecordingHandler((request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Get, request.Method);
Assert.NotNull(request.RequestUri);
Assert.Equal("/v2/invoices/zip-requests/zip_1", request.RequestUri.PathAndQuery);
return Task.FromResult(JsonResponse("{\"id\":\"zip_1\",\"status\":\"finished\"}"));
});

var wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler));
var result = await wrapper.RetrieveZipRequestAsync("zip_1");

Assert.Equal("finished", result["status"]?.ToString());
}

[Fact]
public async Task InvoiceDownloadZipRequestAsync_ReturnsSeekableStreamAtPositionZero()
{
var payload = Encoding.UTF8.GetBytes("zip-request-content");
var handler = new RecordingHandler((request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Get, request.Method);
Assert.NotNull(request.RequestUri);
Assert.Equal("/v2/invoices/zip-requests/zip_1/zip", request.RequestUri.PathAndQuery);
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent(payload)
});
});

var wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler));
using var stream = await wrapper.DownloadZipRequestAsync("zip_1");

Assert.Equal(0, stream.Position);
using var reader = new StreamReader(stream, Encoding.UTF8, false, 1024, leaveOpen: true);
var text = await reader.ReadToEndAsync();
Assert.Equal("zip-request-content", text);
}

[Fact]
public async Task RetentionDownloadZipAsync_ReturnsSeekableStreamAtPositionZero()
{
Expand Down
20 changes: 20 additions & 0 deletions Router/InvoiceRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,25 @@ public static string PreviewPdf()
{
return "invoices/preview/pdf";
}

public static string ListZipRequests(Dictionary<string, object> query = null)
{
return UriWithQuery("invoices/zip-requests", query);
}

public static string CreateZipRequest()
{
return "invoices/zip-requests";
}

public static string RetrieveZipRequest(string id)
{
return $"invoices/zip-requests/{id}";
}

public static string DownloadZipRequest(string id)
{
return $"invoices/zip-requests/{id}/zip";
}
}
}
4 changes: 4 additions & 0 deletions Wrappers/IInvoiceWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,9 @@ public interface IInvoiceWrapper
Task<Invoice> StampDraft(string id, Dictionary<string, object> options = null, CancellationToken cancellationToken = default);
Task<Invoice> CopyToDraftAsync(string id, CancellationToken cancellationToken = default);
Task<Stream> PreviewPdfAsync(Dictionary<string, object> data, CancellationToken cancellationToken = default);
Task<Dictionary<string, object>> CreateZipRequestAsync(Dictionary<string, object> data, CancellationToken cancellationToken = default);
Task<SearchResult<Dictionary<string, object>>> ListZipRequestsAsync(Dictionary<string, object> query = null, CancellationToken cancellationToken = default);
Task<Dictionary<string, object>> RetrieveZipRequestAsync(string id, CancellationToken cancellationToken = default);
Task<Stream> DownloadZipRequestAsync(string id, CancellationToken cancellationToken = default);
}
}
44 changes: 44 additions & 0 deletions Wrappers/InvoiceWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,49 @@ public async Task<Stream> PreviewPdfAsync(Dictionary<string, object> data, Cance
return memory;
}
}

public async Task<Dictionary<string, object>> CreateZipRequestAsync(Dictionary<string, object> data, CancellationToken cancellationToken = default)
{
using (var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"))
using (var response = await client.PostAsync(Router.CreateZipRequest(), content, cancellationToken).ConfigureAwait(false))
{
await this.ThrowIfErrorAsync(response, cancellationToken).ConfigureAwait(false);
var resultString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resultString, this.jsonSettings);
}
}

public async Task<SearchResult<Dictionary<string, object>>> ListZipRequestsAsync(Dictionary<string, object> query = null, CancellationToken cancellationToken = default)
{
using (var response = await client.GetAsync(Router.ListZipRequests(query), cancellationToken).ConfigureAwait(false))
{
await this.ThrowIfErrorAsync(response, cancellationToken).ConfigureAwait(false);
var resultString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonConvert.DeserializeObject<SearchResult<Dictionary<string, object>>>(resultString, this.jsonSettings);
}
}

public async Task<Dictionary<string, object>> RetrieveZipRequestAsync(string id, CancellationToken cancellationToken = default)
{
using (var response = await client.GetAsync(Router.RetrieveZipRequest(id), cancellationToken).ConfigureAwait(false))
{
await this.ThrowIfErrorAsync(response, cancellationToken).ConfigureAwait(false);
var resultString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonConvert.DeserializeObject<Dictionary<string, object>>(resultString, this.jsonSettings);
}
}

public async Task<Stream> DownloadZipRequestAsync(string id, CancellationToken cancellationToken = default)
{
using (var response = await client.GetAsync(Router.DownloadZipRequest(id), cancellationToken).ConfigureAwait(false))
{
await this.ThrowIfErrorAsync(response, cancellationToken).ConfigureAwait(false);
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var memory = new MemoryStream();
await responseStream.CopyToAsync(memory, 81920, cancellationToken).ConfigureAwait(false);
memory.Position = 0;
return memory;
}
}
}
}
2 changes: 1 addition & 1 deletion facturapi-net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<Summary>SDK oficial de Facturapi para .NET para facturación electrónica en México (CFDI), envío de documentos, búsqueda y trazabilidad.</Summary>
<PackageTags>factura factura-electronica facturacion cfdi cfdi40 sat invoice invoicing facturapi mexico</PackageTags>
<Title>Facturapi</Title>
<Version>6.6.0</Version>
<Version>6.7.0</Version>
<PackageVersion>$(Version)</PackageVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
Expand Down
Loading