Repository: VictorAvelar/mollie-api-go
Pull request #278 (2023-08-10 15:38:13)

Terminals API
<!--- Provide a general summary of your changes in the Title above -->

# Description

Enable terminals api (BETA)

## Motivation and context

Maintain 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
- [ ] 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:

- 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 5343869..1eaceec 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -945,6 +945,7 @@ type Client struct {
  Partners       *PartnerService
  Balances       *BalancesService
  ClientLinks    *ClientLinksService
+ Terminals      *TerminalsService
 }
 ```
 
@@ -4688,6 +4689,104 @@ type Subtotal struct {
 
 Subtotal balance descriptor.
 
+#### type Terminal
+
+```go
+type Terminal struct {
+ ID           string          `json:"id,omitempty"`
+ Resource     string          `json:"resource,omitempty"`
+ ProfileID    string          `json:"profileID,omitempty"`
+ Status       TerminalStatus  `json:"status,omitempty"`
+ Brand        string          `json:"brand,omitempty"`
+ Model        string          `json:"model,omitempty"`
+ SerialNumber string          `json:"serialNumber,omitempty"`
+ Currency     string          `json:"currency,omitempty"`
+ Description  string          `json:"description,omitempty"`
+ CreatedAt    *time.Time      `json:"createdAt,omitempty"`
+ UpdatedAt    *time.Time      `json:"updatedAt,omitempty"`
+ Links        PaginationLinks `json:"_links,omitempty"`
+}
+```
+
+Terminal symbolizes a physical device to receive payments.
+
+#### type TerminalList
+
+```go
+type TerminalList struct {
+ Count    int `json:"count,omitempty"`
+ Embedded struct {
+  Terminals []*Terminal `json:"terminals,omitempty"`
+ } `json:"_embedded,omitempty"`
+ Links PaginationLinks `json:"_links,omitempty"`
+}
+```
+
+TerminalList describes the response for terminals list endpoints.
+
+#### type TerminalListOptions
+
+```go
+type TerminalListOptions struct {
+ From      string `url:"from,omitempty"`
+ Limit     int    `url:"limit,omitempty"`
+ ProfileID string `url:"profileID,omitempty"`
+ TestMode  bool   `url:"testMode,omitempty"`
+}
+```
+
+TerminalListOptions holds query string parameters valid for terminals lists.
+
+ProfileID and TestMode are valid only when using access tokens.
+
+#### type TerminalStatus
+
+```go
+type TerminalStatus string
+```
+
+TerminalStatus is the status of the terminal, which is a read-only value
+determined by Mollie.
+
+```go
+const (
+ TerminalPending  TerminalStatus = "pending"
+ TerminalActive   TerminalStatus = "active"
+ TerminalInactive TerminalStatus = "inactive"
+)
+```
+
+Possible terminal statuses.
+
+#### type TerminalsService
+
+```go
+type TerminalsService service
+```
+
+TerminalsService operates over terminals resource.
+
+#### func (*TerminalsService) Get
+
+```go
+func (ts *TerminalsService) Get(ctx context.Context, id string) (res *Response, t *Terminal, err error)
+```
+
+Get terminal retrieves a single terminal object by its terminal ID.
+
+#### func (*TerminalsService) List
+
+```go
+func (ts *TerminalsService) List(ctx context.Context, options *TerminalListOptions) (
+ res *Response,
+ tl *TerminalList,
+ err error,
+)
+```
+
+List retrieves a list of terminals symbolizing the physical devices to receive
+payments.
+
 #### type TransactionType
 
 ```go
