From 7273998cca63945c075bc5e9854eab665186ad7a Mon Sep 17 00:00:00 2001 From: "raul@facturapi.io" Date: Wed, 12 Aug 2026 09:36:25 -0600 Subject: [PATCH 1/9] chore(invoices): add ZIP request methods --- CHANGELOG.md | 4 ++ FacturapiTest/WrapperBehaviorTests.cs | 94 +++++++++++++++++++++++++++ Router/InvoiceRouter.cs | 20 ++++++ Wrappers/IInvoiceWrapper.cs | 4 ++ Wrappers/InvoiceWrapper.cs | 44 +++++++++++++ facturapi-net.csproj | 2 +- 6 files changed, 167 insertions(+), 1 deletion(-) 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 From 0093beda5008f923f9037a85861560c31d645990 Mon Sep 17 00:00:00 2001 From: javorosas Date: Fri, 21 Aug 2026 18:11:45 +0200 Subject: [PATCH 2/9] chore: keep ZIP request methods unreleased --- CHANGELOG.md | 2 +- facturapi-net.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0da10f..1273917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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 +## Unreleased ### Added - Added invoice ZIP request methods: `CreateZipRequestAsync`, `ListZipRequestsAsync`, `RetrieveZipRequestAsync`, and `DownloadZipRequestAsync`. diff --git a/facturapi-net.csproj b/facturapi-net.csproj index 9ddde63..a02772a 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.7.0 + 6.6.0 $(Version) MIT false From a58dc9fc12c49b1baab704bfcd0fdb5ecfe4556b Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 24 Aug 2026 15:50:25 +0200 Subject: [PATCH 3/9] test: avoid query parameter ordering in ZIP request test --- FacturapiTest/WrapperBehaviorTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/FacturapiTest/WrapperBehaviorTests.cs b/FacturapiTest/WrapperBehaviorTests.cs index bebf65d..9295ae5 100644 --- a/FacturapiTest/WrapperBehaviorTests.cs +++ b/FacturapiTest/WrapperBehaviorTests.cs @@ -524,7 +524,11 @@ public async Task InvoiceListZipRequestsAsync_UsesZipRequestsQueryRoute() { 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); + Assert.Equal("/v2/invoices/zip-requests", request.RequestUri.AbsolutePath); + Assert.Equal( + new[] { "limit=20", "month=3", "page=1", "status=finished", "year=2025" }, + request.RequestUri.Query.TrimStart('?').Split('&').OrderBy(parameter => parameter) + ); return Task.FromResult(JsonResponse("{\"page\":1,\"total_pages\":1,\"total_results\":1,\"data\":[{\"id\":\"zip_1\"}]}")); }); From 9c4cbe3343c0b43c14292c1707b79fba20a6760b Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 24 Aug 2026 16:02:19 +0200 Subject: [PATCH 4/9] fix: keep invoice wrapper interface stable --- CHANGELOG.md | 4 +++ FacturapiTest/WrapperBehaviorTests.cs | 2 +- InvoiceZipRequestExtensions.cs | 42 +++++++++++++++++++++++++++ README.md | 2 ++ Wrappers/IInvoiceWrapper.cs | 4 --- Wrappers/IInvoiceZipRequestWrapper.cs | 15 ++++++++++ Wrappers/InvoiceWrapper.cs | 2 +- 7 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 InvoiceZipRequestExtensions.cs create mode 100644 Wrappers/IInvoiceZipRequestWrapper.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1273917..0ee4695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,13 @@ 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). ## Unreleased + ### Added - Added invoice ZIP request methods: `CreateZipRequestAsync`, `ListZipRequestsAsync`, `RetrieveZipRequestAsync`, and `DownloadZipRequestAsync`. +### Fixed +- Keep `IInvoiceWrapper` stable when adding ZIP request methods. + ## [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 9295ae5..173d2f9 100644 --- a/FacturapiTest/WrapperBehaviorTests.cs +++ b/FacturapiTest/WrapperBehaviorTests.cs @@ -505,7 +505,7 @@ public async Task InvoiceCreateZipRequestAsync_UsesZipRequestsPostRoute() return JsonResponse("{\"id\":\"zip_1\",\"status\":\"pending\"}"); }); - var wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler)); + IInvoiceWrapper wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler)); var result = await wrapper.CreateZipRequestAsync(new Dictionary { ["year"] = 2025, diff --git a/InvoiceZipRequestExtensions.cs b/InvoiceZipRequestExtensions.cs new file mode 100644 index 0000000..7fd42dd --- /dev/null +++ b/InvoiceZipRequestExtensions.cs @@ -0,0 +1,42 @@ +using Facturapi.Wrappers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi +{ + public static class InvoiceZipRequestExtensions + { + public static Task> CreateZipRequestAsync(this IInvoiceWrapper invoice, Dictionary data, CancellationToken cancellationToken = default) + { + return GetZipRequestWrapper(invoice).CreateZipRequestAsync(data, cancellationToken); + } + + public static Task>> ListZipRequestsAsync(this IInvoiceWrapper invoice, Dictionary query = null, CancellationToken cancellationToken = default) + { + return GetZipRequestWrapper(invoice).ListZipRequestsAsync(query, cancellationToken); + } + + public static Task> RetrieveZipRequestAsync(this IInvoiceWrapper invoice, string id, CancellationToken cancellationToken = default) + { + return GetZipRequestWrapper(invoice).RetrieveZipRequestAsync(id, cancellationToken); + } + + public static Task DownloadZipRequestAsync(this IInvoiceWrapper invoice, string id, CancellationToken cancellationToken = default) + { + return GetZipRequestWrapper(invoice).DownloadZipRequestAsync(id, cancellationToken); + } + + private static IInvoiceZipRequestWrapper GetZipRequestWrapper(IInvoiceWrapper invoice) + { + if (invoice is IInvoiceZipRequestWrapper zipRequestWrapper) + { + return zipRequestWrapper; + } + + throw new NotSupportedException("The invoice wrapper does not support ZIP requests."); + } + } +} diff --git a/README.md b/README.md index e8303db..a18c8d7 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ Después (v6): ICustomerWrapper customers = client.Customer; ``` +Las interfaces de wrappers se mantienen estables para pruebas y mocks. Las capacidades opcionales se exponen en interfaces adicionales; por ejemplo, un mock que cubra solicitudes ZIP debe implementar `IInvoiceWrapper` e `IInvoiceZipRequestWrapper`. + ## Instalación Puedes instalar Facturapi en tu proyecto usando [Nuget](https://www.nuget.org/) diff --git a/Wrappers/IInvoiceWrapper.cs b/Wrappers/IInvoiceWrapper.cs index 3eafcf4..c38b2aa 100644 --- a/Wrappers/IInvoiceWrapper.cs +++ b/Wrappers/IInvoiceWrapper.cs @@ -25,9 +25,5 @@ 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/IInvoiceZipRequestWrapper.cs b/Wrappers/IInvoiceZipRequestWrapper.cs new file mode 100644 index 0000000..0da1f5a --- /dev/null +++ b/Wrappers/IInvoiceZipRequestWrapper.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface IInvoiceZipRequestWrapper + { + 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 5e3d8c1..7babe47 100644 --- a/Wrappers/InvoiceWrapper.cs +++ b/Wrappers/InvoiceWrapper.cs @@ -9,7 +9,7 @@ namespace Facturapi.Wrappers { - public class InvoiceWrapper : BaseWrapper, IInvoiceWrapper + public class InvoiceWrapper : BaseWrapper, IInvoiceWrapper, IInvoiceZipRequestWrapper { internal InvoiceWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { From 76081f92e5f2c2f43e7bf52d070966614e46e953 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 24 Aug 2026 16:08:48 +0200 Subject: [PATCH 5/9] fix: expose concrete resource wrappers --- CHANGELOG.md | 4 +- FacturapiClient.cs | 22 +++++----- FacturapiTest/ClientCompatibilityTests.cs | 17 ++++++++ FacturapiTest/WrapperBehaviorTests.cs | 2 +- IFacturapiClient.cs | 19 --------- InvoiceZipRequestExtensions.cs | 42 ------------------- README.md | 32 +------------- Wrappers/CartaporteCatalogWrapper.cs | 2 +- Wrappers/CatalogWrapper.cs | 2 +- Wrappers/CustomerWrapper.cs | 2 +- Wrappers/ICartaporteCatalogWrapper.cs | 20 --------- Wrappers/ICatalogWrapper.cs | 12 ------ Wrappers/ICustomerWrapper.cs | 17 -------- Wrappers/IInvoiceWrapper.cs | 29 ------------- Wrappers/IInvoiceZipRequestWrapper.cs | 15 ------- Wrappers/IOrganizationWrapper.cs | 51 ----------------------- Wrappers/IProductWrapper.cs | 15 ------- Wrappers/IReceiptWrapper.cs | 21 ---------- Wrappers/IRetentionWrapper.cs | 22 ---------- Wrappers/IToolWrapper.cs | 11 ----- Wrappers/IWebhookWrapper.cs | 16 ------- Wrappers/InvoiceWrapper.cs | 2 +- Wrappers/OrganizationWrapper.cs | 2 +- Wrappers/ProductWrapper.cs | 2 +- Wrappers/ReceiptWrapper.cs | 2 +- Wrappers/RetentionWrapper.cs | 2 +- Wrappers/ToolWrapper.cs | 2 +- Wrappers/WebhookWrapper.cs | 2 +- 28 files changed, 43 insertions(+), 344 deletions(-) delete mode 100644 IFacturapiClient.cs delete mode 100644 InvoiceZipRequestExtensions.cs delete mode 100644 Wrappers/ICartaporteCatalogWrapper.cs delete mode 100644 Wrappers/ICatalogWrapper.cs delete mode 100644 Wrappers/ICustomerWrapper.cs delete mode 100644 Wrappers/IInvoiceWrapper.cs delete mode 100644 Wrappers/IInvoiceZipRequestWrapper.cs delete mode 100644 Wrappers/IOrganizationWrapper.cs delete mode 100644 Wrappers/IProductWrapper.cs delete mode 100644 Wrappers/IReceiptWrapper.cs delete mode 100644 Wrappers/IRetentionWrapper.cs delete mode 100644 Wrappers/IToolWrapper.cs delete mode 100644 Wrappers/IWebhookWrapper.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee4695..e39e00d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added invoice ZIP request methods: `CreateZipRequestAsync`, `ListZipRequestsAsync`, `RetrieveZipRequestAsync`, and `DownloadZipRequestAsync`. -### Fixed -- Keep `IInvoiceWrapper` stable when adding ZIP request methods. +### Changed +- Expose concrete resource wrappers from `FacturapiClient`; use an injected `HttpClient` or an application-owned abstraction for tests. ## [6.6.0] - 2026-07-01 ### Added diff --git a/FacturapiClient.cs b/FacturapiClient.cs index a97eaeb..3486d0f 100644 --- a/FacturapiClient.cs +++ b/FacturapiClient.cs @@ -6,18 +6,18 @@ namespace Facturapi { - public sealed class FacturapiClient : IFacturapiClient + public sealed class FacturapiClient : IDisposable { - public ICustomerWrapper Customer { get; private set; } - public IProductWrapper Product { get; private set; } - public IInvoiceWrapper Invoice { get; private set; } - public IOrganizationWrapper Organization { get; private set; } - public IReceiptWrapper Receipt { get; private set; } - public IRetentionWrapper Retention { get; private set; } - public ICatalogWrapper Catalog { get; private set; } - public ICartaporteCatalogWrapper CartaporteCatalog { get; private set; } - public IToolWrapper Tool { get; private set; } - public IWebhookWrapper Webhook { get; private set; } + public CustomerWrapper Customer { get; private set; } + public ProductWrapper Product { get; private set; } + public InvoiceWrapper Invoice { get; private set; } + public OrganizationWrapper Organization { get; private set; } + public ReceiptWrapper Receipt { get; private set; } + public RetentionWrapper Retention { get; private set; } + public CatalogWrapper Catalog { get; private set; } + public CartaporteCatalogWrapper CartaporteCatalog { get; private set; } + public ToolWrapper Tool { get; private set; } + public WebhookWrapper Webhook { get; private set; } private readonly HttpClient httpClient; private readonly bool ownsHttpClient; private bool disposed; diff --git a/FacturapiTest/ClientCompatibilityTests.cs b/FacturapiTest/ClientCompatibilityTests.cs index c3a167a..6fe39a3 100644 --- a/FacturapiTest/ClientCompatibilityTests.cs +++ b/FacturapiTest/ClientCompatibilityTests.cs @@ -13,6 +13,23 @@ namespace FacturapiTest { public class ClientCompatibilityTests { + [Fact] + public void Client_ExposesConcreteResourceWrappers() + { + using var client = new FacturapiClient("test_key"); + + Assert.IsType(client.Customer); + Assert.IsType(client.Product); + Assert.IsType(client.Invoice); + Assert.IsType(client.Organization); + Assert.IsType(client.Receipt); + Assert.IsType(client.Retention); + Assert.IsType(client.Catalog); + Assert.IsType(client.CartaporteCatalog); + Assert.IsType(client.Tool); + Assert.IsType(client.Webhook); + } + [Fact] public void Router_ListCustomers_AllowsNullQueryValues() { diff --git a/FacturapiTest/WrapperBehaviorTests.cs b/FacturapiTest/WrapperBehaviorTests.cs index 173d2f9..9295ae5 100644 --- a/FacturapiTest/WrapperBehaviorTests.cs +++ b/FacturapiTest/WrapperBehaviorTests.cs @@ -505,7 +505,7 @@ public async Task InvoiceCreateZipRequestAsync_UsesZipRequestsPostRoute() return JsonResponse("{\"id\":\"zip_1\",\"status\":\"pending\"}"); }); - IInvoiceWrapper wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler)); + var wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler)); var result = await wrapper.CreateZipRequestAsync(new Dictionary { ["year"] = 2025, diff --git a/IFacturapiClient.cs b/IFacturapiClient.cs deleted file mode 100644 index 0217b28..0000000 --- a/IFacturapiClient.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Facturapi.Wrappers; -using System; - -namespace Facturapi -{ - public interface IFacturapiClient : IDisposable - { - ICustomerWrapper Customer { get; } - IProductWrapper Product { get; } - IInvoiceWrapper Invoice { get; } - IOrganizationWrapper Organization { get; } - IReceiptWrapper Receipt { get; } - IRetentionWrapper Retention { get; } - ICatalogWrapper Catalog { get; } - ICartaporteCatalogWrapper CartaporteCatalog { get; } - IToolWrapper Tool { get; } - IWebhookWrapper Webhook { get; } - } -} diff --git a/InvoiceZipRequestExtensions.cs b/InvoiceZipRequestExtensions.cs deleted file mode 100644 index 7fd42dd..0000000 --- a/InvoiceZipRequestExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Facturapi.Wrappers; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi -{ - public static class InvoiceZipRequestExtensions - { - public static Task> CreateZipRequestAsync(this IInvoiceWrapper invoice, Dictionary data, CancellationToken cancellationToken = default) - { - return GetZipRequestWrapper(invoice).CreateZipRequestAsync(data, cancellationToken); - } - - public static Task>> ListZipRequestsAsync(this IInvoiceWrapper invoice, Dictionary query = null, CancellationToken cancellationToken = default) - { - return GetZipRequestWrapper(invoice).ListZipRequestsAsync(query, cancellationToken); - } - - public static Task> RetrieveZipRequestAsync(this IInvoiceWrapper invoice, string id, CancellationToken cancellationToken = default) - { - return GetZipRequestWrapper(invoice).RetrieveZipRequestAsync(id, cancellationToken); - } - - public static Task DownloadZipRequestAsync(this IInvoiceWrapper invoice, string id, CancellationToken cancellationToken = default) - { - return GetZipRequestWrapper(invoice).DownloadZipRequestAsync(id, cancellationToken); - } - - private static IInvoiceZipRequestWrapper GetZipRequestWrapper(IInvoiceWrapper invoice) - { - if (invoice is IInvoiceZipRequestWrapper zipRequestWrapper) - { - return zipRequestWrapper; - } - - throw new NotSupportedException("The invoice wrapper does not support ZIP requests."); - } - } -} diff --git a/README.md b/README.md index a18c8d7..0841fbf 100644 --- a/README.md +++ b/README.md @@ -11,36 +11,6 @@ Facturapi ayuda a generar facturas electrónicas válidas en México (CFDI) de l Si alguna vez has usado [Stripe](https://stripe.com) o [Conekta](https://conekta.io), verás que Facturapi es igual de sencillo de entender e integrar a tu aplicación. -## Migración a v6 - -### ¿Cuándo NO necesitas cambiar nada? - -No necesitas actualizar tu código si: -- Creas `FacturapiClient` y llamas métodos directamente (por ejemplo `await client.Invoice.CreateAsync(...)`). -- Usas `var` al guardar wrappers (por ejemplo `var invoices = client.Invoice;`). -- No dependes de tipos concretos de wrappers en firmas, propiedades o pruebas. - -### ¿Cuándo SÍ necesitas actualizar? - -Debes ajustar tu código si: -- Declaras wrappers como clases concretas (`CustomerWrapper`, `InvoiceWrapper`, etc.). -- Mockeas wrappers concretos en pruebas. -- Expones wrappers concretos en tus propias interfaces o APIs públicas. - -Antes (v5): - -```csharp -CustomerWrapper customers = client.Customer; -``` - -Después (v6): - -```csharp -ICustomerWrapper customers = client.Customer; -``` - -Las interfaces de wrappers se mantienen estables para pruebas y mocks. Las capacidades opcionales se exponen en interfaces adicionales; por ejemplo, un mock que cubra solicitudes ZIP debe implementar `IInvoiceWrapper` e `IInvoiceZipRequestWrapper`. - ## Instalación Puedes instalar Facturapi en tu proyecto usando [Nuget](https://www.nuget.org/) @@ -79,6 +49,8 @@ var customHttpClient = new HttpClient(); var facturapi = FacturapiClient.CreateWithCustomHttpClient("TU_API_KEY", customHttpClient); ``` +Para pruebas, usa este factory con un `HttpMessageHandler` propio que simule las respuestas de la API. Si tu aplicación necesita abstraer Facturapi, define una interfaz en tu propia capa de integración. + ### Métodos asíncronos (async, await) Esta librería utiliza métodos asíncronos. Si tu aplicación no tiene código asíncrono, puedes convertir un método asíncrono en síncrono de la siguiente manera: diff --git a/Wrappers/CartaporteCatalogWrapper.cs b/Wrappers/CartaporteCatalogWrapper.cs index f354a4c..7b5a2c1 100644 --- a/Wrappers/CartaporteCatalogWrapper.cs +++ b/Wrappers/CartaporteCatalogWrapper.cs @@ -6,7 +6,7 @@ namespace Facturapi.Wrappers { - public class CartaporteCatalogWrapper : BaseWrapper, ICartaporteCatalogWrapper + public class CartaporteCatalogWrapper : BaseWrapper { internal CartaporteCatalogWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/CatalogWrapper.cs b/Wrappers/CatalogWrapper.cs index ed21955..0997f33 100644 --- a/Wrappers/CatalogWrapper.cs +++ b/Wrappers/CatalogWrapper.cs @@ -6,7 +6,7 @@ namespace Facturapi.Wrappers { - public class CatalogWrapper : BaseWrapper, ICatalogWrapper + public class CatalogWrapper : BaseWrapper { internal CatalogWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/CustomerWrapper.cs b/Wrappers/CustomerWrapper.cs index ab7e64a..6cc9f70 100644 --- a/Wrappers/CustomerWrapper.cs +++ b/Wrappers/CustomerWrapper.cs @@ -7,7 +7,7 @@ namespace Facturapi.Wrappers { - public class CustomerWrapper : BaseWrapper, ICustomerWrapper + public class CustomerWrapper : BaseWrapper { internal CustomerWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/ICartaporteCatalogWrapper.cs b/Wrappers/ICartaporteCatalogWrapper.cs deleted file mode 100644 index 3beb615..0000000 --- a/Wrappers/ICartaporteCatalogWrapper.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface ICartaporteCatalogWrapper - { - Task> SearchAirTransportCodes(Dictionary query = null, CancellationToken cancellationToken = default); - Task> SearchTransportConfigs(Dictionary query = null, CancellationToken cancellationToken = default); - Task> SearchRightsOfPassage(Dictionary query = null, CancellationToken cancellationToken = default); - Task> SearchCustomsDocuments(Dictionary query = null, CancellationToken cancellationToken = default); - Task> SearchPackagingTypes(Dictionary query = null, CancellationToken cancellationToken = default); - Task> SearchTrailerTypes(Dictionary query = null, CancellationToken cancellationToken = default); - Task> SearchHazardousMaterials(Dictionary query = null, CancellationToken cancellationToken = default); - Task> SearchNavalAuthorizations(Dictionary query = null, CancellationToken cancellationToken = default); - Task> SearchPortStations(Dictionary query = null, CancellationToken cancellationToken = default); - Task> SearchMarineContainers(Dictionary query = null, CancellationToken cancellationToken = default); - } -} diff --git a/Wrappers/ICatalogWrapper.cs b/Wrappers/ICatalogWrapper.cs deleted file mode 100644 index 80a41b3..0000000 --- a/Wrappers/ICatalogWrapper.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface ICatalogWrapper - { - Task> SearchProducts(Dictionary query = null, CancellationToken cancellationToken = default); - Task> SearchUnits(Dictionary query = null, CancellationToken cancellationToken = default); - } -} diff --git a/Wrappers/ICustomerWrapper.cs b/Wrappers/ICustomerWrapper.cs deleted file mode 100644 index 9006fd1..0000000 --- a/Wrappers/ICustomerWrapper.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface ICustomerWrapper - { - Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); - Task CreateAsync(Dictionary data, Dictionary queryParams = null, CancellationToken cancellationToken = default); - Task RetrieveAsync(string id, CancellationToken cancellationToken = default); - Task DeleteAsync(string id, CancellationToken cancellationToken = default); - Task UpdateAsync(string id, Dictionary data, Dictionary queryParams = null, CancellationToken cancellationToken = default); - Task ValidateTaxInfoAsync(string id, CancellationToken cancellationToken = default); - Task SendEditLinkByEmailAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - } -} diff --git a/Wrappers/IInvoiceWrapper.cs b/Wrappers/IInvoiceWrapper.cs deleted file mode 100644 index c38b2aa..0000000 --- a/Wrappers/IInvoiceWrapper.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface IInvoiceWrapper - { - Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); - Task CreateAsync(Dictionary data, Dictionary options = null, CancellationToken cancellationToken = default); - Task RetrieveAsync(string id, CancellationToken cancellationToken = default); - Task CancelAsync(string id, Dictionary query = null, CancellationToken cancellationToken = default); - Task SendByEmailAsync(string id, Dictionary data = null, CancellationToken cancellationToken = default); - Task DownloadZipAsync(string id, CancellationToken cancellationToken = default); - Task DownloadPdfAsync(string id, CancellationToken cancellationToken = default); - Task DownloadXmlAsync(string id, CancellationToken cancellationToken = default); - Task DownloadCancellationReceiptXmlAsync(string id, CancellationToken cancellationToken = default); - Task DownloadCancellationReceiptPdfAsync(string id, CancellationToken cancellationToken = default); - Task UpdateStatusAsync(string id, CancellationToken cancellationToken = default); - Task UpdateDraftAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - Task StampDraftAsync(string id, Dictionary options = null, CancellationToken cancellationToken = default); - [Obsolete("Use StampDraftAsync instead.")] - Task StampDraft(string id, Dictionary options = null, CancellationToken cancellationToken = default); - Task CopyToDraftAsync(string id, CancellationToken cancellationToken = default); - Task PreviewPdfAsync(Dictionary data, CancellationToken cancellationToken = default); - } -} diff --git a/Wrappers/IInvoiceZipRequestWrapper.cs b/Wrappers/IInvoiceZipRequestWrapper.cs deleted file mode 100644 index 0da1f5a..0000000 --- a/Wrappers/IInvoiceZipRequestWrapper.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface IInvoiceZipRequestWrapper - { - 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/IOrganizationWrapper.cs b/Wrappers/IOrganizationWrapper.cs deleted file mode 100644 index 07c7c75..0000000 --- a/Wrappers/IOrganizationWrapper.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface IOrganizationWrapper - { - Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); - Task MeAsync(CancellationToken cancellationToken = default); - Task CheckDomainIsAvailableAsync(string domain, CancellationToken cancellationToken = default); - Task CreateAsync(Dictionary data, CancellationToken cancellationToken = default); - Task RetrieveAsync(string id, CancellationToken cancellationToken = default); - Task DeleteAsync(string id, CancellationToken cancellationToken = default); - Task UploadLogoAsync(string id, Stream file, CancellationToken cancellationToken = default); - Task UploadCertificateAsync(string id, Stream cerFile, Stream keyFile, string password, CancellationToken cancellationToken = default); - Task DeleteCertificateAsync(string id, CancellationToken cancellationToken = default); - Task UpdateLegalAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - Task UpdateReceiptSettingsAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - Task UpdateCustomizationAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - Task UpdateDomainAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - Task GetTestApiKeyAsync(string id, CancellationToken cancellationToken = default); - Task RenewTestApiKeyAsync(string id, CancellationToken cancellationToken = default); - Task ListLiveApiKeysAsync(string id, CancellationToken cancellationToken = default); - Task RenewLiveApiKeyAsync(string id, CancellationToken cancellationToken = default); - Task> ListSeriesAsync(string id, CancellationToken cancellationToken = default); - Task CreateSeriesAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - Task UpdateSeriesAsync(string id, string seriesName, Dictionary data, CancellationToken cancellationToken = default); - Task DeleteSeriesAsync(string id, string seriesName, CancellationToken cancellationToken = default); - Task UpdateDefaultSeriesAsync(string organizationId, Dictionary data, CancellationToken cancellationToken = default); - Task> DeleteLiveApiKeyAsync(string id, string apiKeyId, CancellationToken cancellationToken = default); - Task UpdateSelfInvoiceSettingsAsync(string organizationId, Dictionary data, CancellationToken cancellationToken = default); - Task> ListTeamAccessAsync(string organizationId, CancellationToken cancellationToken = default); - Task RetrieveTeamAccessAsync(string organizationId, string accessId, CancellationToken cancellationToken = default); - Task UpdateTeamAccessRoleAsync(string organizationId, string accessId, string role, CancellationToken cancellationToken = default); - Task RemoveTeamAccessAsync(string organizationId, string accessId, CancellationToken cancellationToken = default); - Task> ListSentTeamInvitesAsync(string organizationId, CancellationToken cancellationToken = default); - Task InviteUserToTeamAsync(string organizationId, Dictionary data, CancellationToken cancellationToken = default); - Task CancelTeamInviteAsync(string organizationId, string inviteKey, CancellationToken cancellationToken = default); - Task> ListReceivedTeamInvitesAsync(CancellationToken cancellationToken = default); - Task RespondTeamInviteAsync(string inviteKey, Dictionary data, CancellationToken cancellationToken = default); - Task> ListTeamRolesAsync(string organizationId, CancellationToken cancellationToken = default); - Task> ListTeamRoleTemplatesAsync(string organizationId, CancellationToken cancellationToken = default); - Task> ListTeamRoleOperationsAsync(string organizationId, CancellationToken cancellationToken = default); - Task RetrieveTeamRoleAsync(string organizationId, string roleId, CancellationToken cancellationToken = default); - Task CreateTeamRoleAsync(string organizationId, Dictionary data, CancellationToken cancellationToken = default); - Task UpdateTeamRoleAsync(string organizationId, string roleId, Dictionary data, CancellationToken cancellationToken = default); - Task DeleteTeamRoleAsync(string organizationId, string roleId, CancellationToken cancellationToken = default); - } -} diff --git a/Wrappers/IProductWrapper.cs b/Wrappers/IProductWrapper.cs deleted file mode 100644 index 006a19a..0000000 --- a/Wrappers/IProductWrapper.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface IProductWrapper - { - Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); - Task CreateAsync(Dictionary data, CancellationToken cancellationToken = default); - Task RetrieveAsync(string id, CancellationToken cancellationToken = default); - Task DeleteAsync(string id, CancellationToken cancellationToken = default); - Task UpdateAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - } -} diff --git a/Wrappers/IReceiptWrapper.cs b/Wrappers/IReceiptWrapper.cs deleted file mode 100644 index 085b480..0000000 --- a/Wrappers/IReceiptWrapper.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface IReceiptWrapper - { - Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); - Task CreateAsync(Dictionary data, CancellationToken cancellationToken = default); - Task RetrieveAsync(string id, CancellationToken cancellationToken = default); - Task CancelAsync(string id, CancellationToken cancellationToken = default); - Task InvoiceAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - Task CreateGlobalInvoiceAsync(Dictionary data, CancellationToken cancellationToken = default); - Task ToInvoiceAsync(Dictionary data, CancellationToken cancellationToken = default); - Task PreviewToInvoicePdfAsync(Dictionary data, CancellationToken cancellationToken = default); - Task SendByEmailAsync(string id, Dictionary data = null, CancellationToken cancellationToken = default); - Task DownloadPdfAsync(string id, CancellationToken cancellationToken = default); - } -} diff --git a/Wrappers/IRetentionWrapper.cs b/Wrappers/IRetentionWrapper.cs deleted file mode 100644 index 31d05c7..0000000 --- a/Wrappers/IRetentionWrapper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface IRetentionWrapper - { - Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); - Task CreateAsync(Dictionary data, CancellationToken cancellationToken = default); - Task RetrieveAsync(string id, CancellationToken cancellationToken = default); - Task CancelAsync(string id, Dictionary query = null, CancellationToken cancellationToken = default); - Task SendByEmailAsync(string id, Dictionary data = null, CancellationToken cancellationToken = default); - Task UpdateDraftAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - Task StampDraftAsync(string id, Dictionary options = null, CancellationToken cancellationToken = default); - Task CopyToDraftAsync(string id, CancellationToken cancellationToken = default); - Task DownloadZipAsync(string id, CancellationToken cancellationToken = default); - Task DownloadPdfAsync(string id, CancellationToken cancellationToken = default); - Task DownloadXmlAsync(string id, CancellationToken cancellationToken = default); - } -} diff --git a/Wrappers/IToolWrapper.cs b/Wrappers/IToolWrapper.cs deleted file mode 100644 index 5ada938..0000000 --- a/Wrappers/IToolWrapper.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface IToolWrapper - { - Task ValidateTaxIdAsync(string taxId, CancellationToken cancellationToken = default); - Task HealthCheckAsync(CancellationToken cancellationToken = default); - } -} diff --git a/Wrappers/IWebhookWrapper.cs b/Wrappers/IWebhookWrapper.cs deleted file mode 100644 index 83db0c0..0000000 --- a/Wrappers/IWebhookWrapper.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface IWebhookWrapper - { - Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); - Task CreateAsync(Dictionary data, CancellationToken cancellationToken = default); - Task RetrieveAsync(string id, CancellationToken cancellationToken = default); - Task UpdateAsync(string id, Dictionary data, CancellationToken cancellationToken = default); - Task DeleteAsync(string id, CancellationToken cancellationToken = default); - Task ValidateSignatureAsync(Dictionary data, CancellationToken cancellationToken = default); - } -} diff --git a/Wrappers/InvoiceWrapper.cs b/Wrappers/InvoiceWrapper.cs index 7babe47..f441bff 100644 --- a/Wrappers/InvoiceWrapper.cs +++ b/Wrappers/InvoiceWrapper.cs @@ -9,7 +9,7 @@ namespace Facturapi.Wrappers { - public class InvoiceWrapper : BaseWrapper, IInvoiceWrapper, IInvoiceZipRequestWrapper + public class InvoiceWrapper : BaseWrapper { internal InvoiceWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/OrganizationWrapper.cs b/Wrappers/OrganizationWrapper.cs index e55ebd2..5597297 100644 --- a/Wrappers/OrganizationWrapper.cs +++ b/Wrappers/OrganizationWrapper.cs @@ -8,7 +8,7 @@ namespace Facturapi.Wrappers { - public class OrganizationWrapper : BaseWrapper, IOrganizationWrapper + public class OrganizationWrapper : BaseWrapper { internal OrganizationWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/ProductWrapper.cs b/Wrappers/ProductWrapper.cs index 2bb3f72..178b928 100644 --- a/Wrappers/ProductWrapper.cs +++ b/Wrappers/ProductWrapper.cs @@ -7,7 +7,7 @@ namespace Facturapi.Wrappers { - public class ProductWrapper : BaseWrapper, IProductWrapper + public class ProductWrapper : BaseWrapper { internal ProductWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/ReceiptWrapper.cs b/Wrappers/ReceiptWrapper.cs index eb93fc3..4ca5ace 100644 --- a/Wrappers/ReceiptWrapper.cs +++ b/Wrappers/ReceiptWrapper.cs @@ -8,7 +8,7 @@ namespace Facturapi.Wrappers { - public class ReceiptWrapper : BaseWrapper, IReceiptWrapper + public class ReceiptWrapper : BaseWrapper { internal ReceiptWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/RetentionWrapper.cs b/Wrappers/RetentionWrapper.cs index 157f0c9..95cd4dc 100644 --- a/Wrappers/RetentionWrapper.cs +++ b/Wrappers/RetentionWrapper.cs @@ -8,7 +8,7 @@ namespace Facturapi.Wrappers { - public class RetentionWrapper : BaseWrapper, IRetentionWrapper + public class RetentionWrapper : BaseWrapper { internal RetentionWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/ToolWrapper.cs b/Wrappers/ToolWrapper.cs index cf3eb2c..94e2957 100644 --- a/Wrappers/ToolWrapper.cs +++ b/Wrappers/ToolWrapper.cs @@ -6,7 +6,7 @@ namespace Facturapi.Wrappers { - public class ToolWrapper : BaseWrapper, IToolWrapper + public class ToolWrapper : BaseWrapper { internal ToolWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/WebhookWrapper.cs b/Wrappers/WebhookWrapper.cs index 94f5776..61b6678 100644 --- a/Wrappers/WebhookWrapper.cs +++ b/Wrappers/WebhookWrapper.cs @@ -7,7 +7,7 @@ namespace Facturapi.Wrappers { - public class WebhookWrapper : BaseWrapper, IWebhookWrapper + public class WebhookWrapper : BaseWrapper { internal WebhookWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { From dfb0a4323557e0e7d87258a44abc39a2b2a39be6 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 24 Aug 2026 16:13:18 +0200 Subject: [PATCH 6/9] Revert "fix: expose concrete resource wrappers" This reverts commit 76081f92e5f2c2f43e7bf52d070966614e46e953. --- CHANGELOG.md | 4 +- FacturapiClient.cs | 22 +++++----- FacturapiTest/ClientCompatibilityTests.cs | 17 -------- FacturapiTest/WrapperBehaviorTests.cs | 2 +- IFacturapiClient.cs | 19 +++++++++ InvoiceZipRequestExtensions.cs | 42 +++++++++++++++++++ README.md | 32 +++++++++++++- Wrappers/CartaporteCatalogWrapper.cs | 2 +- Wrappers/CatalogWrapper.cs | 2 +- Wrappers/CustomerWrapper.cs | 2 +- Wrappers/ICartaporteCatalogWrapper.cs | 20 +++++++++ Wrappers/ICatalogWrapper.cs | 12 ++++++ Wrappers/ICustomerWrapper.cs | 17 ++++++++ Wrappers/IInvoiceWrapper.cs | 29 +++++++++++++ Wrappers/IInvoiceZipRequestWrapper.cs | 15 +++++++ Wrappers/IOrganizationWrapper.cs | 51 +++++++++++++++++++++++ Wrappers/IProductWrapper.cs | 15 +++++++ Wrappers/IReceiptWrapper.cs | 21 ++++++++++ Wrappers/IRetentionWrapper.cs | 22 ++++++++++ Wrappers/IToolWrapper.cs | 11 +++++ Wrappers/IWebhookWrapper.cs | 16 +++++++ Wrappers/InvoiceWrapper.cs | 2 +- Wrappers/OrganizationWrapper.cs | 2 +- Wrappers/ProductWrapper.cs | 2 +- Wrappers/ReceiptWrapper.cs | 2 +- Wrappers/RetentionWrapper.cs | 2 +- Wrappers/ToolWrapper.cs | 2 +- Wrappers/WebhookWrapper.cs | 2 +- 28 files changed, 344 insertions(+), 43 deletions(-) create mode 100644 IFacturapiClient.cs create mode 100644 InvoiceZipRequestExtensions.cs create mode 100644 Wrappers/ICartaporteCatalogWrapper.cs create mode 100644 Wrappers/ICatalogWrapper.cs create mode 100644 Wrappers/ICustomerWrapper.cs create mode 100644 Wrappers/IInvoiceWrapper.cs create mode 100644 Wrappers/IInvoiceZipRequestWrapper.cs create mode 100644 Wrappers/IOrganizationWrapper.cs create mode 100644 Wrappers/IProductWrapper.cs create mode 100644 Wrappers/IReceiptWrapper.cs create mode 100644 Wrappers/IRetentionWrapper.cs create mode 100644 Wrappers/IToolWrapper.cs create mode 100644 Wrappers/IWebhookWrapper.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e39e00d..0ee4695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added invoice ZIP request methods: `CreateZipRequestAsync`, `ListZipRequestsAsync`, `RetrieveZipRequestAsync`, and `DownloadZipRequestAsync`. -### Changed -- Expose concrete resource wrappers from `FacturapiClient`; use an injected `HttpClient` or an application-owned abstraction for tests. +### Fixed +- Keep `IInvoiceWrapper` stable when adding ZIP request methods. ## [6.6.0] - 2026-07-01 ### Added diff --git a/FacturapiClient.cs b/FacturapiClient.cs index 3486d0f..a97eaeb 100644 --- a/FacturapiClient.cs +++ b/FacturapiClient.cs @@ -6,18 +6,18 @@ namespace Facturapi { - public sealed class FacturapiClient : IDisposable + public sealed class FacturapiClient : IFacturapiClient { - public CustomerWrapper Customer { get; private set; } - public ProductWrapper Product { get; private set; } - public InvoiceWrapper Invoice { get; private set; } - public OrganizationWrapper Organization { get; private set; } - public ReceiptWrapper Receipt { get; private set; } - public RetentionWrapper Retention { get; private set; } - public CatalogWrapper Catalog { get; private set; } - public CartaporteCatalogWrapper CartaporteCatalog { get; private set; } - public ToolWrapper Tool { get; private set; } - public WebhookWrapper Webhook { get; private set; } + public ICustomerWrapper Customer { get; private set; } + public IProductWrapper Product { get; private set; } + public IInvoiceWrapper Invoice { get; private set; } + public IOrganizationWrapper Organization { get; private set; } + public IReceiptWrapper Receipt { get; private set; } + public IRetentionWrapper Retention { get; private set; } + public ICatalogWrapper Catalog { get; private set; } + public ICartaporteCatalogWrapper CartaporteCatalog { get; private set; } + public IToolWrapper Tool { get; private set; } + public IWebhookWrapper Webhook { get; private set; } private readonly HttpClient httpClient; private readonly bool ownsHttpClient; private bool disposed; diff --git a/FacturapiTest/ClientCompatibilityTests.cs b/FacturapiTest/ClientCompatibilityTests.cs index 6fe39a3..c3a167a 100644 --- a/FacturapiTest/ClientCompatibilityTests.cs +++ b/FacturapiTest/ClientCompatibilityTests.cs @@ -13,23 +13,6 @@ namespace FacturapiTest { public class ClientCompatibilityTests { - [Fact] - public void Client_ExposesConcreteResourceWrappers() - { - using var client = new FacturapiClient("test_key"); - - Assert.IsType(client.Customer); - Assert.IsType(client.Product); - Assert.IsType(client.Invoice); - Assert.IsType(client.Organization); - Assert.IsType(client.Receipt); - Assert.IsType(client.Retention); - Assert.IsType(client.Catalog); - Assert.IsType(client.CartaporteCatalog); - Assert.IsType(client.Tool); - Assert.IsType(client.Webhook); - } - [Fact] public void Router_ListCustomers_AllowsNullQueryValues() { diff --git a/FacturapiTest/WrapperBehaviorTests.cs b/FacturapiTest/WrapperBehaviorTests.cs index 9295ae5..173d2f9 100644 --- a/FacturapiTest/WrapperBehaviorTests.cs +++ b/FacturapiTest/WrapperBehaviorTests.cs @@ -505,7 +505,7 @@ public async Task InvoiceCreateZipRequestAsync_UsesZipRequestsPostRoute() return JsonResponse("{\"id\":\"zip_1\",\"status\":\"pending\"}"); }); - var wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler)); + IInvoiceWrapper wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler)); var result = await wrapper.CreateZipRequestAsync(new Dictionary { ["year"] = 2025, diff --git a/IFacturapiClient.cs b/IFacturapiClient.cs new file mode 100644 index 0000000..0217b28 --- /dev/null +++ b/IFacturapiClient.cs @@ -0,0 +1,19 @@ +using Facturapi.Wrappers; +using System; + +namespace Facturapi +{ + public interface IFacturapiClient : IDisposable + { + ICustomerWrapper Customer { get; } + IProductWrapper Product { get; } + IInvoiceWrapper Invoice { get; } + IOrganizationWrapper Organization { get; } + IReceiptWrapper Receipt { get; } + IRetentionWrapper Retention { get; } + ICatalogWrapper Catalog { get; } + ICartaporteCatalogWrapper CartaporteCatalog { get; } + IToolWrapper Tool { get; } + IWebhookWrapper Webhook { get; } + } +} diff --git a/InvoiceZipRequestExtensions.cs b/InvoiceZipRequestExtensions.cs new file mode 100644 index 0000000..7fd42dd --- /dev/null +++ b/InvoiceZipRequestExtensions.cs @@ -0,0 +1,42 @@ +using Facturapi.Wrappers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi +{ + public static class InvoiceZipRequestExtensions + { + public static Task> CreateZipRequestAsync(this IInvoiceWrapper invoice, Dictionary data, CancellationToken cancellationToken = default) + { + return GetZipRequestWrapper(invoice).CreateZipRequestAsync(data, cancellationToken); + } + + public static Task>> ListZipRequestsAsync(this IInvoiceWrapper invoice, Dictionary query = null, CancellationToken cancellationToken = default) + { + return GetZipRequestWrapper(invoice).ListZipRequestsAsync(query, cancellationToken); + } + + public static Task> RetrieveZipRequestAsync(this IInvoiceWrapper invoice, string id, CancellationToken cancellationToken = default) + { + return GetZipRequestWrapper(invoice).RetrieveZipRequestAsync(id, cancellationToken); + } + + public static Task DownloadZipRequestAsync(this IInvoiceWrapper invoice, string id, CancellationToken cancellationToken = default) + { + return GetZipRequestWrapper(invoice).DownloadZipRequestAsync(id, cancellationToken); + } + + private static IInvoiceZipRequestWrapper GetZipRequestWrapper(IInvoiceWrapper invoice) + { + if (invoice is IInvoiceZipRequestWrapper zipRequestWrapper) + { + return zipRequestWrapper; + } + + throw new NotSupportedException("The invoice wrapper does not support ZIP requests."); + } + } +} diff --git a/README.md b/README.md index 0841fbf..a18c8d7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,36 @@ Facturapi ayuda a generar facturas electrónicas válidas en México (CFDI) de l Si alguna vez has usado [Stripe](https://stripe.com) o [Conekta](https://conekta.io), verás que Facturapi es igual de sencillo de entender e integrar a tu aplicación. +## Migración a v6 + +### ¿Cuándo NO necesitas cambiar nada? + +No necesitas actualizar tu código si: +- Creas `FacturapiClient` y llamas métodos directamente (por ejemplo `await client.Invoice.CreateAsync(...)`). +- Usas `var` al guardar wrappers (por ejemplo `var invoices = client.Invoice;`). +- No dependes de tipos concretos de wrappers en firmas, propiedades o pruebas. + +### ¿Cuándo SÍ necesitas actualizar? + +Debes ajustar tu código si: +- Declaras wrappers como clases concretas (`CustomerWrapper`, `InvoiceWrapper`, etc.). +- Mockeas wrappers concretos en pruebas. +- Expones wrappers concretos en tus propias interfaces o APIs públicas. + +Antes (v5): + +```csharp +CustomerWrapper customers = client.Customer; +``` + +Después (v6): + +```csharp +ICustomerWrapper customers = client.Customer; +``` + +Las interfaces de wrappers se mantienen estables para pruebas y mocks. Las capacidades opcionales se exponen en interfaces adicionales; por ejemplo, un mock que cubra solicitudes ZIP debe implementar `IInvoiceWrapper` e `IInvoiceZipRequestWrapper`. + ## Instalación Puedes instalar Facturapi en tu proyecto usando [Nuget](https://www.nuget.org/) @@ -49,8 +79,6 @@ var customHttpClient = new HttpClient(); var facturapi = FacturapiClient.CreateWithCustomHttpClient("TU_API_KEY", customHttpClient); ``` -Para pruebas, usa este factory con un `HttpMessageHandler` propio que simule las respuestas de la API. Si tu aplicación necesita abstraer Facturapi, define una interfaz en tu propia capa de integración. - ### Métodos asíncronos (async, await) Esta librería utiliza métodos asíncronos. Si tu aplicación no tiene código asíncrono, puedes convertir un método asíncrono en síncrono de la siguiente manera: diff --git a/Wrappers/CartaporteCatalogWrapper.cs b/Wrappers/CartaporteCatalogWrapper.cs index 7b5a2c1..f354a4c 100644 --- a/Wrappers/CartaporteCatalogWrapper.cs +++ b/Wrappers/CartaporteCatalogWrapper.cs @@ -6,7 +6,7 @@ namespace Facturapi.Wrappers { - public class CartaporteCatalogWrapper : BaseWrapper + public class CartaporteCatalogWrapper : BaseWrapper, ICartaporteCatalogWrapper { internal CartaporteCatalogWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/CatalogWrapper.cs b/Wrappers/CatalogWrapper.cs index 0997f33..ed21955 100644 --- a/Wrappers/CatalogWrapper.cs +++ b/Wrappers/CatalogWrapper.cs @@ -6,7 +6,7 @@ namespace Facturapi.Wrappers { - public class CatalogWrapper : BaseWrapper + public class CatalogWrapper : BaseWrapper, ICatalogWrapper { internal CatalogWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/CustomerWrapper.cs b/Wrappers/CustomerWrapper.cs index 6cc9f70..ab7e64a 100644 --- a/Wrappers/CustomerWrapper.cs +++ b/Wrappers/CustomerWrapper.cs @@ -7,7 +7,7 @@ namespace Facturapi.Wrappers { - public class CustomerWrapper : BaseWrapper + public class CustomerWrapper : BaseWrapper, ICustomerWrapper { internal CustomerWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/ICartaporteCatalogWrapper.cs b/Wrappers/ICartaporteCatalogWrapper.cs new file mode 100644 index 0000000..3beb615 --- /dev/null +++ b/Wrappers/ICartaporteCatalogWrapper.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface ICartaporteCatalogWrapper + { + Task> SearchAirTransportCodes(Dictionary query = null, CancellationToken cancellationToken = default); + Task> SearchTransportConfigs(Dictionary query = null, CancellationToken cancellationToken = default); + Task> SearchRightsOfPassage(Dictionary query = null, CancellationToken cancellationToken = default); + Task> SearchCustomsDocuments(Dictionary query = null, CancellationToken cancellationToken = default); + Task> SearchPackagingTypes(Dictionary query = null, CancellationToken cancellationToken = default); + Task> SearchTrailerTypes(Dictionary query = null, CancellationToken cancellationToken = default); + Task> SearchHazardousMaterials(Dictionary query = null, CancellationToken cancellationToken = default); + Task> SearchNavalAuthorizations(Dictionary query = null, CancellationToken cancellationToken = default); + Task> SearchPortStations(Dictionary query = null, CancellationToken cancellationToken = default); + Task> SearchMarineContainers(Dictionary query = null, CancellationToken cancellationToken = default); + } +} diff --git a/Wrappers/ICatalogWrapper.cs b/Wrappers/ICatalogWrapper.cs new file mode 100644 index 0000000..80a41b3 --- /dev/null +++ b/Wrappers/ICatalogWrapper.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface ICatalogWrapper + { + Task> SearchProducts(Dictionary query = null, CancellationToken cancellationToken = default); + Task> SearchUnits(Dictionary query = null, CancellationToken cancellationToken = default); + } +} diff --git a/Wrappers/ICustomerWrapper.cs b/Wrappers/ICustomerWrapper.cs new file mode 100644 index 0000000..9006fd1 --- /dev/null +++ b/Wrappers/ICustomerWrapper.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface ICustomerWrapper + { + Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); + Task CreateAsync(Dictionary data, Dictionary queryParams = null, CancellationToken cancellationToken = default); + Task RetrieveAsync(string id, CancellationToken cancellationToken = default); + Task DeleteAsync(string id, CancellationToken cancellationToken = default); + Task UpdateAsync(string id, Dictionary data, Dictionary queryParams = null, CancellationToken cancellationToken = default); + Task ValidateTaxInfoAsync(string id, CancellationToken cancellationToken = default); + Task SendEditLinkByEmailAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + } +} diff --git a/Wrappers/IInvoiceWrapper.cs b/Wrappers/IInvoiceWrapper.cs new file mode 100644 index 0000000..c38b2aa --- /dev/null +++ b/Wrappers/IInvoiceWrapper.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface IInvoiceWrapper + { + Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); + Task CreateAsync(Dictionary data, Dictionary options = null, CancellationToken cancellationToken = default); + Task RetrieveAsync(string id, CancellationToken cancellationToken = default); + Task CancelAsync(string id, Dictionary query = null, CancellationToken cancellationToken = default); + Task SendByEmailAsync(string id, Dictionary data = null, CancellationToken cancellationToken = default); + Task DownloadZipAsync(string id, CancellationToken cancellationToken = default); + Task DownloadPdfAsync(string id, CancellationToken cancellationToken = default); + Task DownloadXmlAsync(string id, CancellationToken cancellationToken = default); + Task DownloadCancellationReceiptXmlAsync(string id, CancellationToken cancellationToken = default); + Task DownloadCancellationReceiptPdfAsync(string id, CancellationToken cancellationToken = default); + Task UpdateStatusAsync(string id, CancellationToken cancellationToken = default); + Task UpdateDraftAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + Task StampDraftAsync(string id, Dictionary options = null, CancellationToken cancellationToken = default); + [Obsolete("Use StampDraftAsync instead.")] + Task StampDraft(string id, Dictionary options = null, CancellationToken cancellationToken = default); + Task CopyToDraftAsync(string id, CancellationToken cancellationToken = default); + Task PreviewPdfAsync(Dictionary data, CancellationToken cancellationToken = default); + } +} diff --git a/Wrappers/IInvoiceZipRequestWrapper.cs b/Wrappers/IInvoiceZipRequestWrapper.cs new file mode 100644 index 0000000..0da1f5a --- /dev/null +++ b/Wrappers/IInvoiceZipRequestWrapper.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface IInvoiceZipRequestWrapper + { + 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/IOrganizationWrapper.cs b/Wrappers/IOrganizationWrapper.cs new file mode 100644 index 0000000..07c7c75 --- /dev/null +++ b/Wrappers/IOrganizationWrapper.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface IOrganizationWrapper + { + Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); + Task MeAsync(CancellationToken cancellationToken = default); + Task CheckDomainIsAvailableAsync(string domain, CancellationToken cancellationToken = default); + Task CreateAsync(Dictionary data, CancellationToken cancellationToken = default); + Task RetrieveAsync(string id, CancellationToken cancellationToken = default); + Task DeleteAsync(string id, CancellationToken cancellationToken = default); + Task UploadLogoAsync(string id, Stream file, CancellationToken cancellationToken = default); + Task UploadCertificateAsync(string id, Stream cerFile, Stream keyFile, string password, CancellationToken cancellationToken = default); + Task DeleteCertificateAsync(string id, CancellationToken cancellationToken = default); + Task UpdateLegalAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + Task UpdateReceiptSettingsAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + Task UpdateCustomizationAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + Task UpdateDomainAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + Task GetTestApiKeyAsync(string id, CancellationToken cancellationToken = default); + Task RenewTestApiKeyAsync(string id, CancellationToken cancellationToken = default); + Task ListLiveApiKeysAsync(string id, CancellationToken cancellationToken = default); + Task RenewLiveApiKeyAsync(string id, CancellationToken cancellationToken = default); + Task> ListSeriesAsync(string id, CancellationToken cancellationToken = default); + Task CreateSeriesAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + Task UpdateSeriesAsync(string id, string seriesName, Dictionary data, CancellationToken cancellationToken = default); + Task DeleteSeriesAsync(string id, string seriesName, CancellationToken cancellationToken = default); + Task UpdateDefaultSeriesAsync(string organizationId, Dictionary data, CancellationToken cancellationToken = default); + Task> DeleteLiveApiKeyAsync(string id, string apiKeyId, CancellationToken cancellationToken = default); + Task UpdateSelfInvoiceSettingsAsync(string organizationId, Dictionary data, CancellationToken cancellationToken = default); + Task> ListTeamAccessAsync(string organizationId, CancellationToken cancellationToken = default); + Task RetrieveTeamAccessAsync(string organizationId, string accessId, CancellationToken cancellationToken = default); + Task UpdateTeamAccessRoleAsync(string organizationId, string accessId, string role, CancellationToken cancellationToken = default); + Task RemoveTeamAccessAsync(string organizationId, string accessId, CancellationToken cancellationToken = default); + Task> ListSentTeamInvitesAsync(string organizationId, CancellationToken cancellationToken = default); + Task InviteUserToTeamAsync(string organizationId, Dictionary data, CancellationToken cancellationToken = default); + Task CancelTeamInviteAsync(string organizationId, string inviteKey, CancellationToken cancellationToken = default); + Task> ListReceivedTeamInvitesAsync(CancellationToken cancellationToken = default); + Task RespondTeamInviteAsync(string inviteKey, Dictionary data, CancellationToken cancellationToken = default); + Task> ListTeamRolesAsync(string organizationId, CancellationToken cancellationToken = default); + Task> ListTeamRoleTemplatesAsync(string organizationId, CancellationToken cancellationToken = default); + Task> ListTeamRoleOperationsAsync(string organizationId, CancellationToken cancellationToken = default); + Task RetrieveTeamRoleAsync(string organizationId, string roleId, CancellationToken cancellationToken = default); + Task CreateTeamRoleAsync(string organizationId, Dictionary data, CancellationToken cancellationToken = default); + Task UpdateTeamRoleAsync(string organizationId, string roleId, Dictionary data, CancellationToken cancellationToken = default); + Task DeleteTeamRoleAsync(string organizationId, string roleId, CancellationToken cancellationToken = default); + } +} diff --git a/Wrappers/IProductWrapper.cs b/Wrappers/IProductWrapper.cs new file mode 100644 index 0000000..006a19a --- /dev/null +++ b/Wrappers/IProductWrapper.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface IProductWrapper + { + Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); + Task CreateAsync(Dictionary data, CancellationToken cancellationToken = default); + Task RetrieveAsync(string id, CancellationToken cancellationToken = default); + Task DeleteAsync(string id, CancellationToken cancellationToken = default); + Task UpdateAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + } +} diff --git a/Wrappers/IReceiptWrapper.cs b/Wrappers/IReceiptWrapper.cs new file mode 100644 index 0000000..085b480 --- /dev/null +++ b/Wrappers/IReceiptWrapper.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface IReceiptWrapper + { + Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); + Task CreateAsync(Dictionary data, CancellationToken cancellationToken = default); + Task RetrieveAsync(string id, CancellationToken cancellationToken = default); + Task CancelAsync(string id, CancellationToken cancellationToken = default); + Task InvoiceAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + Task CreateGlobalInvoiceAsync(Dictionary data, CancellationToken cancellationToken = default); + Task ToInvoiceAsync(Dictionary data, CancellationToken cancellationToken = default); + Task PreviewToInvoicePdfAsync(Dictionary data, CancellationToken cancellationToken = default); + Task SendByEmailAsync(string id, Dictionary data = null, CancellationToken cancellationToken = default); + Task DownloadPdfAsync(string id, CancellationToken cancellationToken = default); + } +} diff --git a/Wrappers/IRetentionWrapper.cs b/Wrappers/IRetentionWrapper.cs new file mode 100644 index 0000000..31d05c7 --- /dev/null +++ b/Wrappers/IRetentionWrapper.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface IRetentionWrapper + { + Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); + Task CreateAsync(Dictionary data, CancellationToken cancellationToken = default); + Task RetrieveAsync(string id, CancellationToken cancellationToken = default); + Task CancelAsync(string id, Dictionary query = null, CancellationToken cancellationToken = default); + Task SendByEmailAsync(string id, Dictionary data = null, CancellationToken cancellationToken = default); + Task UpdateDraftAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + Task StampDraftAsync(string id, Dictionary options = null, CancellationToken cancellationToken = default); + Task CopyToDraftAsync(string id, CancellationToken cancellationToken = default); + Task DownloadZipAsync(string id, CancellationToken cancellationToken = default); + Task DownloadPdfAsync(string id, CancellationToken cancellationToken = default); + Task DownloadXmlAsync(string id, CancellationToken cancellationToken = default); + } +} diff --git a/Wrappers/IToolWrapper.cs b/Wrappers/IToolWrapper.cs new file mode 100644 index 0000000..5ada938 --- /dev/null +++ b/Wrappers/IToolWrapper.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface IToolWrapper + { + Task ValidateTaxIdAsync(string taxId, CancellationToken cancellationToken = default); + Task HealthCheckAsync(CancellationToken cancellationToken = default); + } +} diff --git a/Wrappers/IWebhookWrapper.cs b/Wrappers/IWebhookWrapper.cs new file mode 100644 index 0000000..83db0c0 --- /dev/null +++ b/Wrappers/IWebhookWrapper.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Facturapi.Wrappers +{ + public interface IWebhookWrapper + { + Task> ListAsync(Dictionary query = null, CancellationToken cancellationToken = default); + Task CreateAsync(Dictionary data, CancellationToken cancellationToken = default); + Task RetrieveAsync(string id, CancellationToken cancellationToken = default); + Task UpdateAsync(string id, Dictionary data, CancellationToken cancellationToken = default); + Task DeleteAsync(string id, CancellationToken cancellationToken = default); + Task ValidateSignatureAsync(Dictionary data, CancellationToken cancellationToken = default); + } +} diff --git a/Wrappers/InvoiceWrapper.cs b/Wrappers/InvoiceWrapper.cs index f441bff..7babe47 100644 --- a/Wrappers/InvoiceWrapper.cs +++ b/Wrappers/InvoiceWrapper.cs @@ -9,7 +9,7 @@ namespace Facturapi.Wrappers { - public class InvoiceWrapper : BaseWrapper + public class InvoiceWrapper : BaseWrapper, IInvoiceWrapper, IInvoiceZipRequestWrapper { internal InvoiceWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/OrganizationWrapper.cs b/Wrappers/OrganizationWrapper.cs index 5597297..e55ebd2 100644 --- a/Wrappers/OrganizationWrapper.cs +++ b/Wrappers/OrganizationWrapper.cs @@ -8,7 +8,7 @@ namespace Facturapi.Wrappers { - public class OrganizationWrapper : BaseWrapper + public class OrganizationWrapper : BaseWrapper, IOrganizationWrapper { internal OrganizationWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/ProductWrapper.cs b/Wrappers/ProductWrapper.cs index 178b928..2bb3f72 100644 --- a/Wrappers/ProductWrapper.cs +++ b/Wrappers/ProductWrapper.cs @@ -7,7 +7,7 @@ namespace Facturapi.Wrappers { - public class ProductWrapper : BaseWrapper + public class ProductWrapper : BaseWrapper, IProductWrapper { internal ProductWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/ReceiptWrapper.cs b/Wrappers/ReceiptWrapper.cs index 4ca5ace..eb93fc3 100644 --- a/Wrappers/ReceiptWrapper.cs +++ b/Wrappers/ReceiptWrapper.cs @@ -8,7 +8,7 @@ namespace Facturapi.Wrappers { - public class ReceiptWrapper : BaseWrapper + public class ReceiptWrapper : BaseWrapper, IReceiptWrapper { internal ReceiptWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/RetentionWrapper.cs b/Wrappers/RetentionWrapper.cs index 95cd4dc..157f0c9 100644 --- a/Wrappers/RetentionWrapper.cs +++ b/Wrappers/RetentionWrapper.cs @@ -8,7 +8,7 @@ namespace Facturapi.Wrappers { - public class RetentionWrapper : BaseWrapper + public class RetentionWrapper : BaseWrapper, IRetentionWrapper { internal RetentionWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/ToolWrapper.cs b/Wrappers/ToolWrapper.cs index 94e2957..cf3eb2c 100644 --- a/Wrappers/ToolWrapper.cs +++ b/Wrappers/ToolWrapper.cs @@ -6,7 +6,7 @@ namespace Facturapi.Wrappers { - public class ToolWrapper : BaseWrapper + public class ToolWrapper : BaseWrapper, IToolWrapper { internal ToolWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { diff --git a/Wrappers/WebhookWrapper.cs b/Wrappers/WebhookWrapper.cs index 61b6678..94f5776 100644 --- a/Wrappers/WebhookWrapper.cs +++ b/Wrappers/WebhookWrapper.cs @@ -7,7 +7,7 @@ namespace Facturapi.Wrappers { - public class WebhookWrapper : BaseWrapper + public class WebhookWrapper : BaseWrapper, IWebhookWrapper { internal WebhookWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { From eadc6c9826dbfbd3999cfd25837bd513c87152db Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 24 Aug 2026 16:13:18 +0200 Subject: [PATCH 7/9] Revert "fix: keep invoice wrapper interface stable" This reverts commit 9c4cbe3343c0b43c14292c1707b79fba20a6760b. --- CHANGELOG.md | 4 --- FacturapiTest/WrapperBehaviorTests.cs | 2 +- InvoiceZipRequestExtensions.cs | 42 --------------------------- README.md | 2 -- Wrappers/IInvoiceWrapper.cs | 4 +++ Wrappers/IInvoiceZipRequestWrapper.cs | 15 ---------- Wrappers/InvoiceWrapper.cs | 2 +- 7 files changed, 6 insertions(+), 65 deletions(-) delete mode 100644 InvoiceZipRequestExtensions.cs delete mode 100644 Wrappers/IInvoiceZipRequestWrapper.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee4695..1273917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,9 @@ 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). ## Unreleased - ### Added - Added invoice ZIP request methods: `CreateZipRequestAsync`, `ListZipRequestsAsync`, `RetrieveZipRequestAsync`, and `DownloadZipRequestAsync`. -### Fixed -- Keep `IInvoiceWrapper` stable when adding ZIP request methods. - ## [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 173d2f9..9295ae5 100644 --- a/FacturapiTest/WrapperBehaviorTests.cs +++ b/FacturapiTest/WrapperBehaviorTests.cs @@ -505,7 +505,7 @@ public async Task InvoiceCreateZipRequestAsync_UsesZipRequestsPostRoute() return JsonResponse("{\"id\":\"zip_1\",\"status\":\"pending\"}"); }); - IInvoiceWrapper wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler)); + var wrapper = new InvoiceWrapper("test_key", "v2", CreateHttpClient(handler)); var result = await wrapper.CreateZipRequestAsync(new Dictionary { ["year"] = 2025, diff --git a/InvoiceZipRequestExtensions.cs b/InvoiceZipRequestExtensions.cs deleted file mode 100644 index 7fd42dd..0000000 --- a/InvoiceZipRequestExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Facturapi.Wrappers; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi -{ - public static class InvoiceZipRequestExtensions - { - public static Task> CreateZipRequestAsync(this IInvoiceWrapper invoice, Dictionary data, CancellationToken cancellationToken = default) - { - return GetZipRequestWrapper(invoice).CreateZipRequestAsync(data, cancellationToken); - } - - public static Task>> ListZipRequestsAsync(this IInvoiceWrapper invoice, Dictionary query = null, CancellationToken cancellationToken = default) - { - return GetZipRequestWrapper(invoice).ListZipRequestsAsync(query, cancellationToken); - } - - public static Task> RetrieveZipRequestAsync(this IInvoiceWrapper invoice, string id, CancellationToken cancellationToken = default) - { - return GetZipRequestWrapper(invoice).RetrieveZipRequestAsync(id, cancellationToken); - } - - public static Task DownloadZipRequestAsync(this IInvoiceWrapper invoice, string id, CancellationToken cancellationToken = default) - { - return GetZipRequestWrapper(invoice).DownloadZipRequestAsync(id, cancellationToken); - } - - private static IInvoiceZipRequestWrapper GetZipRequestWrapper(IInvoiceWrapper invoice) - { - if (invoice is IInvoiceZipRequestWrapper zipRequestWrapper) - { - return zipRequestWrapper; - } - - throw new NotSupportedException("The invoice wrapper does not support ZIP requests."); - } - } -} diff --git a/README.md b/README.md index a18c8d7..e8303db 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,6 @@ Después (v6): ICustomerWrapper customers = client.Customer; ``` -Las interfaces de wrappers se mantienen estables para pruebas y mocks. Las capacidades opcionales se exponen en interfaces adicionales; por ejemplo, un mock que cubra solicitudes ZIP debe implementar `IInvoiceWrapper` e `IInvoiceZipRequestWrapper`. - ## Instalación Puedes instalar Facturapi en tu proyecto usando [Nuget](https://www.nuget.org/) 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/IInvoiceZipRequestWrapper.cs b/Wrappers/IInvoiceZipRequestWrapper.cs deleted file mode 100644 index 0da1f5a..0000000 --- a/Wrappers/IInvoiceZipRequestWrapper.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Facturapi.Wrappers -{ - public interface IInvoiceZipRequestWrapper - { - 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 7babe47..5e3d8c1 100644 --- a/Wrappers/InvoiceWrapper.cs +++ b/Wrappers/InvoiceWrapper.cs @@ -9,7 +9,7 @@ namespace Facturapi.Wrappers { - public class InvoiceWrapper : BaseWrapper, IInvoiceWrapper, IInvoiceZipRequestWrapper + public class InvoiceWrapper : BaseWrapper, IInvoiceWrapper { internal InvoiceWrapper(string apiKey, string apiVersion, HttpClient httpClient) : base(apiKey, apiVersion, httpClient) { From 5c74b49f57a4af89614af6ed09cf9fb6026ae6fb Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 24 Aug 2026 16:19:55 +0200 Subject: [PATCH 8/9] docs: clarify wrapper interface compatibility --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e8303db..e0ea4fc 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ Después (v6): ICustomerWrapper customers = client.Customer; ``` +### Contrato de interfaces + +`IFacturapiClient` y las interfaces `I*Wrapper` son contratos públicos para mocks e inyección. Agregar una capacidad de la API al wrapper correspondiente se considera una feature compatible y se publica en una versión menor. Las implementaciones manuales de esas interfaces deben incorporar los nuevos miembros; los integradores que no las implementan directamente no requieren cambios. + ## Instalación Puedes instalar Facturapi en tu proyecto usando [Nuget](https://www.nuget.org/) From 75c2f441e39be546254de202f5978445265ac907 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 24 Aug 2026 16:26:54 +0200 Subject: [PATCH 9/9] docs: clarify wrapper interface versioning --- AGENTS.md | 8 ++++++++ README.md | 4 ---- 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a088f5c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +# SDK Contribution Notes + +## Wrapper Interface Compatibility + +- `IFacturapiClient` and the `I*Wrapper` interfaces are supported public contracts for injection and mocks. +- Adding an API capability to its existing `I*Wrapper` is an additive minor SDK change, not a breaking major-version change. Manual interface implementations must add the new members. +- Do not introduce capability interfaces solely to avoid extending an existing resource wrapper. +- For SDK tests, prefer injecting an `HttpClient` with a custom `HttpMessageHandler`. Applications that need a smaller dependency should define an abstraction in their own integration layer. diff --git a/README.md b/README.md index e0ea4fc..e8303db 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,6 @@ Después (v6): ICustomerWrapper customers = client.Customer; ``` -### Contrato de interfaces - -`IFacturapiClient` y las interfaces `I*Wrapper` son contratos públicos para mocks e inyección. Agregar una capacidad de la API al wrapper correspondiente se considera una feature compatible y se publica en una versión menor. Las implementaciones manuales de esas interfaces deben incorporar los nuevos miembros; los integradores que no las implementan directamente no requieren cambios. - ## Instalación Puedes instalar Facturapi en tu proyecto usando [Nuget](https://www.nuget.org/)