From e00ec77d2874ced91adc1a9f3fb84c168d0aaa84 Mon Sep 17 00:00:00 2001 From: Vincent Kok Date: Tue, 12 Sep 2023 18:46:26 +0200 Subject: [PATCH] Add support for the client link api (#325) * Create boilerplate code for the ClientLink API * Add unit test for create client link endpoint * Add method to generate a client link URL with parameters * Add documentation on ClientLinkClient * Fix spacing issue in documentation links * Increase version number to 3.2.0.0 --------- Co-authored-by: Vincent Kok --- README.md | 39 ++++++++ .../Client/Abstract/IClientLinkClient.cs | 18 ++++ src/Mollie.Api/Client/ClientLinkClient.cs | 42 +++++++++ src/Mollie.Api/DependencyInjection.cs | 2 + .../ClientLink/Request/ClientLinkOwner.cs | 28 ++++++ .../ClientLink/Request/ClientLinkRequest.cs | 33 +++++++ .../ClientLink/Response/ClientLinkResponse.cs | 14 +++ .../Response/ClientLinkResponseLinks.cs | 10 +++ src/Mollie.Api/Mollie.Api.csproj | 8 +- .../Client/ClientLinkClientTests.cs | 89 +++++++++++++++++++ 10 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 src/Mollie.Api/Client/Abstract/IClientLinkClient.cs create mode 100644 src/Mollie.Api/Client/ClientLinkClient.cs create mode 100644 src/Mollie.Api/Models/ClientLink/Request/ClientLinkOwner.cs create mode 100644 src/Mollie.Api/Models/ClientLink/Request/ClientLinkRequest.cs create mode 100644 src/Mollie.Api/Models/ClientLink/Response/ClientLinkResponse.cs create mode 100644 src/Mollie.Api/Models/ClientLink/Response/ClientLinkResponseLinks.cs create mode 100644 tests/Mollie.Tests.Unit/Client/ClientLinkClientTests.cs diff --git a/README.md b/README.md index 86187dcd..39396b75 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Have you spotted a bug or want to add a missing feature? All pull requests are w [14. Payment link Api](#14-payment-link-api) [15. Balances Api](#15-balances-api) [16. Terminal Api](#16-terminal-api) +[17. Client Link Api](#17-client-link-api) ## 1. Getting started @@ -75,6 +76,7 @@ This library currently supports the following API's: - Onboarding API - Balances API - Terminal API +- ClientLink API ### Supported .NET versions This library is built using .NET standard 2.0. This means that the package supports the following .NET implementations: @@ -984,3 +986,40 @@ using ITerminalClient client = new TerminalClient({yourApiKey}); TerminalResponse response = await client.GetTerminalAsync({yourTerminalId}); ``` +## 17. Client Link Api +### Create a client link +Link a new organization to your OAuth application, in effect creating a new client. +```C# +var request = new ClientLinkRequest +{ + Owner = new ClientLinkOwner + { + Email = "norris@chucknorrisfacts.net", + GivenName = "Chuck", + FamilyName = "Norris", + Locale = "en_US" + }, + Address = new AddressObject() + { + StreetAndNumber = "Keizersgracht 126", + PostalCode = "1015 CW", + City = "Amsterdam", + Country = "NL" + }, + Name = "Mollie B.V.", + RegistrationNumber = "30204462", + VatNumber = "NL815839091B01" +}; +using IClientLinkClient client = new ClientLinkClient({yourClientId}, {yourClientSecret}); +ClientLinkResponse response = await clientLinkClient.CreateClientLinkAsync(request); +``` + +### Generate the client link URL +To the ClientLink link you created through the API, you can then add the OAuth details of your application, the client_id, scope you want to request +```C# +using IClientLinkClient client = new ClientLinkClient({yourClientId}, {yourClientSecret}); +ClientLinkResponse response = await clientLinkClient.CreateClientLinkAsync(request); +var clientLinkUrl = response.Links.ClientLink; +string result = clientLinkClient.GenerateClientLinkWithParameters(clientLinkUrl, {yourState}, {yourScope}, {forceApprovalPrompt}); +``` + diff --git a/src/Mollie.Api/Client/Abstract/IClientLinkClient.cs b/src/Mollie.Api/Client/Abstract/IClientLinkClient.cs new file mode 100644 index 00000000..f5e1f2e9 --- /dev/null +++ b/src/Mollie.Api/Client/Abstract/IClientLinkClient.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Mollie.Api.Models.ClientLink.Request; +using Mollie.Api.Models.ClientLink.Response; + +namespace Mollie.Api.Client.Abstract +{ + public interface IClientLinkClient + { + Task CreateClientLinkAsync(ClientLinkRequest request); + + string GenerateClientLinkWithParameters( + string clientLinkUrl, + string state, + List scopes, + bool forceApprovalPrompt = false); + } +} \ No newline at end of file diff --git a/src/Mollie.Api/Client/ClientLinkClient.cs b/src/Mollie.Api/Client/ClientLinkClient.cs new file mode 100644 index 00000000..26b0221d --- /dev/null +++ b/src/Mollie.Api/Client/ClientLinkClient.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Mollie.Api.Client.Abstract; +using Mollie.Api.Extensions; +using Mollie.Api.Models.ClientLink.Request; +using Mollie.Api.Models.ClientLink.Response; + +namespace Mollie.Api.Client { + public class ClientLinkClient : OauthBaseMollieClient, IClientLinkClient + { + private readonly string _clientId; + + public ClientLinkClient(string clientId, string oauthAccessToken, HttpClient httpClient = null) + : base(oauthAccessToken, httpClient) + { + this._clientId = clientId; + } + + public async Task CreateClientLinkAsync(ClientLinkRequest request) + { + return await this.PostAsync($"client-links", request) + .ConfigureAwait(false); + } + + public string GenerateClientLinkWithParameters( + string clientLinkUrl, + string state, + List scopes, + bool forceApprovalPrompt = false) + { + var parameters = new Dictionary { + {"client_id", _clientId}, + {"state", state}, + {"scope", string.Join(" ", scopes)}, + {"approval_prompt", forceApprovalPrompt ? "force" : "auto"} + }; + + return clientLinkUrl + parameters.ToQueryString(); + } + } +} \ No newline at end of file diff --git a/src/Mollie.Api/DependencyInjection.cs b/src/Mollie.Api/DependencyInjection.cs index 7e2c3e9a..15a3ad30 100644 --- a/src/Mollie.Api/DependencyInjection.cs +++ b/src/Mollie.Api/DependencyInjection.cs @@ -56,6 +56,8 @@ public static IServiceCollection AddMollieApi( new SubscriptionClient(mollieOptions.ApiKey, httpClient), retryPolicy); RegisterMollieApiClient(services, httpClient => new TerminalClient(mollieOptions.ApiKey, httpClient), retryPolicy); + RegisterMollieApiClient(services, httpClient => + new ClientLinkClient(mollieOptions.ClientId, mollieOptions.ApiKey, httpClient), retryPolicy); return services; } diff --git a/src/Mollie.Api/Models/ClientLink/Request/ClientLinkOwner.cs b/src/Mollie.Api/Models/ClientLink/Request/ClientLinkOwner.cs new file mode 100644 index 00000000..466a7170 --- /dev/null +++ b/src/Mollie.Api/Models/ClientLink/Request/ClientLinkOwner.cs @@ -0,0 +1,28 @@ +namespace Mollie.Api.Models.ClientLink.Request +{ + public class ClientLinkOwner + { + /// + /// The email address of your customer. + /// + public string Email { get; set; } + + /// + /// The given name (first name) of your customer. + /// + public string GivenName { get; set; } + + /// + /// The family name (surname) of your customer. + /// + public string FamilyName { get; set; } + + /// + /// Allows you to preset the language to be used in the login / authorize flow. When this parameter is + /// omitted, the browser language will be used instead. You can provide any xx_XX format ISO 15897 locale, + /// but the authorize flow currently only supports the following languages: + /// en_US nl_NL nl_BE fr_FR fr_BE de_DE es_ES it_IT + /// + public string Locale { get; set; } + } +} \ No newline at end of file diff --git a/src/Mollie.Api/Models/ClientLink/Request/ClientLinkRequest.cs b/src/Mollie.Api/Models/ClientLink/Request/ClientLinkRequest.cs new file mode 100644 index 00000000..db8b456d --- /dev/null +++ b/src/Mollie.Api/Models/ClientLink/Request/ClientLinkRequest.cs @@ -0,0 +1,33 @@ +namespace Mollie.Api.Models.ClientLink.Request +{ + public class ClientLinkRequest + { + /// + /// Personal data of your customer which is required for this endpoint. + /// + public ClientLinkOwner Owner { get; set; } + + /// + /// Name of the organization. + /// + public string Name { get; set; } + + /// + /// Address of the organization. Note that the country parameter + /// must always be provided. + /// + public AddressObject Address { get; set; } + + /// + /// The Chamber of Commerce (or local equivalent) registration number + /// of the organization. + /// + public string RegistrationNumber { get; set; } + + /// + /// The VAT number of the organization, if based in the European Union + /// or the United Kingdom. + /// + public string VatNumber { get; set; } + } +} \ No newline at end of file diff --git a/src/Mollie.Api/Models/ClientLink/Response/ClientLinkResponse.cs b/src/Mollie.Api/Models/ClientLink/Response/ClientLinkResponse.cs new file mode 100644 index 00000000..bdc2d95d --- /dev/null +++ b/src/Mollie.Api/Models/ClientLink/Response/ClientLinkResponse.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Mollie.Api.Models.ClientLink.Response +{ + public class ClientLinkResponse + { + public string Id { get; set; } + + public string Resource { get; set; } + + [JsonProperty("_links")] + public ClientLinkResponseLinks Links { get; set; } + } +} \ No newline at end of file diff --git a/src/Mollie.Api/Models/ClientLink/Response/ClientLinkResponseLinks.cs b/src/Mollie.Api/Models/ClientLink/Response/ClientLinkResponseLinks.cs new file mode 100644 index 00000000..d65d0908 --- /dev/null +++ b/src/Mollie.Api/Models/ClientLink/Response/ClientLinkResponseLinks.cs @@ -0,0 +1,10 @@ +using Mollie.Api.Models.Url; + +namespace Mollie.Api.Models.ClientLink.Response +{ + public class ClientLinkResponseLinks + { + public UrlLink ClientLink { get; set; } + public UrlLink Documentation { get; set; } + } +} \ No newline at end of file diff --git a/src/Mollie.Api/Mollie.Api.csproj b/src/Mollie.Api/Mollie.Api.csproj index 2b1d4d88..0f684e55 100644 --- a/src/Mollie.Api/Mollie.Api.csproj +++ b/src/Mollie.Api/Mollie.Api.csproj @@ -1,7 +1,7 @@  - 3.0.0.0 + 3.2.0.0 True Vincent Kok This is a wrapper for the Mollie REST webservice. All payment methods and webservice calls are supported. @@ -9,9 +9,9 @@ Mollie Payment API https://github.com/Viincenttt/MollieApi Mollie - 3.1.0.0 - 3.1.0.0 - 3.1.0.0 + 3.2.0.0 + 3.2.0.0 + 3.2.0.0 netstandard2.0 README.md LICENSE diff --git a/tests/Mollie.Tests.Unit/Client/ClientLinkClientTests.cs b/tests/Mollie.Tests.Unit/Client/ClientLinkClientTests.cs new file mode 100644 index 00000000..4f848f64 --- /dev/null +++ b/tests/Mollie.Tests.Unit/Client/ClientLinkClientTests.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using Mollie.Api.Client; +using Mollie.Api.Models.Chargeback; +using Mollie.Api.Models.ClientLink.Request; +using Mollie.Api.Models.ClientLink.Response; +using RichardSzalay.MockHttp; +using Xunit; + +namespace Mollie.Tests.Unit.Client; + +public class ClientLinkClientTests : BaseClientTests +{ + [Fact] + public async Task CreateClientLinkAsync_ResponseIsDeserializedInExpectedFormat() + { + // Given: We create a payment link + const string clientLinkId = "csr_vZCnNQsV2UtfXxYifWKWH"; + const string clientLinkUrl = "myurl"; + string clientLinkResponseJson = CreateClientLinkResponseJson(clientLinkId, clientLinkUrl); + var mockHttp = new MockHttpMessageHandler(); + mockHttp.When( HttpMethod.Post, $"{BaseMollieClient.ApiEndPoint}client-links") + .Respond("application/json", clientLinkResponseJson); + HttpClient httpClient = mockHttp.ToHttpClient(); + ClientLinkClient clientLinkClient = new ClientLinkClient("clientId", "access_1234", httpClient); + + // When: We send the request + ClientLinkResponse response = await clientLinkClient.CreateClientLinkAsync(new ClientLinkRequest()); + + // Then + response.Id.Should().Be(clientLinkId); + response.Links.ClientLink.Href.Should().Be(clientLinkUrl); + mockHttp.VerifyNoOutstandingRequest(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GenerateClientLinkWithParameters_GeneratesExpectedUrl(bool forceApprovalPrompt) + { + // Arrange + const string clientId = "app_j9Pakf56Ajta6Y65AkdTtAv"; + const string clientLinkUrl = "https://my.mollie.com/dashboard/client-link/csr_vZCnNQsV2UtfXxYifWKWH"; + const string state = "decafbad"; + var scopes = new List() + { + "onboarding.read", + "organizations.read", + "payments.write", + "payments.read", + "profiles.write", + }; + ClientLinkClient clientLinkClient = new ClientLinkClient( + clientId, "access_1234", new MockHttpMessageHandler().ToHttpClient()); + + // Act + string result = clientLinkClient.GenerateClientLinkWithParameters( + clientLinkUrl, state, scopes, forceApprovalPrompt); + + // Assert + string expectedApprovalPrompt = forceApprovalPrompt ? "force" : "auto"; + result.Should() + .Be("https://my.mollie.com/dashboard/client-link/csr_vZCnNQsV2UtfXxYifWKWH" + + $"?client_id={clientId}" + + $"&state={state}" + + "&scope=onboarding.read+organizations.read+payments.write+payments.read+profiles.write" + + $"&approval_prompt={expectedApprovalPrompt}"); + } + + private string CreateClientLinkResponseJson(string id, string clientLinkUrl) + { + return $@"{{ + ""id"": ""{id}"", + ""resource"": ""client-link"", + ""_links"": {{ + ""clientLink"": {{ + ""href"": ""{clientLinkUrl}"", + ""type"": ""text/html"" + }}, + ""documentation"": {{ + ""href"": ""https://docs.mollie.com/reference/v2/clients-api/create-client-link"", + ""type"": ""text/html"" + }} + }} +}}"; + } +} \ No newline at end of file