Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ICustomerWrapper customers = client.Customer;
- `Invoice.StampDraftAsync` and legacy `StampDraft` now co-exist; `StampDraft` is marked as obsolete.
- Added initial `FacturapiTest` test project with regression coverage for query building and wrapper behavior.
- Added `FacturapiClient.CreateWithCustomHttpClient(...)` for advanced scenarios where consumers need to provide their own `HttpClient` without changing the default constructor.
- Added organization team-management endpoints to `Organization` / `IOrganizationWrapper`: access listing and retrieval, invite send/cancel/respond flows, role listing/templates/operations, role CRUD, and role reassignment for team members.

### Fixed

Expand Down
123 changes: 123 additions & 0 deletions FacturapiTest/WrapperBehaviorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,129 @@ public async Task OrganizationDeleteSeriesAsync_UsesDeleteSeriesRoute()
Assert.Equal("A", result.Name);
}

[Fact]
public async Task OrganizationListTeamAccessAsync_UsesTeamRoute()
{
var handler = new RecordingHandler((request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Get, request.Method);
Assert.NotNull(request.RequestUri);
Assert.Equal("/v2/organizations/org_1/team", request.RequestUri.PathAndQuery);
return Task.FromResult(JsonResponse("[]"));
});

var wrapper = new OrganizationWrapper("test_key", "v2", CreateHttpClient(handler));
var result = await wrapper.ListTeamAccessAsync("org_1");

Assert.NotNull(result);
}

[Fact]
public async Task OrganizationInviteUserToTeamAsync_UsesInvitesRoute()
{
var handler = new RecordingHandler(async (request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Post, request.Method);
Assert.NotNull(request.RequestUri);
Assert.Equal("/v2/organizations/org_1/team/invites", request.RequestUri.PathAndQuery);
Assert.NotNull(request.Content);
var body = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Contains("\"email\":\"dev@example.com\"", body);

return JsonResponse("{\"id\":\"inv_001\",\"email\":\"dev@example.com\"}");
});

var wrapper = new OrganizationWrapper("test_key", "v2", CreateHttpClient(handler));
var result = await wrapper.InviteUserToTeamAsync("org_1", new Dictionary<string, object>
{
["email"] = "dev@example.com"
});

Assert.Equal("inv_001", result.Id);
}

[Fact]
public async Task OrganizationListTeamRoleOperationsAsync_UsesOperationsRoute()
{
var handler = new RecordingHandler((request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Get, request.Method);
Assert.NotNull(request.RequestUri);
Assert.Equal("/v2/organizations/org_1/team/roles/operations", request.RequestUri.PathAndQuery);
return Task.FromResult(JsonResponse("[\"invoice:list\"]"));
});

var wrapper = new OrganizationWrapper("test_key", "v2", CreateHttpClient(handler));
var result = await wrapper.ListTeamRoleOperationsAsync("org_1");

Assert.Single(result);
Assert.Equal("invoice:list", result[0]);
}

[Fact]
public async Task OrganizationRemoveTeamAccessAsync_ParsesOkResponse()
{
var handler = new RecordingHandler((request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Delete, request.Method);
Assert.NotNull(request.RequestUri);
Assert.Equal("/v2/organizations/org_1/team/acc_1", request.RequestUri.PathAndQuery);
return Task.FromResult(JsonResponse("{\"ok\":true}"));
});

var wrapper = new OrganizationWrapper("test_key", "v2", CreateHttpClient(handler));
var result = await wrapper.RemoveTeamAccessAsync("org_1", "acc_1");

Assert.True(result);
}

[Fact]
public async Task OrganizationRespondTeamInviteAsync_UsesInviteResponseRoute()
{
var handler = new RecordingHandler(async (request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Post, request.Method);
Assert.NotNull(request.RequestUri);
Assert.Equal("/v2/organizations/invites/inv_1/response", request.RequestUri.PathAndQuery);
Assert.NotNull(request.Content);
var body = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Contains("\"accept\":true", body);
return JsonResponse("{\"ok\":true}");
});

var wrapper = new OrganizationWrapper("test_key", "v2", CreateHttpClient(handler));
var result = await wrapper.RespondTeamInviteAsync("inv_1", new Dictionary<string, object>
{
["accept"] = true
});

Assert.True(result);
}

[Fact]
public async Task OrganizationUpdateTeamRoleAsync_UsesRoleRoute()
{
var handler = new RecordingHandler(async (request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Put, request.Method);
Assert.NotNull(request.RequestUri);
Assert.Equal("/v2/organizations/org_1/team/roles/role_1", request.RequestUri.PathAndQuery);
Assert.NotNull(request.Content);
var body = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Contains("\"name\":\"Senior billing analyst\"", body);
return JsonResponse("{\"id\":\"role_1\",\"name\":\"Senior billing analyst\"}");
});

var wrapper = new OrganizationWrapper("test_key", "v2", CreateHttpClient(handler));
var result = await wrapper.UpdateTeamRoleAsync("org_1", "role_1", new Dictionary<string, object>
{
["name"] = "Senior billing analyst"
});

Assert.Equal("role_1", result.Id);
Assert.Equal("Senior billing analyst", result.Name);
}

