Skip to content

Commit 3782aa1

Browse files
authored
Merge pull request #41 from FacturAPI/feat/improve-resource-cleanup
Feat/improve resource cleanup
2 parents 3fdf83f + a7f8334 commit 3782aa1

21 files changed

+911
-525
lines changed

.github/workflows/deploy.yml

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,41 @@ jobs:
1010
runs-on: ubuntu-latest
1111

1212
steps:
13-
- name: Checkout code
14-
uses: actions/checkout@v4
15-
16-
- name: Setup .NET
17-
uses: actions/setup-dotnet@v4
18-
with:
19-
dotnet-version: '8.0.x' # Adjust the .NET version as needed
20-
21-
- name: Restore dependencies
22-
run: dotnet restore
23-
24-
- name: Build
25-
run: dotnet build --configuration Release --no-restore
26-
27-
- name: Pack
28-
run: dotnet pack --configuration Release --no-build --output ./nupkg
29-
30-
- name: Install xmllint
31-
run: sudo apt-get update && sudo apt-get install -y libxml2-utils
32-
33-
- name: Check if version exists on NuGet
34-
id: version-check
35-
run: |
36-
PACKAGE_ID="Facturapi"
37-
VERSION=$(xmllint --xpath "string(//Project/PropertyGroup/Version)" facturapi-net.csproj)
38-
echo "Detected version: $VERSION"
39-
echo "version=$VERSION" >> $GITHUB_OUTPUT
40-
if curl -sSf "https://api.nuget.org/v3-flatcontainer/${PACKAGE_ID,,}/$VERSION/${PACKAGE_ID,,}.$VERSION.nupkg" > /dev/null; then
41-
echo "Version $VERSION already exists. Skipping push."
42-
echo "exists=true" >> $GITHUB_OUTPUT
43-
else
44-
echo "Version $VERSION does not exist. Proceeding with publish."
45-
echo "exists=false" >> $GITHUB_OUTPUT
46-
fi
47-
48-
- name: Publish to NuGet
49-
if: steps.version-check.outputs.exists == 'false'
50-
run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Setup .NET
17+
uses: actions/setup-dotnet@v4
18+
with:
19+
dotnet-version: "10.0.x" # Adjust the .NET version as needed
20+
21+
- name: Restore dependencies
22+
run: dotnet restore
23+
24+
- name: Build
25+
run: dotnet build --configuration Release --no-restore
26+
27+
- name: Pack
28+
run: dotnet pack --configuration Release --no-build --output ./nupkg
29+
30+
- name: Install xmllint
31+
run: sudo apt-get update && sudo apt-get install -y libxml2-utils
32+
33+
- name: Check if version exists on NuGet
34+
id: version-check
35+
run: |
36+
PACKAGE_ID="Facturapi"
37+
VERSION=$(xmllint --xpath "string(//Project/PropertyGroup/Version)" facturapi-net.csproj)
38+
echo "Detected version: $VERSION"
39+
echo "version=$VERSION" >> $GITHUB_OUTPUT
40+
if curl -sSf "https://api.nuget.org/v3-flatcontainer/${PACKAGE_ID,,}/$VERSION/${PACKAGE_ID,,}.$VERSION.nupkg" > /dev/null; then
41+
echo "Version $VERSION already exists. Skipping push."
42+
echo "exists=true" >> $GITHUB_OUTPUT
43+
else
44+
echo "Version $VERSION does not exist. Proceeding with publish."
45+
echo "exists=false" >> $GITHUB_OUTPUT
46+
fi
47+
48+
- name: Publish to NuGet
49+
if: steps.version-check.outputs.exists == 'false'
50+
run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json

CHANGELOG.md

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

8-
## [4.12.0] = Unreleased
8+
## [5.0.0] - 2025-12-10
9+
10+
### Breaking
11+
12+
- Wrappers can no longer be constructed directly; their constructors are internal and they are intended to be used only through `FacturapiClient`.
13+
- Renamed existing methods to match documented C# surface: `Organization.List/Create/Update/DeleteSeriesAsync` (were `*SeriesGroupAsync`), `Invoice.UpdateStatusAsync` (was `UpdateStatus`), and `Tool.ValidateTaxIdAsync` (was `ValidateTaxId`).
14+
- Carta Porte catalog methods moved to a dedicated `CartaporteCatalogWrapper`; `CartaporteCatalog` is now of that type, and `CatalogWrapper` retains only product/unit catalogs.
915

