Repository: VictorAvelar/mollie-api-go
Pull request #276 (2023-08-10 08:07:10)

Client links feature
<!--- Provide a general summary of your changes in the Title above -->

# Description

Add service to interact with Client Links (currently in BETA).

## Motivation and context

Offer full API coverage.

## How has this been tested?

Please describe in detail how you tested your changes.

Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc.

- [x] Unit tests added / updated
- [x] The tests run on docker (using `make test`)
- [x] The required `test data` has been added / updated

If you are running the tests locally, please specify:

- running in devcontainer.

## Types of changes

What types of changes does your code introduce? Put an `x` in all the boxes that apply:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [x] This change requires a documentation update

## Checklist

Go over all the following points, and put an `x` in all the boxes that apply.

Please, please, please, don't send your pull request until all of the boxes are ticked. Once your pull request is created, it will trigger a build on our continuous integration server to make sure your [tests and code style pass](https://help.github.com/articles/about-required-status-checks/).

- [x] I have read the **[CONTRIBUTING](CONTRIBUTING.md)** document.
- [x] My pull request addresses exactly one patch/feature.
- [x] I have created a branch for this patch/feature.
- [x] Each individual commit in the pull request is meaningful.
- [x] If my change requires a change to the documentation, I have updated it accordingly.

<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->



