Repository: VictorAvelar/mollie-api-go (go)

Issue:
Integrate the new partners API
**Is your feature request related to a problem? Please describe.**

Wednesday, 20th¶
-  Added a new Partners section containing Get client and List client endpoints.

**Describe the solution you'd like**
New resources are implemented with a Partners service and tests are included.

**Additional context**
New Partners API area was release on October 20th by Mollie.

[Official API reference](https://docs.mollie.com/reference/v2/partners-api/get-client)


Pull request description:
Add Mollie Partners API
<!--- Provide a general summary of your changes in the Title above -->

## Description

Implementation to list and get partner clients from Mollie's API.

## Motivation and context

Closes #156 

## How has this been tested?

Unit tests for the added code.

## 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)

## 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] I have added tests to cover my changes.
- [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 e21c9d7..29d1200 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -380,6 +380,7 @@ type Client struct {
 	Permissions   *PermissionsService
 	Onboarding    *OnboardingService
 	PaymentLinks  *PaymentLinksService
+	Partners      *PartnerService
 }
 ```
 
@@ -446,6 +447,17 @@ 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 Commission
+
+```go
+type Commission struct {
+	Count       int     `json:"count,omitempty"`
+	TotalAmount *Amount `json:"totalAmount,omitempty"`
+}
+```
+
+Commission describes a partner take from any operation on Mollie's API.
+
 #### type Config
 
 ```go
@@ -714,6 +726,17 @@ const (
 
 Valid Fee regions
 
+#### type GetPartnerClientOptions
+
+```go
+type GetPartnerClientOptions struct {
+	Embed string `url:"embed,omitempty"`
+}
+```
+
+GetPartnerClientOptions contains valid query parameters for the get clients
+endpoint.
+
 #### type GiftCardEnabled
 
 ```go
@@ -747,6 +770,7 @@ const (
 	Fashioncheque              GiftCardIssuer = "fashioncheque"
 	Festivalcadeau             GiftCardIssuer = "festivalcadeau"
 	Good4fun                   GiftCardIssuer = "good4fun"
+	HuistuinCadeauKaart        GiftCardIssuer = "huistuincadeaukaart"
 	KlusCadeu                  GiftCardIssuer = "kluscadeau"
 	Kunstencultuurcadeaukaart  GiftCardIssuer = "kunstencultuurcadeaukaart"
 	Nationalebioscoopbon       GiftCardIssuer = "nationalebioscoopbon"
@@ -984,6 +1008,20 @@ type ListMethods struct {
 
 ListMethods describes a list of paginated payment methods.
 
+#### type ListPartnerClientsOptions
+
+```go
+type ListPartnerClientsOptions struct {
+	From  int `url:"from,omitempty"`
+	Limit int `url:"limit,omitempty"`
+	Year  int `url:"year,omitempty"`
+	Month int `url:"month,omitempty"`
+}
+```
+
+ListPartnerClientsOptions contains valid query parameters for the list clients
+endpoint.
+
 #### type ListPaymentOptions
 
 ```go
@@ -1847,6 +1885,75 @@ type PaginationLinks struct {
 
 PaginationLinks describes the hal component of paginated responses.
 
+#### type PartnerClient
+
+```go
+type PartnerClient struct {
+	Resource              string             `json:"resource,omitempty"`
+	ID                    string             `json:"id,omitempty"`
+	OrganizationCreatedAt *time.Time         `json:"organizationCreatedAt,omitempty"`
+	Commission            Commission         `json:"commission,omitempty"`
+	Links                 PartnerClientLinks `json:"_links,omitempty"`
+}
+```
+
+PartnerClient describes a partner client.
+
+#### type PartnerClientLinks
+
+```go
+type PartnerClientLinks struct {
+	Self          *URL `json:"self,omitempty"`
+	Organization  *URL `json:"organization,omitempty"`
+	Onboarding    *URL `json:"onboarding,omitempty"`
+	Documentation *URL `json:"documentation,omitempty"`
+}
+```
+
+PartnerClientLinks contains URL objects relevant to the client.
+
+#### type PartnerClientList
+
+```go
+type PartnerClientList struct {
+	Count          int `json:"count,omitempty"`
+	PartnerClients struct {
+		Clients []*PartnerClient `json:"clients,omitempty"`
+	} `json:"_embedded,omitempty"`
+	Links PaginationLinks `json:"_links,omitempty"`
+}
+```
+
+PartnerClientList describes a list of partner clients.
+
+#### type PartnerService
+
+```go
+type PartnerService service
+```
+
+PartnerService operates over the partners API.
+
+#### func (\*PartnerService) Get
+
+```go
+func (ps *PartnerService) Get(id string, opts *GetPartnerClientOptions) (pc *PartnerClient, err error)
+```
+
+Get retrieves a single client by its ID.
+
+See: https://docs.mollie.com/reference/v2/partners-api/get-client
+
+#### func (\*PartnerService) List
+
+```go
+func (ps *PartnerService) List(opts *ListPartnerClientsOptions) (pc *PartnerClientList, err error)
+```
+
+Get retrieves all clients.
+
+See: https://docs.mollie.com/reference/v2/partners-api/list-clients
+
 #### type Payment
 
 ```go
diff --git a/go.mod b/go.mod
index 80625fe..2a03503 100644
--- a/go.mod
+++ b/go.mod
@@ -4,5 +4,5 @@ go 1.15
 
 require (
 	github.com/google/go-querystring v1.1.0
-	golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1
+	golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5
 )
diff --git a/go.sum b/go.sum
index 9883c62..0d63eec 100644
--- a/go.sum
+++ b/go.sum
@@ -188,6 +188,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 h1:B333XXssMuKQeBwiNODx4TupZy7bf4sxFZnN2ZOcvUE=
 golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5 h1:v79phzBz03tsVCUTbvTBmmC3CUXF5mKYt7DA4ZVldpM=
+golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/mollie/mollie.go b/mollie/mollie.go
index 25fa402..ce09815 100644
--- a/mollie/mollie.go
+++ b/mollie/mollie.go
@@ -58,6 +58,7 @@ type Client struct {
 	Permissions   *PermissionsService
 	Onboarding    *OnboardingService
 	PaymentLinks  *PaymentLinksService
+	Partners      *PartnerService
 }
 
 type service struct {
@@ -191,6 +192,7 @@ func NewClient(baseClient *http.Client, c *Config) (mollie *Client, err error) {
 	mollie.Permissions = (*PermissionsService)(&mollie.common)
 	mollie.Onboarding = (*OnboardingService)(&mollie.common)
 	mollie.PaymentLinks = (*PaymentLinksService)(&mollie.common)
+	mollie.Partners = (*PartnerService)(&mollie.common)
 
 	mollie.userAgent = strings.Join([]string{
 		runtime.GOOS,
diff --git a/mollie/partners.go b/mollie/partners.go
new file mode 100644
index 0000000..0cd04a4
--- /dev/null
+++ b/mollie/partners.go
@@ -0,0 +1,111 @@
+package mollie
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/google/go-querystring/query"
+)
+
+// PartnerClient describes a partner client.
+type PartnerClient struct {
+	Resource              string             `json:"resource,omitempty"`
+	ID                    string             `json:"id,omitempty"`
+	OrganizationCreatedAt *time.Time         `json:"organizationCreatedAt,omitempty"`
+	Commission            Commission         `json:"commission,omitempty"`
+	Links                 PartnerClientLinks `json:"_links,omitempty"`
+}
+
+// PartnerClientList describes a list of partner clients.
+type PartnerClientList struct {
+	Count          int `json:"count,omitempty"`
+	PartnerClients struct {
+		Clients []*PartnerClient `json:"clients,omitempty"`
+	} `json:"_embedded,omitempty"`
+	Links PaginationLinks `json:"_links,omitempty"`
+}
+
+// PartnerClientLinks contains URL objects relevant to the client.
+type PartnerClientLinks struct {
+	Self          *URL `json:"self,omitempty"`
+	Organization  *URL `json:"organization,omitempty"`
+	Onboarding    *URL `json:"onboarding,omitempty"`
+	Documentation *URL `json:"documentation,omitempty"`
+}
+
+// Commission describes a partner take from any
+// operation on Mollie's API.
+type Commission struct {
+	Count       int     `json:"count,omitempty"`
+	TotalAmount *Amount `json:"totalAmount,omitempty"`
+}
+
+// GetPartnerClientOptions contains valid query parameters for the get clients endpoint.
+type GetPartnerClientOptions struct {
+	Embed string `url:"embed,omitempty"`
+}
+
+// ListPartnerClientsOptions contains valid query parameters for the list clients endpoint.
+type ListPartnerClientsOptions struct {
+	From  int `url:"from,omitempty"`
+	Limit int `url:"limit,omitempty"`
+	Year  int `url:"year,omitempty"`
+	Month int `url:"month,omitempty"`
+}
+
+// PartnerService operates over the partners API.
+type PartnerService service
+
+// Get retrieves all clients.
+//
+// See: https://docs.mollie.com/reference/v2/partners-api/list-clients
+func (ps *PartnerService) List(opts *ListPartnerClientsOptions) (pc *PartnerClientList, err error) {
+	u := "v2/clients"
+	if opts != nil {
+		v, _ := query.Values(opts)
+		u = fmt.Sprintf("%s?%s", u, v.Encode())
+	}
+
+	req, err := ps.client.NewAPIRequest(http.MethodGet, u, nil)
+	if err != nil {
+		return
+	}
+
+	res, err := ps.client.Do(req)
+	if err != nil {
+		return
+	}
+
+	if err = json.Unmarshal(res.content, &pc); err != nil {
+		return
+	}
+	return
+}
+
+// Get retrieves a single client by its ID.
+//
+// See: https://docs.mollie.com/reference/v2/partners-api/get-client
+func (ps *PartnerService) Get(id string, opts *GetPartnerClientOptions) (pc *PartnerClient, err error) {
+	u := fmt.Sprintf("v2/clients/%s", id)
+	if opts != nil {
+		v, _ := query.Values(opts)
+		u = fmt.Sprintf("%s?%s", u, v.Encode())
+	}
+
+	req, err := ps.client.NewAPIRequest(http.MethodGet, u, nil)
+	if err != nil {
+		return
+	}
+
+	res, err := ps.client.Do(req)
+	if err != nil {
+		return
+	}
+
+	if err = json.Unmarshal(res.content, &pc); err != nil {
+		return
+	}
+	return
+}


Test patch:
diff --git a/mollie/partners_test.go b/mollie/partners_test.go
new file mode 100644
index 0000000..164a660
--- /dev/null
+++ b/mollie/partners_test.go
@@ -0,0 +1,176 @@
+package mollie
+
+import (
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+
+	"github.com/VictorAvelar/mollie-api-go/v2/testdata"
+)
+
+func TestPartnerService_Get(t *testing.T) {
+	setup()
+	defer teardown()
+	id := "org_1337"
+	_ = tClient.WithAuthenticationValue("test_token")
+	tMux.HandleFunc("/v2/clients/"+id, func(w http.ResponseWriter, r *http.Request) {
+		testHeader(t, r, AuthHeader, "Bearer test_token")
+		testMethod(t, r, "GET")
+		if _, ok := r.Header[AuthHeader]; !ok {
+			w.WriteHeader(http.StatusUnauthorized)
+		}
+
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte(testdata.GetPartnerClientResponse))
+	})
+
+	res, err := tClient.Partners.Get(id, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if res.ID != id {
+		t.Errorf("mismatching info. want %v, got %v", id, res.ID)
+	}
+}
+
+func TestPartnerService_GetWithOptions(t *testing.T) {
+	setup()
+	defer teardown()
+	id := "org_1337"
+	_ = tClient.WithAuthenticationValue("test_token")
+	tMux.HandleFunc("/v2/clients/"+id, func(w http.ResponseWriter, r *http.Request) {
+		testHeader(t, r, AuthHeader, "Bearer test_token")
+		testMethod(t, r, "GET")
+		if !strings.Contains(r.URL.String(), "?embed=organization") {
+			w.WriteHeader(http.StatusBadRequest)
+		}
+		if _, ok := r.Header[AuthHeader]; !ok {
+			w.WriteHeader(http.StatusUnauthorized)
+		}
+
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte(testdata.GetPartnerClientResponse))
+	})
+
+	res, err := tClient.Partners.Get(id, &GetPartnerClientOptions{Embed: "organization"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if res.ID != id {
+		t.Errorf("mismatching info. want %v, got %v", id, res.ID)
+	}
+}
+
+func TestPartnerService_List(t *testing.T) {
+	setup()
+	defer teardown()
+	_ = tClient.WithAuthenticationValue("test_token")
+	tMux.HandleFunc("/v2/clients", func(w http.ResponseWriter, r *http.Request) {
+		testHeader(t, r, AuthHeader, "Bearer test_token")
+		testMethod(t, r, "GET")
+		if _, ok := r.Header[AuthHeader]; !ok {
+			w.WriteHeader(http.StatusUnauthorized)
+		}
+
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte(testdata.ListPartnerClientsResponse))
+	})
+
+	res, err := tClient.Partners.List(nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(res.PartnerClients.Clients) != res.Count {
+		t.Error("mismatching info.")
+	}
+}
+
+func TestPartnerService_ListWithOptions(t *testing.T) {
+	setup()
+	defer teardown()
+	_ = tClient.WithAuthenticationValue("test_token")
+	tMux.HandleFunc("/v2/clients", func(w http.ResponseWriter, r *http.Request) {
+		testHeader(t, r, AuthHeader, "Bearer test_token")
+		testMethod(t, r, "GET")
+		if _, ok := r.Header[AuthHeader]; !ok {
+			w.WriteHeader(http.StatusUnauthorized)
+		}
+		if !strings.Contains(r.URL.String(), "?year=2020") {
+			w.WriteHeader(http.StatusBadRequest)
+		}
+
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte(testdata.ListPartnerClientsResponse))
+	})
+
+	res, err := tClient.Partners.List(&ListPartnerClientsOptions{
+		Year: 2020,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(res.PartnerClients.Clients) != res.Count {
+		t.Error("mismatching info.")
+	}
+}
+
+func TestPartnerService_HttpRequestErrors(t *testing.T) {
+	setup()
+	defer teardown()
+
+	tMux.HandleFunc("/v2/clients/", errorHandler)
+
+	_, gerr := tClient.Partners.Get("org_1337", nil)
+	_, rerr := tClient.Partners.List(nil)
+
+	tests := []error{rerr, gerr}
+
+	for _, tt := range tests {
+		if tt == nil {
+			t.Fail()
+		}
+	}
+}
+
+func TestPartnerService_NewAPIRequestErrors(t *testing.T) {
+	setup()
+	defer teardown()
+	u, _ := url.Parse(tServer.URL)
+	tClient.BaseURL = u
+	tMux.HandleFunc("/v2/clients/", errorHandler)
+
+	_, gerr := tClient.Partners.Get("org_1337", nil)
+	_, rerr := tClient.Partners.List(nil)
+
+	tests := []error{rerr, gerr}
+
+	for _, tt := range tests {
+		if tt != errBadBaseURL {
+			t.Fail()
+		}
+	}
+}
+
+func TestPartnerService_EncodingResponseErrors(t *testing.T) {
+	setup()
+	defer teardown()
+	tMux.HandleFunc("/v2/clients/", encodingHandler)
+
+	_, gerr := tClient.Partners.Get("org_1337", nil)
+	_, rerr := tClient.Partners.List(nil)
+
+	tests := []error{rerr, gerr}
+
+	for _, tt := range tests {
+		if tt == nil {
+			t.Fail()
+		} else if !strings.Contains(tt.Error(), "invalid character") {
+			t.Errorf("unexpected error %v", tt)
+		}
+	}
+}
diff --git a/testdata/partners.go b/testdata/partners.go
new file mode 100644
index 0000000..21bacc8
--- /dev/null
+++ b/testdata/partners.go
@@ -0,0 +1,85 @@
+package testdata
+
+const ListPartnerClientsResponse = `{
+   "count":1,
+   "_embedded":{
+      "clients":[
+         {
+            "resource":"client",
+            "id":"org_1337",
+            "organizationCreatedAt":"2018-03-21T13:13:37+00:00",
+            "commission":{
+               "count":200,
+               "totalAmount":{
+                  "currency":"EUR",
+                  "value":"10.00"
+               }
+            },
+            "_links":{
+               "self":{
+                  "href":"https://api.mollie.com/v2/clients/org_1337",
+                  "type":"application/hal+json"
+               },
+               "organization":{
+                  "href":"https://api.mollie.com/v2/organizations/org_1337",
+                  "type":"application/hal+json"
+               },
+               "onboarding":{
+                  "href":"https://api.mollie.com/v2/onboarding/org_1337",
+                  "type":"application/hal+json"
+               },
+               "documentation":{
+                  "href":"https://docs.mollie.com/reference/v2/partners-api/get-client",
+                  "type":"text/html"
+               }
+            }
+         }
+      ]
+   },
+   "_links":{
+      "self":{
+         "href":"https://api.mollie.com/v2/clients?limit=3",
+         "type":"application/hal+json"
+      },
+      "previous":null,
+      "next":{
+         "href":"https://api.mollie.com/v2/clients?from=org_1379&limit=3",
+         "type":"application/hal+json"
+      },
+      "documentation":{
+         "href":"https://docs.mollie.com/reference/v2/partners-api/list-clients",
+         "type":"text/html"
+      }
+   }
+}`
+
+const GetPartnerClientResponse = `{
+    "resource": "client",
+    "id": "org_1337",
+    "organizationCreatedAt": "2018-03-21T13:13:37+00:00",
+    "commission": {
+        "count": 200,
+        "totalAmount": {
+            "currency": "EUR",
+            "value": "10.00"
+        }
+    },
+    "_links": {
+        "self": {
+            "href": "https://api.mollie.com/v2/clients/org_1337",
+            "type": "application/hal+json"
+        },
+        "organization": {
+            "href": "https://api.mollie.com/v2/organizations/org_1337",
+            "type": "application/hal+json"
+        },
+        "onboarding": {
+            "href": "https://api.mollie.com/v2/onboarding/org_1337",
+            "type": "application/hal+json"
+        },
+        "documentation": {
+            "href": "https://docs.mollie.com/reference/v2/partners-api/get-client",
+            "type": "text/html"
+        }
+    }
+}`