1016
### Added
1117

18+
- Expose webhook methods through `FacturapiClient`/`IFacturapiClient`.
19+
- New organization endpoints: `MeAsync` (`/organizations/me`), `CheckDomainIsAvailableAsync`, `UpdateReceiptSettingsAsync`, and `UpdateDomainAsync`.
20+
- New `CartaporteCatalogWrapper` for Carta Porte catalog searches.
21+
- Added `DomainAvailability` model for domain check responses.
22+
- Added `Tool.HealthCheckAsync` for `/check`.
1223
- `FacturapiException.Status` now surfaces the HTTP status code when available.
24+
- Introduced `IFacturapiClient` so consumers can mock the client surface in tests.
25+
- Optional `CancellationToken` parameters on client methods to allow request cancellation from callers.
26+
27+
### Changed
28+
29+
- `FacturapiClient` now implements `IDisposable`; call `Dispose()` when finished (or wrap in `using`) to release HTTP resources. If not disposed, garbage collection will eventually clean up, but explicit disposal avoids lingering HTTP connections.
30+
31+
### Fixed
32+
33+
- `Invoices.PreviewPdfAsync` now calls the documented POST endpoint with a JSON body (breaking change to the method signature).
34+
- `Receipts.CreateGlobalInvoiceAsync` posts directly to `/receipts/global-invoice` and no longer requires an id (breaking change to the signature).
35+
- Receipt routes now hit `/receipts/{id}` for cancel, invoice, email, and PDF download instead of invoice endpoints.
36+
- `Organizations.CreateSeriesAsync` uses POST (not PUT) to `/organizations/{id}/series-group`, matching the API.
1337

1438
## [4.11.0] - 2025-12-10
1539

@@ -65,7 +89,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6589
- Type IepsMode for Tax model
6690
- Type Factor for Tax model
6791

68-
## [4.7.0] = 2025-02-25
92+
## [4.7.0] - 2025-02-25
6993

7094
### Added
7195

