diff --git a/CHANGELOG.md b/CHANGELOG.md index c6dbb03..b0da10f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. + ## [6.6.0] - 2026-07-01 ### Added - Added retention draft methods: `UpdateDraftAsync`, `StampDraftAsync`, and `CopyToDraftAsync`. diff --git a/FacturapiTest/WrapperBehaviorTests.cs b/FacturapiTest/WrapperBehaviorTests.cs index b433691..bebf65d 100644 --- a/FacturapiTest/WrapperBehaviorTests.cs +++ b/FacturapiTest/WrapperBehaviorTests.cs @@ -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 + { + ["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\"}]}")); + }); + + var wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler)); + var result = await wrapper.ListZipRequestsAsync(new Dictionary + { + ["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() { diff --git a/Router/InvoiceRouter.cs b/Router/InvoiceRouter.cs index 658c01f..e1a4f2c 100644 --- a/Router/InvoiceRouter.cs +++ b/Router/InvoiceRouter.cs @@ -67,5 +67,25 @@ public static string PreviewPdf() { return "invoices/preview/pdf"; } + + public static string ListZipRequests(Dictionary 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"; + } } } diff --git a/Wrappers/IInvoiceWrapper.cs b/Wrappers/IInvoiceWrapper.cs index c38b2aa..3eafcf4 100644 --- a/Wrappers/IInvoiceWrapper.cs +++ b/Wrappers/IInvoiceWrapper.cs @@ -25,5 +25,9 @@ public interface IInvoiceWrapper Task StampDraft(string id, Dictionary options = null, CancellationToken cancellationToken = default); Task CopyToDraftAsync(string id, CancellationToken cancellationToken = default); Task PreviewPdfAsync(Dictionary data, CancellationToken cancellationToken = default); + Task> CreateZipRequestAsync(Dictionary data, CancellationToken cancellationToken = default); + Task>> ListZipRequestsAsync(Dictionary query = null, CancellationToken cancellationToken = default); + Task> RetrieveZipRequestAsync(string id, CancellationToken cancellationToken = default); + Task DownloadZipRequestAsync(string id, CancellationToken cancellationToken = default); } } diff --git a/Wrappers/InvoiceWrapper.cs b/Wrappers/InvoiceWrapper.cs index b9c891d..5e3d8c1 100644 --- a/Wrappers/InvoiceWrapper.cs +++ b/Wrappers/InvoiceWrapper.cs @@ -188,5 +188,49 @@ public async Task PreviewPdfAsync(Dictionary data, Cance return memory; } } + + public async Task> CreateZipRequestAsync(Dictionary 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>(resultString, this.jsonSettings); + } + } + + public async Task>> ListZipRequestsAsync(Dictionary 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>>(resultString, this.jsonSettings); + } + } + + public async Task> 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>(resultString, this.jsonSettings); + } + } + + public async Task 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; + } + } } } diff --git a/facturapi-net.csproj b/facturapi-net.csproj index a02772a..9ddde63 100644 --- a/facturapi-net.csproj +++ b/facturapi-net.csproj @@ -11,7 +11,7 @@ SDK oficial de Facturapi para .NET para facturación electrónica en México (CFDI), envío de documentos, búsqueda y trazabilidad. factura factura-electronica facturacion cfdi cfdi40 sat invoice invoicing facturapi mexico Facturapi - 6.6.0 + 6.7.0 $(Version) MIT false