Patch:
diff --git a/docs/README.md b/docs/README.md
index 1574727..5343869 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -121,6 +121,24 @@ type ApplicationFee struct {
 ApplicationFee allows you to split a payment between a platform and connected
 merchant accounts.
 
+#### type ApprovalPromptAction
+
+```go
+type ApprovalPromptAction string
+```
+
+ApprovalPromptAction represents possible actions to be performed once the client
+link is created and redirected to the dashboard.
+
+```go
+const (
+ ForceApproval ApprovalPromptAction = "force"
+ AutoApproval  ApprovalPromptAction = "auto"
+)
+```
+
+Possible approval prompt actions.
+
 #### type Balance
 
 ```go
@@ -926,6 +944,7 @@ type Client struct {
  PaymentLinks   *PaymentLinksService
  Partners       *PartnerService
  Balances       *BalancesService
+ ClientLinks    *ClientLinksService
 }
 ```
 
@@ -1004,6 +1023,94 @@ Ideally your API key will be provided from and environment variable or a secret
 management engine. This should only be used when environment variables are
 "impossible" to be used.
 
+#### type ClientDetails
+
+```go
+type ClientDetails struct {
+ Owner              Owner    `json:"owner,omitempty"`
+ Name               string   `json:"name,omitempty"`
+ Address            *Address `json:"address,omitempty"`
+ RegistrationNumber string   `json:"registrationNumber,omitempty"`
+ VATNumber          string   `json:"vatNumber,omitempty"`
+}
+```
+
+ClientDetails contains information to link a new organization to an OAuth
+application.
+
+#### type ClientLink
+
+```go
+type ClientLink struct {
+ ID       string          `json:"id,omitempty"`
+ Resource string          `json:"resource,omitempty"`
+ Links    ClientLinkLinks `json:"_links,omitempty"`
+}
+```
+
+ClientLink object with redirect target.
+
+#### type ClientLinkFinalizeOptions
+
+```go
+type ClientLinkFinalizeOptions struct {
+ ClientID       string `url:"clientID,omitempty"`
+ State          string `url:"state,omitempty"`
+ Scope          string `url:"scope,omitempty"`
+ ApprovalPrompt string `url:"approvalPrompt,omitempty"`
+}
+```
+
+ClientLinkFinalizeOptions subset of the parameters allowed for the Authorize
+endpoint.
+
+#### type ClientLinkLinks
+
+```go
+type ClientLinkLinks struct {
+ ClientLink    *URL `json:"clientLink,omitempty"`
+ Documentation *URL `json:"documentation,omitempty"`
+}
+```
+
+ClientLinkLinks describes all the possible links to be returned with a client
+links response object.
+
+#### type ClientLinksService
+
+```go
+type ClientLinksService service
+```
+
+ClientLinksService interacts with the Client Links API to create new
+organizations for your customers.
+
+#### func (*ClientLinksService) CreateClientLink
+
+```go
+func (cls *ClientLinksService) CreateClientLink(ctx context.Context, cd *ClientDetails) (
+ res *Response,
+ cl *ClientLink,
+ err error,
+)
+```
+
+CreateClientLink based on the provided ClientDetails.
+
+See: <https://docs.mollie.com/reference/v2/client-links-api/create-client-link>
+
+#### func (*ClientLinksService) CreateFinalizeClientLink
+
+```go
+func (cls *ClientLinksService) CreateFinalizeClientLink(
+ ctx context.Context,
+ clientLink string,
+ options *ClientLinkFinalizeOptions,
+) (
+ clientLinkURI string,
+)
+```
+
 #### type Commission
 
 ```go
@@ -2708,6 +2815,19 @@ authenticated organization.
 
 See: <https://docs.mollie.com/reference/v2/organizations-api/get-partner>
 
+#### type Owner
+
+```go
+type Owner struct {
+ Email      string `json:"email,omitempty"`
+ GivenName  string `json:"givenName,omitempty"`
+ FamilyName string `json:"familyName,omitempty"`
+ Locale     Locale `json:"locale,omitempty"`
+}
+```
+
+Personal data of your customer.
+
 #### type PaginationLinks
 
 ```go
diff --git a/mollie/client_links.go b/mollie/client_links.go
new file mode 100644
index 0000000..3646293
--- /dev/null
+++ b/mollie/client_links.go
@@ -0,0 +1,90 @@
+package mollie
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/google/go-querystring/query"
+)
+
+// ClientLinksService interacts with the Client Links API to create
+// new organizations for your customers.
+type ClientLinksService service
+
+// ClientDetails contains information to link a new organization to an
+// OAuth application.
+type ClientDetails struct {
+	Owner              Owner    `json:"owner,omitempty"`
+	Name               string   `json:"name,omitempty"`
+	Address            *Address `json:"address,omitempty"`
+	RegistrationNumber string   `json:"registrationNumber,omitempty"`
+	VATNumber          string   `json:"vatNumber,omitempty"`
+}
+
+// ClientLinkLinks describes all the possible links to be returned with
+// a client links response object.
+type ClientLinkLinks struct {
+	ClientLink    *URL `json:"clientLink,omitempty"`
+	Documentation *URL `json:"documentation,omitempty"`
+}
+
+// ClientLink object with redirect target.
+type ClientLink struct {
+	ID       string          `json:"id,omitempty"`
+	Resource string          `json:"resource,omitempty"`
+	Links    ClientLinkLinks `json:"_links,omitempty"`
+}
+
+// CreateClientLink based on the provided ClientDetails.
+//
+// See: https://docs.mollie.com/reference/v2/client-links-api/create-client-link
+func (cls *ClientLinksService) CreateClientLink(ctx context.Context, cd *ClientDetails) (
+	res *Response,
+	cl *ClientLink,
+	err error,
+) {
+	res, err = cls.client.post(ctx, "v2/client-links", cd, nil)
+	if err != nil {
+		return
+	}
+
+	if err = json.Unmarshal(res.content, &cl); err != nil {
+		return
+	}
+
+	return
+}
+
+// ApprovalPromptAction represents possible actions to be performed
+// once the client link is created and redirected to the dashboard.
+type ApprovalPromptAction string
+
+// Possible approval prompt actions.
+const (
+	ForceApproval ApprovalPromptAction = "force"
+	AutoApproval  ApprovalPromptAction = "auto"
+)
+
+// ClientLinkFinalizeOptions subset of the parameters allowed for the Authorize endpoint.
+type ClientLinkFinalizeOptions struct {
+	ClientID       string `url:"clientID,omitempty"`
+	State          string `url:"state,omitempty"`
+	Scope          string `url:"scope,omitempty"`
+	ApprovalPrompt string `url:"approvalPrompt,omitempty"`
+}
+
+func (cls *ClientLinksService) CreateFinalizeClientLink(
+	ctx context.Context,
+	clientLink string,
+	options *ClientLinkFinalizeOptions,
+) (
+	clientLinkURI string,
+) {
+	if options != nil {
+		v, _ := query.Values(options)
+		clientLinkURI = fmt.Sprintf("%s?%s", clientLink, v.Encode())
+	}
+
+	return
+}
diff --git a/mollie/common_types.go b/mollie/common_types.go
index bf8c81b..e990df6 100644
--- a/mollie/common_types.go
+++ b/mollie/common_types.go
@@ -344,3 +344,11 @@ type Image struct {
 	Size2X string `json:"size2X,omitempty"`
 	Svg    string `json:"svg,omitempty"`
 }