FacturapiClient.cs

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,57 @@
1-
namespace Facturapi
1+
using Facturapi.Wrappers;
2+
using System;
3+
using System.Net.Http;
4+
using System.Net.Http.Headers;
5+
using System.Text;
6+
7+
namespace Facturapi
28
{
3-
public class FacturapiClient
9+
public sealed class FacturapiClient : IFacturapiClient
410
{
5-
public Wrappers.CustomerWrapper Customer { get; private set; }
6-
public Wrappers.ProductWrapper Product { get; private set; }
7-
public Wrappers.InvoiceWrapper Invoice { get; private set; }
8-
public Wrappers.OrganizationWrapper Organization { get; private set; }
9-
public Wrappers.ReceiptWrapper Receipt { get; private set; }
10-
public Wrappers.RetentionWrapper Retention { get; private set; }
11-
public Wrappers.CatalogWrapper Catalog { get; private set; }
12-
public Wrappers.CatalogWrapper CartaporteCatalog { get; private set; }
13-
public Wrappers.ToolWrapper Tool { get; private set; }
11+
public CustomerWrapper Customer { get; private set; }
12+
public ProductWrapper Product { get; private set; }
13+
public InvoiceWrapper Invoice { get; private set; }
14+
public OrganizationWrapper Organization { get; private set; }
15+
public ReceiptWrapper Receipt { get; private set; }
16+
public RetentionWrapper Retention { get; private set; }
17+
public CatalogWrapper Catalog { get; private set; }
18+
public CartaporteCatalogWrapper CartaporteCatalog { get; private set; }
19+
public ToolWrapper Tool { get; private set; }
20+
public WebhookWrapper Webhook { get; private set; }
21+
private readonly HttpClient httpClient;
22+
private bool disposed;
1423

1524
public FacturapiClient(string apiKey, string apiVersion = "v2")
1625
{
17-
this.Customer = new Wrappers.CustomerWrapper(apiKey, apiVersion);
18-
this.Product = new Wrappers.ProductWrapper(apiKey, apiVersion);
19-
this.Invoice = new Wrappers.InvoiceWrapper(apiKey, apiVersion);
20-
this.Organization = new Wrappers.OrganizationWrapper(apiKey, apiVersion);
21-
this.Receipt = new Wrappers.ReceiptWrapper(apiKey, apiVersion);
22-
this.Retention = new Wrappers.RetentionWrapper(apiKey, apiVersion);
23-
this.Catalog = new Wrappers.CatalogWrapper(apiKey, apiVersion);
24-
this.CartaporteCatalog = new Wrappers.CatalogWrapper(apiKey, apiVersion);
25-
this.Tool = new Wrappers.ToolWrapper(apiKey, apiVersion);
26+
var apiKeyBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(apiKey + ":"));
27+
this.httpClient = new HttpClient
28+
{
29+
BaseAddress = new Uri($"https://www.facturapi.io/{apiVersion}/")
30+
};
31+
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", apiKeyBase64);
32+
33+
this.Customer = new CustomerWrapper(apiKey, apiVersion, this.httpClient);
34+
this.Product = new ProductWrapper(apiKey, apiVersion, this.httpClient);
35+
this.Invoice = new InvoiceWrapper(apiKey, apiVersion, this.httpClient);
36+
this.Organization = new OrganizationWrapper(apiKey, apiVersion, this.httpClient);
37+
this.Receipt = new ReceiptWrapper(apiKey, apiVersion, this.httpClient);
38+
this.Retention = new RetentionWrapper(apiKey, apiVersion, this.httpClient);
39+
this.Catalog = new CatalogWrapper(apiKey, apiVersion, this.httpClient);
40+
this.CartaporteCatalog = new CartaporteCatalogWrapper(apiKey, apiVersion, this.httpClient);
41+
this.Tool = new ToolWrapper(apiKey, apiVersion, this.httpClient);
42+
this.Webhook = new WebhookWrapper(apiKey, apiVersion, this.httpClient);
43+
}
44+
45+
public void Dispose()
46+
{
47+
if (this.disposed)
48+
{
49+
return;
50+
}
51+
52+
this.httpClient?.Dispose();
53+
this.disposed = true;
54+
GC.SuppressFinalize(this);
2655
}
2756
}
2857
}

IFacturapiClient.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Facturapi.Wrappers;
2+
using System;
3+
4+
namespace Facturapi
5+
{
6+
public interface IFacturapiClient : IDisposable
7+
{
8+
CustomerWrapper Customer { get; }
9+
ProductWrapper Product { get; }
10+
InvoiceWrapper Invoice { get; }
11+
OrganizationWrapper Organization { get; }
12+
ReceiptWrapper Receipt { get; }
13+
RetentionWrapper Retention { get; }
14+
CatalogWrapper Catalog { get; }
15+
CartaporteCatalogWrapper CartaporteCatalog { get; }
16+
ToolWrapper Tool { get; }
17+
WebhookWrapper Webhook { get; }
18+
}
19+
}

Models/DomainAvailability.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Facturapi
2+
{
3+
public class DomainAvailability
4+
{
5+
public bool Available { get; set; }
6+
}
7+
}

Router/HealthRouter.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Facturapi
2+
{
3+
internal static partial class Router
4+
{
5+
public static string HealthCheck()
6+
{
7+
return "check";
8+
}
9+
}
10+
}

Router/InvoiceRouter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ public static string CopyInvoice(string id)
6363
return $"invoices/{id}/copy";
6464
}
6565

66-
public static string PreviewPdf(Dictionary<string, object> query = null)
66+
public static string PreviewPdf()
6767
{
68-
return UriWithQuery("invoices/preview/pdf", query);
68+
return "invoices/preview/pdf";
6969
}
7070
}
7171
}

Router/OrganizationRouter.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ public static string ListOrganizations(Dictionary<string, object> query = null)
1010
return UriWithQuery("organizations", query);
1111
}
1212

