diff --git a/Dockerfile b/Dockerfile index 583df9c..a7c40d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 ARG CONFIGURATION="Release" ARG NUGET_PACKAGE_VERSION="1.0.0" -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS restore +FROM mcr.microsoft.com/dotnet/sdk:10.0.401 AS restore ARG CONFIGURATION @@ -33,6 +33,7 @@ COPY ./tests/ ./tests/ COPY ./samples/ ./samples/ COPY ./external/ ./external/ COPY ./enumerations/ ./enumerations/ +COPY ./.git/ ./.git/ RUN dotnet build --configuration $CONFIGURATION --no-restore FROM build AS test diff --git a/enumerations/extra/psc.yml b/enumerations/extra/psc.yml index a380762..72420e8 100644 --- a/enumerations/extra/psc.yml +++ b/enumerations/extra/psc.yml @@ -1,3 +1,6 @@ +person_with_significant_control_notification_kind: + 'personal-notification': '' + person_with_significant_control_kind: 'corporate-entity-person-with-significant-control': '' 'corporate-entity-beneficial-owner': '' diff --git a/global.json b/global.json index d46d21e..b5715a7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.100", + "version": "10.0.401", "rollForward": "latestFeature", "allowPrerelease": false } diff --git a/src/CompaniesHouse/CompaniesHouseClient.cs b/src/CompaniesHouse/CompaniesHouseClient.cs index e515fae..47d72d9 100644 --- a/src/CompaniesHouse/CompaniesHouseClient.cs +++ b/src/CompaniesHouse/CompaniesHouseClient.cs @@ -180,6 +180,11 @@ public Task> GetLegalPerson return _companiesHousePersonsWithSignificantControlDetailsClient.GetLegalPersonBeneficialOwnerAsync(companyNumber, notificationId, cancellationToken); } + public Task> GetNotificationsAsync(string companyNumber, string pscId, string? filter = null, int startIndex = 0, int pageSize = 25, CancellationToken cancellationToken = default) + { + return _companiesHousePersonsWithSignificantControlDetailsClient.GetNotificationsAsync(companyNumber, pscId, filter, startIndex, pageSize, cancellationToken); + } + public Task> GetPersonsWithSignificantControlStatementsAsync(string companyNumber, int startIndex = 0, int pageSize = 25, bool? registerView = null, CancellationToken cancellationToken = default) { return _companiesHousePersonsWithSignificantControlDetailsClient.GetPersonsWithSignificantControlStatementsAsync(companyNumber, startIndex, pageSize, registerView, cancellationToken); diff --git a/src/CompaniesHouse/CompaniesHousePersonsWithSignificantControlDetailsClient.cs b/src/CompaniesHouse/CompaniesHousePersonsWithSignificantControlDetailsClient.cs index b08e0aa..4cceb25 100644 --- a/src/CompaniesHouse/CompaniesHousePersonsWithSignificantControlDetailsClient.cs +++ b/src/CompaniesHouse/CompaniesHousePersonsWithSignificantControlDetailsClient.cs @@ -49,6 +49,11 @@ public async Task> GetLegal return await GetAsync(_uriBuilder.BuildLegalPersonBeneficialOwner(companyNumber, notificationId), cancellationToken).ConfigureAwait(false); } + public async Task> GetNotificationsAsync(string companyNumber, string pscId, string? filter = null, int startIndex = 0, int pageSize = 25, CancellationToken cancellationToken = default) + { + return await GetAsync(_uriBuilder.BuildNotifications(companyNumber, pscId, filter, startIndex, pageSize), cancellationToken).ConfigureAwait(false); + } + public async Task> GetPersonsWithSignificantControlStatementsAsync(string companyNumber, int startIndex = 0, int pageSize = 25, bool? registerView = null, CancellationToken cancellationToken = default) { return await GetAsync(_uriBuilder.BuildStatementsList(companyNumber, startIndex, pageSize, registerView), cancellationToken).ConfigureAwait(false); diff --git a/src/CompaniesHouse/ICompaniesHousePersonsWithSignificantControlDetailsClient.cs b/src/CompaniesHouse/ICompaniesHousePersonsWithSignificantControlDetailsClient.cs index 8c5cd47..0e0d2ee 100644 --- a/src/CompaniesHouse/ICompaniesHousePersonsWithSignificantControlDetailsClient.cs +++ b/src/CompaniesHouse/ICompaniesHousePersonsWithSignificantControlDetailsClient.cs @@ -18,6 +18,8 @@ public interface ICompaniesHousePersonsWithSignificantControlDetailsClient Task> GetLegalPersonBeneficialOwnerAsync(string companyNumber, string notificationId, CancellationToken cancellationToken = default); + Task> GetNotificationsAsync(string companyNumber, string pscId, string? filter = null, int startIndex = 0, int pageSize = 25, CancellationToken cancellationToken = default); + Task> GetPersonsWithSignificantControlStatementsAsync(string companyNumber, int startIndex = 0, int pageSize = 25, bool? registerView = null, CancellationToken cancellationToken = default); Task> GetPersonsWithSignificantControlStatementAsync(string companyNumber, string statementId, CancellationToken cancellationToken = default); diff --git a/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotification.cs b/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotification.cs new file mode 100644 index 0000000..cfdde50 --- /dev/null +++ b/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotification.cs @@ -0,0 +1,58 @@ +using System; +using System.Text.Json.Serialization; +using CompaniesHouse.Response.Appointments; +using CompaniesHouse.Response.Officers; + +namespace CompaniesHouse.Response.PersonsWithSignificantControl +{ + public class PersonWithSignificantControlNotification + { + [JsonPropertyName("address")] + public Address? Address { get; set; } + + [JsonPropertyName("ceased_on")] + public DateTime? CeasedOn { get; set; } + + [JsonPropertyName("country_of_residence")] + public string? CountryOfResidence { get; set; } + + [JsonPropertyName("etag")] + public string? ETag { get; set; } + + [JsonPropertyName("identification")] + public PersonWithSignificantControlIdentification? Identification { get; set; } + + [JsonPropertyName("identity_verification_details")] + public IdentityVerificationDetails? IdentityVerificationDetails { get; set; } + + [JsonPropertyName("is_sanctioned")] + public bool? IsSanctioned { get; set; } + + [JsonPropertyName("kind")] + public PersonWithSignificantControlKind Kind { get; set; } + + [JsonPropertyName("links")] + public PersonWithSignificantControlNotificationLinks? Links { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("name_elements")] + public NameElements? NameElements { get; set; } + + [JsonPropertyName("nationality")] + public string? Nationality { get; set; } + + [JsonPropertyName("natures_of_control")] + public PersonWithSignificantControlNatureOfControl[]? NaturesOfControl { get; set; } + + [JsonPropertyName("notified_on")] + public DateTime? NotifiedOn { get; set; } + + [JsonPropertyName("notified_to")] + public PersonWithSignificantControlNotifiedTo? NotifiedTo { get; set; } + + [JsonPropertyName("principal_office_address")] + public Address? PrincipalOfficeAddress { get; set; } + } +} diff --git a/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotificationLinks.cs b/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotificationLinks.cs new file mode 100644 index 0000000..d711c46 --- /dev/null +++ b/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotificationLinks.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace CompaniesHouse.Response.PersonsWithSignificantControl +{ + public class PersonWithSignificantControlNotificationLinks + { + [JsonPropertyName("company")] + public string? Company { get; set; } + } +} diff --git a/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotifications.cs b/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotifications.cs new file mode 100644 index 0000000..9934d71 --- /dev/null +++ b/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotifications.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace CompaniesHouse.Response.PersonsWithSignificantControl +{ + public class PersonWithSignificantControlNotifications + { + [JsonPropertyName("active_count")] + public int? ActiveCount { get; set; } + + [JsonPropertyName("ceased_count")] + public int? CeasedCount { get; set; } + + [JsonPropertyName("inactive_count")] + public int? InactiveCount { get; set; } + + [JsonPropertyName("items")] + public PersonWithSignificantControlNotification[]? Items { get; set; } + + [JsonPropertyName("items_per_page")] + public int? ItemsPerPage { get; set; } + + [JsonPropertyName("date_of_birth")] + public DateOfBirth? DateOfBirth { get; set; } + + [JsonPropertyName("kind")] + public PersonWithSignificantControlNotificationKind Kind { get; set; } + + [JsonPropertyName("links")] + public PersonWithSignificantControlNotificationsLinks? Links { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("start_index")] + public int? StartIndex { get; set; } + + [JsonPropertyName("total_results")] + public int? TotalResults { get; set; } + } +} diff --git a/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotificationsLinks.cs b/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotificationsLinks.cs new file mode 100644 index 0000000..6eb4e78 --- /dev/null +++ b/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotificationsLinks.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace CompaniesHouse.Response.PersonsWithSignificantControl +{ + public class PersonWithSignificantControlNotificationsLinks + { + [JsonPropertyName("self")] + public string? Self { get; set; } + } +} diff --git a/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotifiedTo.cs b/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotifiedTo.cs new file mode 100644 index 0000000..bcad0d9 --- /dev/null +++ b/src/CompaniesHouse/Response/PersonsWithSignificantControl/PersonWithSignificantControlNotifiedTo.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CompaniesHouse.Response.PersonsWithSignificantControl +{ + public class PersonWithSignificantControlNotifiedTo + { + [JsonPropertyName("company_name")] + public string? CompanyName { get; set; } + + [JsonPropertyName("company_number")] + public string? CompanyNumber { get; set; } + + [JsonPropertyName("company_status")] + public CompanyStatus CompanyStatus { get; set; } + } +} diff --git a/src/CompaniesHouse/UriBuilders/IPersonsWithSignificantControlDetailsUriBuilder.cs b/src/CompaniesHouse/UriBuilders/IPersonsWithSignificantControlDetailsUriBuilder.cs index 56ed63c..8eb14b8 100644 --- a/src/CompaniesHouse/UriBuilders/IPersonsWithSignificantControlDetailsUriBuilder.cs +++ b/src/CompaniesHouse/UriBuilders/IPersonsWithSignificantControlDetailsUriBuilder.cs @@ -16,6 +16,8 @@ public interface IPersonsWithSignificantControlDetailsUriBuilder Uri BuildLegalPersonBeneficialOwner(string companyNumber, string notificationId); + Uri BuildNotifications(string companyNumber, string pscId, string? filter, int startIndex, int pageSize); + Uri BuildStatementsList(string companyNumber, int startIndex, int pageSize, bool? registerView); Uri BuildStatement(string companyNumber, string statementId); diff --git a/src/CompaniesHouse/UriBuilders/PersonsWithSignificantControlDetailsUriBuilder.cs b/src/CompaniesHouse/UriBuilders/PersonsWithSignificantControlDetailsUriBuilder.cs index 7181fcf..b928595 100644 --- a/src/CompaniesHouse/UriBuilders/PersonsWithSignificantControlDetailsUriBuilder.cs +++ b/src/CompaniesHouse/UriBuilders/PersonsWithSignificantControlDetailsUriBuilder.cs @@ -36,6 +36,21 @@ public Uri BuildLegalPersonBeneficialOwner(string companyNumber, string notifica return Build(companyNumber, $"persons-with-significant-control/legal-person-beneficial-owner/{Uri.EscapeDataString(notificationId)}"); } + public Uri BuildNotifications(string companyNumber, string pscId, string? filter, int startIndex, int pageSize) + { + var queryParts = new List(); + + if (!string.IsNullOrWhiteSpace(filter)) + { + queryParts.Add("filter=" + Uri.EscapeDataString(filter)); + } + + queryParts.Add("items_per_page=" + pageSize.ToString(CultureInfo.InvariantCulture)); + queryParts.Add("start_index=" + startIndex.ToString(CultureInfo.InvariantCulture)); + + return Build(companyNumber, $"persons-with-significant-control/{Uri.EscapeDataString(pscId)}/notifications?{string.Join("&", queryParts)}"); + } + public Uri BuildStatementsList(string companyNumber, int startIndex, int pageSize, bool? registerView) { var queryParts = new List diff --git a/src/CompaniesHouse/enum-map.txt b/src/CompaniesHouse/enum-map.txt index 99d26d0..62cd1b7 100644 --- a/src/CompaniesHouse/enum-map.txt +++ b/src/CompaniesHouse/enum-map.txt @@ -29,4 +29,5 @@ insolvency_status|CompaniesHouse.Response.Insolvency|InsolvencyStatus|false insolvency_case_date_type|CompaniesHouse.Response.Insolvency|CaseDateType|false insolvency_case_type|CompaniesHouse.Response.Insolvency|InsolvencyCaseType|true person_with_significant_control_kind|CompaniesHouse.Response.PersonsWithSignificantControl|PersonWithSignificantControlKind|false +person_with_significant_control_notification_kind|CompaniesHouse.Response.PersonsWithSignificantControl|PersonWithSignificantControlNotificationKind|false person_with_significant_control_nature_of_control|CompaniesHouse.Response.PersonsWithSignificantControl|PersonWithSignificantControlNatureOfControl|false diff --git a/tests/CompaniesHouse.ScenarioTests/ScenarioFactAttribute.cs b/tests/CompaniesHouse.ScenarioTests/ScenarioFactAttribute.cs new file mode 100644 index 0000000..35c964b --- /dev/null +++ b/tests/CompaniesHouse.ScenarioTests/ScenarioFactAttribute.cs @@ -0,0 +1,16 @@ +using System; +using Xunit; + +namespace CompaniesHouse.ScenarioTests +{ + public sealed class ScenarioFactAttribute : FactAttribute + { + public ScenarioFactAttribute() + { + if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("COMPANIES_HOUSE_API_KEY"))) + { + Skip = "COMPANIES_HOUSE_API_KEY environment variable is not set - skipping scenario test that calls the real API."; + } + } + } +} diff --git a/tests/CompaniesHouse.ScenarioTests/SearchForAnOfficerAndFetchCorrespondingCompanyScenarioTests.cs b/tests/CompaniesHouse.ScenarioTests/SearchForAnOfficerAndFetchCorrespondingCompanyScenarioTests.cs index ee3ee71..c23b651 100644 --- a/tests/CompaniesHouse.ScenarioTests/SearchForAnOfficerAndFetchCorrespondingCompanyScenarioTests.cs +++ b/tests/CompaniesHouse.ScenarioTests/SearchForAnOfficerAndFetchCorrespondingCompanyScenarioTests.cs @@ -16,7 +16,7 @@ public SearchForAnOfficerAndFetchCorrespondingCompanyScenarioTests() _client = new CompaniesHouseClient(settings); } - [Fact] + [ScenarioFact] public async Task RunScenario() { var officersSearch = await _client.SearchOfficerAsync(new SearchOfficerRequest() { Query = "Richard Branson" }); diff --git a/tests/CompaniesHouse.ScenarioTests/SearchResponseDeserializationScenarioTests.cs b/tests/CompaniesHouse.ScenarioTests/SearchResponseDeserializationScenarioTests.cs index 7b64c43..d9ad5cd 100644 --- a/tests/CompaniesHouse.ScenarioTests/SearchResponseDeserializationScenarioTests.cs +++ b/tests/CompaniesHouse.ScenarioTests/SearchResponseDeserializationScenarioTests.cs @@ -7,6 +7,7 @@ using CompaniesHouse.Response.Search.CompanySearch; using CompaniesHouse.Response.Search.DissolvedCompaniesSearch; using CompaniesHouse.Response.Search.OfficerSearch; +using CompaniesHouse.Response.Search; using Shouldly; using Xunit; diff --git a/tests/CompaniesHouse.ScenarioTests/UsingMicrosoftServiceContainerTests.cs b/tests/CompaniesHouse.ScenarioTests/UsingMicrosoftServiceContainerTests.cs index 508c428..cdf9d85 100644 --- a/tests/CompaniesHouse.ScenarioTests/UsingMicrosoftServiceContainerTests.cs +++ b/tests/CompaniesHouse.ScenarioTests/UsingMicrosoftServiceContainerTests.cs @@ -8,7 +8,7 @@ namespace CompaniesHouse.ScenarioTests { public class SearchingCompaniesUsingMicrosoftServiceContainerTests { - [Fact] + [ScenarioFact] public async Task CanResolveCompaniesHouseClients() { var serviceCollection = new ServiceCollection(); diff --git a/tests/CompaniesHouse.Tests/CompaniesHousePersonsWithSignificantControlDetailsClientTests/CompaniesHousePersonsWithSignificantControlDetailsClientTests.cs b/tests/CompaniesHouse.Tests/CompaniesHousePersonsWithSignificantControlDetailsClientTests/CompaniesHousePersonsWithSignificantControlDetailsClientTests.cs index 0b05469..96e3aa8 100644 --- a/tests/CompaniesHouse.Tests/CompaniesHousePersonsWithSignificantControlDetailsClientTests/CompaniesHousePersonsWithSignificantControlDetailsClientTests.cs +++ b/tests/CompaniesHouse.Tests/CompaniesHousePersonsWithSignificantControlDetailsClientTests/CompaniesHousePersonsWithSignificantControlDetailsClientTests.cs @@ -1,6 +1,7 @@ using System; using System.Net.Http; using System.Threading.Tasks; +using CompaniesHouse.Response; using CompaniesHouse.Response.PersonsWithSignificantControl; using CompaniesHouse.UriBuilders; using Moq; @@ -43,6 +44,27 @@ public async Task GivenCapturedStatementList_WhenGettingStatementsList_ThenEnvel result.Data.Items[0].Links.Self.ShouldBe("/company/05124262/persons-with-significant-control-statements/8xxEeFpu5Xmpf1ce1FmwM-sK8J8"); } + [Fact] + public async Task GivenCapturedNotifications_WhenGettingNotifications_ThenEnvelopeAndItemsDeserialize() + { + var uri = new Uri("https://wibble.com/company/05124262/persons-with-significant-control/abc/notifications"); + HttpMessageHandler handler = new StubHttpMessageHandler(uri, NotificationsJson); + var uriBuilder = CreateUriBuilder(uri); + + var client = new CompaniesHousePersonsWithSignificantControlDetailsClient(new HttpClient(handler), uriBuilder.Object); + var result = await client.GetNotificationsAsync("05124262", "abc", "active", 0, 25); + + result.Data.TotalResults.ShouldBe(1); + var items = result.Data.Items.ShouldNotBeNull(); + items.Length.ShouldBe(1); + items[0].Kind.ShouldBe(new PersonWithSignificantControlKind("individual-person-with-significant-control")); + items[0].NotifiedTo?.CompanyNumber.ShouldBe("05124262"); + items[0].NotifiedTo?.CompanyStatus.ShouldBe(new CompanyStatus("active")); + result.Data.Kind.ShouldBe(new PersonWithSignificantControlNotificationKind("personal-notification")); + result.Data.DateOfBirth?.Month.ShouldBe(7); + result.Data.DateOfBirth?.Year.ShouldBe(1979); + } + [Fact] public async Task GivenCapturedSuperSecure_WhenGettingSuperSecurePsc_ThenObservedFieldsDeserialize() { @@ -69,6 +91,7 @@ private static Mock CreateUriBu uriBuilder.Setup(x => x.BuildCorporateEntityBeneficialOwner(It.IsAny(), It.IsAny())).Returns(uri); uriBuilder.Setup(x => x.BuildLegalPerson(It.IsAny(), It.IsAny())).Returns(uri); uriBuilder.Setup(x => x.BuildLegalPersonBeneficialOwner(It.IsAny(), It.IsAny())).Returns(uri); + uriBuilder.Setup(x => x.BuildNotifications(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(uri); uriBuilder.Setup(x => x.BuildStatementsList(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(uri); uriBuilder.Setup(x => x.BuildStatement(It.IsAny(), It.IsAny())).Returns(uri); uriBuilder.Setup(x => x.BuildSuperSecure(It.IsAny(), It.IsAny())).Returns(uri); @@ -95,6 +118,7 @@ private static Mock CreateUriBu private const string StatementListJson = """ { "items_per_page":25, + "date_of_birth":{"month":7,"year":1979}, "items":[ { "etag":"95ca7497819e5fbc1144b6a3ef09f477228f3f5f", @@ -127,5 +151,30 @@ private static Mock CreateUriBu "links":{"self":"/company/1/persons-with-significant-control/super-secure/2"} } """; + + private const string NotificationsJson = """ + { + "active_count":1, + "ceased_count":0, + "inactive_count":0, + "items_per_page":25, + "items":[ + { + "etag":"abc", + "kind":"individual-person-with-significant-control", + "name":"Chris Brown", + "notified_on":"2019-01-16", + "natures_of_control":["ownership-of-shares-25-to-50-percent"], + "notified_to":{"company_number":"05124262","company_name":"Example Ltd","company_status":"active"}, + "links":{"company":"/company/05124262"} + } + ], + "kind":"personal-notification", + "name":"Chris Brown", + "start_index":0, + "total_results":1, + "links":{"self":"/company/05124262/persons-with-significant-control/abc/notifications"} + } + """; } } diff --git a/tests/CompaniesHouse.Tests/UriBuilders/PersonsWithSignificantControlDetailsUriBuilderTests/PersonsWithSignificantControlDetailsUriBuilderTests.cs b/tests/CompaniesHouse.Tests/UriBuilders/PersonsWithSignificantControlDetailsUriBuilderTests/PersonsWithSignificantControlDetailsUriBuilderTests.cs index e09ad3a..01591d3 100644 --- a/tests/CompaniesHouse.Tests/UriBuilders/PersonsWithSignificantControlDetailsUriBuilderTests/PersonsWithSignificantControlDetailsUriBuilderTests.cs +++ b/tests/CompaniesHouse.Tests/UriBuilders/PersonsWithSignificantControlDetailsUriBuilderTests/PersonsWithSignificantControlDetailsUriBuilderTests.cs @@ -51,6 +51,13 @@ public void BuildLegalPersonBeneficialOwner_EncodesIds() .ShouldBe(new Uri("company/00%2F123/persons-with-significant-control/legal-person-beneficial-owner/a%2Fb", UriKind.Relative)); } + [Fact] + public void BuildNotifications_IncludesFilterAndPaging() + { + _builder.BuildNotifications("00/123", "a/b", "active", 25, 10) + .ShouldBe(new Uri("company/00%2F123/persons-with-significant-control/a%2Fb/notifications?filter=active&items_per_page=10&start_index=25", UriKind.Relative)); + } + [Fact] public void BuildStatementsList_IncludesPagingAndOptionalRegisterView() {