+
+// Personal data of your customer.
+type Owner struct {
+	Email      string `json:"email,omitempty"`
+	GivenName  string `json:"givenName,omitempty"`
+	FamilyName string `json:"familyName,omitempty"`
+	Locale     Locale `json:"locale,omitempty"`
+}
diff --git a/mollie/mollie.go b/mollie/mollie.go
index ea214d3..974705d 100644
--- a/mollie/mollie.go
+++ b/mollie/mollie.go
@@ -66,6 +66,7 @@ type Client struct {
 	PaymentLinks   *PaymentLinksService
 	Partners       *PartnerService
 	Balances       *BalancesService
+	ClientLinks    *ClientLinksService
 }
 
 type service struct {
@@ -300,6 +301,7 @@ func NewClient(baseClient *http.Client, conf *Config) (mollie *Client, err error
 	mollie.PaymentLinks = (*PaymentLinksService)(&mollie.common)
 	mollie.Partners = (*PartnerService)(&mollie.common)
 	mollie.Balances = (*BalancesService)(&mollie.common)
+	mollie.ClientLinks = (*ClientLinksService)(&mollie.common)
 
 	mollie.userAgent = strings.Join([]string{
 		runtime.GOOS,


Test patch:
diff --git a/mollie/client_links_test.go b/mollie/client_links_test.go
new file mode 100644
index 0000000..366bec2
--- /dev/null
+++ b/mollie/client_links_test.go
@@ -0,0 +1,170 @@
+package mollie
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/VictorAvelar/mollie-api-go/v3/testdata"
+	"github.com/stretchr/testify/suite"
+)
+
+type clientLinkSuite struct{ suite.Suite }
+
+func (cls *clientLinkSuite) SetupSuite() { setEnv() }
+
+func (cls *clientLinkSuite) TearDownSuite() { unsetEnv() }
+
+func (cls *clientLinkSuite) TestCreateClientLink() {
+	type args struct {
+		ctx context.Context
+		cd  *ClientDetails
+	}
+
+	cases := []struct {
+		name    string
+		args    args
+		wantErr bool
+		err     error
+		handler http.HandlerFunc
+		pre     func()
+	}{
+		{
+			"create new client link",
+			args{
+				context.Background(),
+				&ClientDetails{},
+			},
+			false,
+			nil,
+			func(w http.ResponseWriter, r *http.Request) {
+				testHeader(cls.T(), r, AuthHeader, "Bearer token_X12b31ggg23")
+				testMethod(cls.T(), r, "POST")
+				if _, ok := r.Header[AuthHeader]; !ok {
+					w.WriteHeader(http.StatusUnauthorized)
+				}
+
+				_, _ = w.Write([]byte(testdata.CreateClientLinkResponse))
+			},
+			noPre,
+		},
+		{
+			"create client link, an error is returned from the server",
+			args{
+				context.Background(),
+				&ClientDetails{},
+			},
+			true,
+			fmt.Errorf("500 Internal Server Error: An internal server error occurred while processing your request."),
+			errorHandler,
+			noPre,
+		},
+		{
+			"create client link, an error occurs when parsing json",
+			args{
+				context.Background(),
+				&ClientDetails{},
+			},
+			true,
+			fmt.Errorf("invalid character 'h' looking for beginning of object key string"),
+			encodingHandler,
+			noPre,
+		},
+		{
+			"create client link, invalid url when building request",
+			args{
+				context.Background(),
+				&ClientDetails{},
+			},
+			true,
+			errBadBaseURL,
+			errorHandler,
+			crashSrv,
+		},
+	}
+
+	for _, c := range cases {
+		setup()
+		defer teardown()
+		cls.T().Run(c.name, func(t *testing.T) {
+			tMux.HandleFunc(
+				"/v2/client-links",
+				c.handler,
+			)
+			c.pre()
+
+			res, cb, err := tClient.ClientLinks.CreateClientLink(c.args.ctx, c.args.cd)
+			if c.wantErr {
+				cls.Error(err)
+				cls.EqualError(err, c.err.Error())
+			} else {
+				cls.Nil(err)
+				cls.Same(c.args.ctx, res.Request.Context())
+				cls.IsType(&ClientLink{}, cb)
+				cls.IsType(&http.Response{}, res.Response)
+			}
+		})
+	}
+}
+
+func (cls *clientLinkSuite) TestCreateFinalizeClientLink() {
+	type args struct {
+		ctx        context.Context
+		clientLink string
+		options    *ClientLinkFinalizeOptions
+	}
+	tests := []struct {
+		name              string
+		args              args
+		wantClientLinkURI string
+	}{
+		{
+			"constructs client link finalize step correctly.",
+			args{
+				context.Background(),
+				"https://my.mollie.com/dashboard/client-link/finalize/csr_vZCnNQsV2UtfXxYifWKWH",
+				&ClientLinkFinalizeOptions{
+					ClientID: "app_j9Pakf56Ajta6Y65AkdTtAv",
+					State:    "decafbad",
+					Scope:    "onboarding.read+organization.read+payments.write+payments.read+profiles.write",
+				},
+			},
+			"https://my.mollie.com/dashboard/client-link/finalize/csr_vZCnNQsV2UtfXxYifWKWH?clientID=app_j9Pakf56Ajta6Y65AkdTtAv&scope=onboarding.read%2Borganization.read%2Bpayments.write%2Bpayments.read%2Bprofiles.write&state=decafbad",
+		},
+		{
+			"constructs client link finalize with complex values",
+			args{
+				context.Background(),
+				"https://my.mollie.com/dashboard/client-link/finalize/csr_vZCnNQsV2UtfXxYifWKWH",
+				&ClientLinkFinalizeOptions{
+					ClientID: "",
+					State:    "\ns\\s\\s\\s\n",
+					Scope:    "",
+				},
+			},
+			"https://my.mollie.com/dashboard/client-link/finalize/csr_vZCnNQsV2UtfXxYifWKWH?state=%0As%5Cs%5Cs%5Cs%0A",
+		},
+		{
+			"constructs client link finalize with complex values",
+			args{
+				context.Background(),
+				"https://my.mollie.com/dashboard/client-link/finalize/csr_vZCnNQsV2UtfXxYifWKWH",
+				&ClientLinkFinalizeOptions{},
+			},
+			"https://my.mollie.com/dashboard/client-link/finalize/csr_vZCnNQsV2UtfXxYifWKWH?",
+		},
+	}
+	for _, tt := range tests {
+		cls.T().Run(tt.name, func(t *testing.T) {
+			gotClientLinkURI := tClient.ClientLinks.CreateFinalizeClientLink(tt.args.ctx, tt.args.clientLink, tt.args.options)
+			if gotClientLinkURI != tt.wantClientLinkURI {
+				t.Errorf("ClientLinksService.CreateFinalizeClientLink() = %v, want %v", gotClientLinkURI, tt.wantClientLinkURI)
+			}
+		})
+	}
+}
+
+func TestClientLinks(t *testing.T) {
+	suite.Run(t, new(clientLinkSuite))
+}
diff --git a/testdata/client_links.go b/testdata/client_links.go
new file mode 100644
index 0000000..e9d263e
--- /dev/null
+++ b/testdata/client_links.go
@@ -0,0 +1,17 @@
+package testdata
+
+// CreateClientLinkResponse examples.
+const CreateClientLinkResponse = `{
+    "id": "csr_vZCnNQsV2UtfXxYifWKWH",
+    "resource": "client-link",
+    "_links": {
+        "clientLink": {
+            "href": "https://my.mollie.com/dashboard/client-link/finalize/csr_vZCnNQsV2UtfXxYifWKWH",
+            "type": "text/html"
+        },
+        "documentation": {
+            "href": "https://docs.mollie.com/reference/v2/clients-api/create-client-link",
+            "type": "text/html"
+        }
+    }
+}`