13+
public static string OrganizationMe()
14+
{
15+
return "organizations/me";
16+
}
17+
1318
public static string RetrieveOrganization(string id)
1419
{
1520
return $"organizations/{id}";
@@ -25,6 +30,11 @@ public static string DeleteOrganization(string id)
2530
return RetrieveOrganization(id);
2631
}
2732

33+
public static string CheckDomainAvailability(Dictionary<string, object> query = null)
34+
{
35+
return UriWithQuery("organizations/domain-check", query);
36+
}
37+
2838
public static string UpdateLegal(string id)
2939
{
3040
return $"{RetrieveOrganization(id)}/legal";
@@ -40,6 +50,11 @@ public static string UploadLogo(string id)
4050
return $"{RetrieveOrganization(id)}/logo";
4151
}
4252

53+
public static string UpdateReceipts(string id)
54+
{
55+
return $"{RetrieveOrganization(id)}/receipts";
56+
}
57+
4358
public static string UploadCertificate(string id)
4459
{
4560
return $"{RetrieveOrganization(id)}/certificate";
@@ -100,5 +115,10 @@ public static string UpdateSelfInvoiceSettings(string organizationId)
100115
{
101116
return $"{RetrieveOrganization(organizationId)}/self-invoice";
102117
}
118+
119+
public static string UpdateDomain(string organizationId)
120+
{
121+
return $"{RetrieveOrganization(organizationId)}/domain";
122+
}
103123
}
104124
}

Router/ReceiptRouter.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,22 @@ public static string CancelReceipt(string id)
3030

3131
public static string InvoiceReceipt(string id)
3232
{
33-
return $"receipts/(id)/invoice";
33+
return $"receipts/{id}/invoice";
3434
}
3535

36-
public static string CreateGlobalInvoice(string id)
36+
public static string CreateGlobalInvoice()
3737
{
3838
return $"receipts/global-invoice";
3939
}
4040

4141
public static string DownloadReceiptPdf(string id)
4242
{
43-
return $"receipts/(id)/pdf";
43+
return $"receipts/{id}/pdf";
4444
}
4545

4646
public static string SendReceiptByEmail(string id)
4747
{
48-
return $"receipts/(id)/email";
48+
return $"receipts/{id}/email";
4949
}
5050
}
5151
}

Wrappers/BaseWrapper.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,23 @@
44
using System.Net.Http;
55
using System.Text;
66
using System.Threading.Tasks;
7+
using System.Threading;
78

89
namespace Facturapi.Wrappers
910
{
1011
public abstract class BaseWrapper
1112
{
12-
protected const string BASE_URL = "https://www.facturapi.io/";
1313
protected HttpClient client;
1414
protected JsonSerializerSettings jsonSettings { get; set; }
1515
public string apiKey { get; set; }
1616
public string apiVersion { get; set; }
1717

18-
public BaseWrapper(string apiKey, string apiVersion = "v2")
18+
public BaseWrapper(string apiKey, string apiVersion, HttpClient httpClient)
1919
{
20-
var apiKeyBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(apiKey + ":"));
21-
this.client = new HttpClient()
22-
{
23-
BaseAddress = new Uri($"{BASE_URL}/{apiVersion}/")
24-
};
25-
this.client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", apiKeyBase64);
20+
this.apiKey = apiKey;
21+
this.apiVersion = apiVersion;
22+
23+
this.client = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
2624
this.jsonSettings = new JsonSerializerSettings
2725
{
2826
ContractResolver = new SnakeCasePropertyNamesContractResolver(),
@@ -72,8 +70,10 @@ protected FacturapiException CreateException(string resultString, HttpResponseMe
7270
return new FacturapiException(message, status);
7371
}
7472

75-
protected async Task ThrowIfErrorAsync(HttpResponseMessage response)
73+
protected async Task ThrowIfErrorAsync(HttpResponseMessage response, CancellationToken cancellationToken = default)
7674
{
75+
cancellationToken.ThrowIfCancellationRequested();
76+
7777
if (response.IsSuccessStatusCode)
7878
{
7979
return;

0 commit comments

Comments
 (0)