[Fact]
public async Task RetentionListAsync_UsesRetentionsRoute()
{
Expand Down
17 changes: 17 additions & 0 deletions Models/OrganizationInvite.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;

namespace Facturapi
{
public class OrganizationInvite
{
public string Id { get; set; }
public DateTime? CreatedAt { get; set; }
public string Email { get; set; }
public string OrganizationName { get; set; }
public string Role { get; set; }
public string RoleName { get; set; }
public List<string> Roles { get; set; }
public DateTime? ExpiresAt { get; set; }
}
}
20 changes: 20 additions & 0 deletions Models/OrganizationTeamRole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;

namespace Facturapi
{
public class OrganizationTeamRole
{
public string Id { get; set; }
public string Name { get; set; }
public string TemplateCode { get; set; }
public string Scope { get; set; }
public string Organization { get; set; }
public List<string> Operations { get; set; }
public int UsedBy { get; set; }
public DateTime? CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public Dictionary<string, object> CreatedBy { get; set; }
public Dictionary<string, object> UpdatedBy { get; set; }
}
}
12 changes: 12 additions & 0 deletions Models/OrganizationTeamRoleTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;

namespace Facturapi
{
public class OrganizationTeamRoleTemplate
{
public string Code { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<string> Operations { get; set; }
}
}
18 changes: 18 additions & 0 deletions Models/OrganizationUserAccess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;

namespace Facturapi
{
public class OrganizationUserAccess
{
public string Id { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public string Role { get; set; }
public string RoleName { get; set; }
public string Organization { get; set; }
public List<string> Operations { get; set; }
public DateTime? CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
}
55 changes: 55 additions & 0 deletions Router/OrganizationRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,60 @@ public static string UpdateDomain(string organizationId)
{
return $"{RetrieveOrganization(organizationId)}/domain";
}

public static string ListTeamAccess(string organizationId)
{
return $"{RetrieveOrganization(organizationId)}/team";
}

public static string RetrieveTeamAccess(string organizationId, string accessId)
{
return $"{ListTeamAccess(organizationId)}/{accessId}";
}

public static string UpdateTeamAccessRole(string organizationId, string accessId)
{
return $"{RetrieveTeamAccess(organizationId, accessId)}/role";
}

public static string ListSentTeamInvites(string organizationId)
{
return $"{ListTeamAccess(organizationId)}/invites";
}

public static string CancelTeamInvite(string organizationId, string inviteKey)
{
return $"{ListSentTeamInvites(organizationId)}/{inviteKey}";
}

public static string ListReceivedTeamInvites()
{
return "organizations/invites/pending";
}

public static string RespondTeamInvite(string inviteKey)
{
return $"organizations/invites/{inviteKey}/response";
}

public static string ListTeamRoles(string organizationId)
{
return $"{ListTeamAccess(organizationId)}/roles";
}

public static string ListTeamRoleTemplates(string organizationId)
{
return $"{ListTeamRoles(organizationId)}/templates";
}

public static string ListTeamRoleOperations(string organizationId)
{
return $"{ListTeamRoles(organizationId)}/operations";
}

public static string RetrieveTeamRole(string organizationId, string roleId)
{
return $"{ListTeamRoles(organizationId)}/{roleId}";
}
}
}
16 changes: 16 additions & 0 deletions Wrappers/IOrganizationWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,21 @@ public interface IOrganizationWrapper
Task<SeriesGroup> DeleteSeriesAsync(string id, string seriesName, CancellationToken cancellationToken = default);
Task<List<LiveApiKey>> DeleteLiveApiKeyAsync(string id, string apiKeyId, CancellationToken cancellationToken = default);
Task<Organization> UpdateSelfInvoiceSettingsAsync(string organizationId, Dictionary<string, object> data, CancellationToken cancellationToken = default);
Task<List<OrganizationUserAccess>> ListTeamAccessAsync(string organizationId, CancellationToken cancellationToken = default);
Task<OrganizationUserAccess> RetrieveTeamAccessAsync(string organizationId, string accessId, CancellationToken cancellationToken = default);
Task<OrganizationUserAccess> UpdateTeamAccessRoleAsync(string organizationId, string accessId, string role, CancellationToken cancellationToken = default);
Task<bool> RemoveTeamAccessAsync(string organizationId, string accessId, CancellationToken cancellationToken = default);
Task<List<OrganizationInvite>> ListSentTeamInvitesAsync(string organizationId, CancellationToken cancellationToken = default);
Task<OrganizationInvite> InviteUserToTeamAsync(string organizationId, Dictionary<string, object> data, CancellationToken cancellationToken = default);
Task<bool> CancelTeamInviteAsync(string organizationId, string inviteKey, CancellationToken cancellationToken = default);
Task<List<OrganizationInvite>> ListReceivedTeamInvitesAsync(CancellationToken cancellationToken = default);
Task<bool> RespondTeamInviteAsync(string inviteKey, Dictionary<string, object> data, CancellationToken cancellationToken = default);
Task<List<OrganizationTeamRole>> ListTeamRolesAsync(string organizationId, CancellationToken cancellationToken = default);
Task<List<OrganizationTeamRoleTemplate>> ListTeamRoleTemplatesAsync(string organizationId, CancellationToken cancellationToken = default);
Task<List<string>> ListTeamRoleOperationsAsync(string organizationId, CancellationToken cancellationToken = default);
Task<OrganizationTeamRole> RetrieveTeamRoleAsync(string organizationId, string roleId, CancellationToken cancellationToken = default);
Task<OrganizationTeamRole> CreateTeamRoleAsync(string organizationId, Dictionary<string, object> data, CancellationToken cancellationToken = default);
Task<OrganizationTeamRole> UpdateTeamRoleAsync(string organizationId, string roleId, Dictionary<string, object> data, CancellationToken cancellationToken = default);
Task<bool> DeleteTeamRoleAsync(string organizationId, string roleId, CancellationToken cancellationToken = default);
}
}
Loading
Loading