diff --git a/mollie/mollie.go b/mollie/mollie.go
index 974705d..2f9d5b0 100644
--- a/mollie/mollie.go
+++ b/mollie/mollie.go
@@ -67,6 +67,7 @@ type Client struct {
 	Partners       *PartnerService
 	Balances       *BalancesService
 	ClientLinks    *ClientLinksService
+	Terminals      *TerminalsService
 }
 
 type service struct {
@@ -302,6 +303,7 @@ func NewClient(baseClient *http.Client, conf *Config) (mollie *Client, err error
 	mollie.Partners = (*PartnerService)(&mollie.common)
 	mollie.Balances = (*BalancesService)(&mollie.common)
 	mollie.ClientLinks = (*ClientLinksService)(&mollie.common)
+	mollie.Terminals = (*TerminalsService)(&mollie.common)
 
 	mollie.userAgent = strings.Join([]string{
 		runtime.GOOS,
diff --git a/mollie/terminals.go b/mollie/terminals.go
new file mode 100644
index 0000000..27e89d1
--- /dev/null
+++ b/mollie/terminals.go
@@ -0,0 +1,92 @@
+package mollie
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"time"
+)
+
+// TerminalsService operates over terminals resource.
+type TerminalsService service
+
+// TerminalStatus is the status of the terminal, which is a read-only value determined by Mollie.
+type TerminalStatus string
+
+// Possible terminal statuses.
+const (
+	TerminalPending  TerminalStatus = "pending"
+	TerminalActive   TerminalStatus = "active"
+	TerminalInactive TerminalStatus = "inactive"
+)
+
+// Terminal symbolizes a physical device to receive payments.
+type Terminal struct {
+	ID           string          `json:"id,omitempty"`
+	Resource     string          `json:"resource,omitempty"`
+	ProfileID    string          `json:"profileID,omitempty"`
+	Status       TerminalStatus  `json:"status,omitempty"`
+	Brand        string          `json:"brand,omitempty"`
+	Model        string          `json:"model,omitempty"`
+	SerialNumber string          `json:"serialNumber,omitempty"`
+	Currency     string          `json:"currency,omitempty"`
+	Description  string          `json:"description,omitempty"`
+	CreatedAt    *time.Time      `json:"createdAt,omitempty"`
+	UpdatedAt    *time.Time      `json:"updatedAt,omitempty"`
+	Links        PaginationLinks `json:"_links,omitempty"`
+}
+
+// Get terminal retrieves a single terminal object by its terminal ID.
+func (ts *TerminalsService) Get(ctx context.Context, id string) (res *Response, t *Terminal, err error) {
+	res, err = ts.client.get(ctx, fmt.Sprintf("v2/terminals/%s", id), nil)
+	if err != nil {
+		return
+	}
+
+	if err = json.Unmarshal(res.content, &t); err != nil {
+		return
+	}
+
+	return
+}
+
+// TerminalListOptions holds query string parameters valid for terminals lists.
+//
+// ProfileID and TestMode are valid only when using access tokens.
+type TerminalListOptions struct {
+	From      string `url:"from,omitempty"`
+	Limit     int    `url:"limit,omitempty"`
+	ProfileID string `url:"profileID,omitempty"`
+	TestMode  bool   `url:"testMode,omitempty"`
+}
+
+// TerminalList describes the response for terminals list endpoints.
+type TerminalList struct {
+	Count    int `json:"count,omitempty"`
+	Embedded struct {
+		Terminals []*Terminal `json:"terminals,omitempty"`
+	} `json:"_embedded,omitempty"`
+	Links PaginationLinks `json:"_links,omitempty"`
+}
+
+// List retrieves a list of terminals symbolizing the physical devices to receive payments.
+func (ts *TerminalsService) List(ctx context.Context, options *TerminalListOptions) (
+	res *Response,
+	tl *TerminalList,
+	err error,
+) {
+	if ts.client.HasAccessToken() && ts.client.config.testing {
+		options.TestMode = true
+	}
+
+	res, err = ts.client.get(ctx, "v2/terminals", options)
+	if err != nil {
+		return
+	}
+
+	if err = json.Unmarshal(res.content, &tl); err != nil {
+		return
+	}
+
+	return
+}


Test patch:
diff --git a/mollie/terminals_test.go b/mollie/terminals_test.go
new file mode 100644
index 0000000..0108c61
--- /dev/null
+++ b/mollie/terminals_test.go
@@ -0,0 +1,209 @@
+package mollie
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/VictorAvelar/mollie-api-go/v3/testdata"
+	"github.com/stretchr/testify/suite"
+)
+
+type terminalsServiceSuite struct{ suite.Suite }
+
+func (ts *terminalsServiceSuite) SetupSuite() { setEnv() }
+
+func (ts *terminalsServiceSuite) TearDownSuite() { unsetEnv() }
+
+func (ts *terminalsServiceSuite) TestTerminalsService_Get() {
+	type args struct {
+		ctx context.Context
+		id  string
+	}
+
+	cases := []struct {
+		name    string
+		args    args
+		wantErr bool
+		err     error
+		want    string
+		pre     func()
+		handler http.HandlerFunc
+	}{
+		{
+			"get terminal correctly",
+			args{
+				context.Background(),
+				"term_7MgL4wea46qkRcoTZjWEH",
+			},
+			false,
+			nil,
+			testdata.GetTerminalResponse,
+			noPre,
+			func(w http.ResponseWriter, r *http.Request) {
+				testHeader(ts.T(), r, AuthHeader, "Bearer token_X12b31ggg23")
+				testMethod(ts.T(), r, "GET")
+
+				if _, ok := r.Header[AuthHeader]; !ok {
+					w.WriteHeader(http.StatusUnauthorized)
+				}
+				_, _ = w.Write([]byte(testdata.GetTerminalResponse))
+			},
+		},
+		{
+			"get terminal, an error is returned from the server",
+			args{
+				context.Background(),
+				"term_7MgL4wea46qkRcoTZjWEH",
+			},
+			true,
+			fmt.Errorf("500 Internal Server Error: An internal server error occurred while processing your request."),
+			"",
+			noPre,
+			errorHandler,
+		},
+		{
+			"get terminal, an error occurs when parsing json",
+			args{
+				context.Background(),
+				"term_7MgL4wea46qkRcoTZjWEH",
+			},
+			true,
+			fmt.Errorf("invalid character 'h' looking for beginning of object key string"),
+			"",
+			noPre,
+			encodingHandler,
+		},
+		{
+			"get terminal, invalid url when building request",
+			args{
+				context.Background(),
+				"term_7MgL4wea46qkRcoTZjWEH",
+			},
+			true,
+			errBadBaseURL,
+			"",
+			crashSrv,
+			errorHandler,
+		},
+	}
+
+	for _, c := range cases {
+		setup()
+		defer teardown()
+
+		ts.T().Run(c.name, func(t *testing.T) {
+			c.pre()
+			tMux.HandleFunc(fmt.Sprintf("/v2/terminals/%s", c.args.id), c.handler)
+
+			res, m, err := tClient.Terminals.Get(c.args.ctx, c.args.id)
+			if c.wantErr {
+				ts.NotNil(err)
+				ts.EqualError(err, c.err.Error())
+			} else {
+				ts.Nil(err)
+				ts.IsType(&Terminal{}, m)
+				ts.IsType(&http.Response{}, res.Response)
+			}
+		})
+	}
+}
+
+func (ts *terminalsServiceSuite) TestTerminalsService_List() {
+	type args struct {
+		ctx     context.Context
+		options *TerminalListOptions
+	}
+
+	cases := []struct {
+		name    string
+		args    args
+		wantErr bool
+		err     error
+		want    string
+		pre     func()
+		handler http.HandlerFunc
+	}{
+		{
+			"list terminals correctly",
+			args{
+				context.Background(),
+				&TerminalListOptions{},
+			},
+			false,
+			nil,
+			testdata.GetTerminalResponse,
+			noPre,
+			func(w http.ResponseWriter, r *http.Request) {
+				testHeader(ts.T(), r, AuthHeader, "Bearer token_X12b31ggg23")
+				testMethod(ts.T(), r, "GET")
+
+				if _, ok := r.Header[AuthHeader]; !ok {
+					w.WriteHeader(http.StatusUnauthorized)
+				}
+				_, _ = w.Write([]byte(testdata.ListTerminalsResponse))
+			},
+		},
+		{
+			"get terminals list, an error is returned from the server",
+			args{
+				context.Background(),
+				nil,
+			},
+			true,
+			fmt.Errorf("500 Internal Server Error: An internal server error occurred while processing your request."),
+			"",
+			noPre,
+			errorHandler,
+		},
+		{
+			"get terminals list, an error occurs when parsing json",
+			args{
+				context.Background(),
+				nil,
+			},
+			true,
+			fmt.Errorf("invalid character 'h' looking for beginning of object key string"),
+			"",
+			noPre,
+			encodingHandler,
+		},
+		{
+			"get terminals list, invalid url when building request",
+			args{
+				context.Background(),
+				nil,
+			},
+			true,
+			errBadBaseURL,
+			"",
+			crashSrv,
+			errorHandler,
+		},
+	}
+
+	for _, c := range cases {
+		setup()
+		defer teardown()
+
+		ts.T().Run(c.name, func(t *testing.T) {
+			c.pre()
+			tMux.HandleFunc("/v2/terminals", c.handler)
+
+			res, m, err := tClient.Terminals.List(c.args.ctx, c.args.options)
+			if c.wantErr {
+				ts.NotNil(err)
+				ts.EqualError(err, c.err.Error())
+			} else {
+				ts.Nil(err)
+				ts.IsType(&TerminalList{}, m)
+				ts.IsType(&http.Response{}, res.Response)
+			}
+		})
+	}
+}
+
+func TestTerminalService(t *testing.T) {
+	suite.Run(t, new(terminalsServiceSuite))
+}
diff --git a/testdata/terminals.go b/testdata/terminals.go
new file mode 100644
index 0000000..3e3d849
--- /dev/null
+++ b/testdata/terminals.go
@@ -0,0 +1,139 @@
+package testdata
+
+// ListTerminalsResponse example.
+const ListTerminalsResponse = `{
+    "count": 5,
+    "_embedded": {
+        "terminals": [
+            {
+                "id": "term_7MgL4wea46qkRcoTZjWEH",
+                "profileId": "pfl_QkEhN94Ba",
+                "status": "active",
+                "brand": "PAX",
+                "model": "A920",
+                "serialNumber": "1234567890",
+                "currency": "EUR",
+                "description": "Terminal #12341",
+                "createdAt": "2022-02-12T11:58:35.0Z",
+                "updatedAt": "2022-11-15T13:32:11.0Z",
+                "_links": {
+                    "self": {
+                        "href": "https://api.mollie.com/v2/terminals/term_7MgL4wea46qkRcoTZjWEH",
+                        "type": "application/hal+json"
+                    }
+                }
+            },
+            {
+                "id": "term_7sgL4wea46qkRcoysdiWEH",
+                "profileId": "pfl_QkEhN94Ba",
+                "status": "active",
+                "brand": "PAX",
+                "model": "A920",
+                "serialNumber": "1234567890",
+                "currency": "MEX",
+                "description": "Terminal #12342",
+                "createdAt": "2022-02-12T11:58:35.0Z",
+                "updatedAt": "2022-11-15T13:32:11.0Z",
+                "_links": {
+                    "self": {
+                        "href": "https://api.mollie.com/v2/terminals/term_7sgL4wea46qkRcoysdiWEH",
+                        "type": "application/hal+json"
+                    }
+                }
+            },
+            {
+                "id": "term_7MgLsdD*b3asDayWEH",
+                "profileId": "pfl_QkEhN94Ba",
+                "status": "active",
+                "brand": "PAX",
+                "model": "A920",
+                "serialNumber": "1234567890",
+                "currency": "GBP",
+                "description": "Terminal #12343",
+                "createdAt": "2022-02-12T11:58:35.0Z",
+                "updatedAt": "2022-11-15T13:32:11.0Z",
+                "_links": {
+                    "self": {
+                        "href": "https://api.mollie.com/v2/terminals/term_7MgLsdD*b3asDayWEH",
+                        "type": "application/hal+json"
+                    }
+                }
+            },
+            {
+                "id": "term_7MgL4j5jAowWqkRcoTZjWEH",
+                "profileId": "pfl_QkEhN94Ba",
+                "status": "active",
+                "brand": "PAX",
+                "model": "A920",
+                "serialNumber": "1234567890",
+                "currency": "DLS",
+                "description": "Terminal #12344",
+                "createdAt": "2022-02-12T11:58:35.0Z",
+                "updatedAt": "2022-11-15T13:32:11.0Z",
+                "_links": {
+                    "self": {
+                        "href": "https://api.mollie.com/v2/terminals/term_7MgL4j5jAowWqkRcoTZjWEH",
+                        "type": "application/hal+json"
+                    }
+                }
+            },
+            {
+                "id": "term_7MgL4we02ujSeRcoTZjWEH",
+                "profileId": "pfl_QkEhN94Ba",
+                "status": "active",
+                "brand": "PAX",
+                "model": "A920",
+                "serialNumber": "1234567890",
+                "currency": "COP",
+                "description": "Terminal #12345",
+                "createdAt": "2022-02-12T11:58:35.0Z",
+                "updatedAt": "2022-11-15T13:32:11.0Z",
+                "_links": {
+                    "self": {
+                        "href": "https://api.mollie.com/v2/terminals/term_7MgL4we02ujSeRcoTZjWEH",
+                        "type": "application/hal+json"
+                    }
+                }
+            }
+        ]
+    },
+    "_links": {
+        "self": {
+            "href": "https://api.mollie.com/v2/terminals?limit=5",
+            "type": "application/hal+json"
+        },
+        "previous": null,
+        "next": {
+            "href": "https://api.mollie.com/v2/terminals?from=term_7MgL4we02ujSeRcoTZjWEH&limit=5",
+            "type": "application/hal+json"
+        },
+        "documentation": {
+            "href": "https://docs.mollie.com/reference/v2/terminals-api/list-terminals",
+            "type": "text/html"
+        }
+    }
+}`
+
+// GetTerminalResponse example.
+const GetTerminalResponse = `{
+    "id": "term_7MgL4wea46qkRcoTZjWEH",
+    "profileId": "pfl_QkEhN94Ba",
+    "status": "active",
+    "brand": "PAX",
+    "model": "A920",
+    "serialNumber": "1234567890",
+    "currency": "EUR",
+    "description": "Terminal #12345",
+    "createdAt": "2022-02-12T11:58:35.0Z",
+    "updatedAt": "2022-11-15T13:32:11.0Z",
+    "_links": {
+        "self": {
+            "href": "https://api.mollie.com/v2/terminals/term_7MgL4wea46qkRcoTZjWEH",
+            "type": "application/hal+json"
+        },
+        "documentation": {
+            "href": "https://docs.mollie.com/reference/v2/terminals-api/get-terminal",
+            "type": "text/html"
+        }
+    }
+}`