From 9bad7ab645e1eba9c845f5eebe19d97d353eadcb Mon Sep 17 00:00:00 2001 From: Victor Hugo Avelar Ossorio Date: Thu, 10 Aug 2023 15:34:52 +0000 Subject: [PATCH 1/4] feat(terminals): add terminals api --- mollie/mollie.go | 2 + mollie/terminals.go | 88 +++++++++++++++++ mollie/terminals_test.go | 209 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+) create mode 100644 mollie/terminals.go create mode 100644 mollie/terminals_test.go diff --git a/mollie/mollie.go b/mollie/mollie.go index 974705d8..2f9d5b00 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 00000000..4dd5a1c4 --- /dev/null +++ b/mollie/terminals.go @@ -0,0 +1,88 @@ +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, +) { + res, err = ts.client.get(ctx, "v2/terminals", options) + if err != nil { + return + } + + if err = json.Unmarshal(res.content, &tl); err != nil { + return + } + + return +} diff --git a/mollie/terminals_test.go b/mollie/terminals_test.go new file mode 100644 index 00000000..0108c61e --- /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)) +} From 806e5c8ab154f458b737bb9eaf3489a183c9af1e Mon Sep 17 00:00:00 2001 From: Victor Hugo Avelar Ossorio Date: Thu, 10 Aug 2023 15:35:14 +0000 Subject: [PATCH 2/4] chore(terminals): add terminals test data --- testdata/terminals.go | 139 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 testdata/terminals.go diff --git a/testdata/terminals.go b/testdata/terminals.go new file mode 100644 index 00000000..3e3d8491 --- /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" + } + } +}` From ba8abb3601006034bf87b67ee0ab7f65ff9dfd63 Mon Sep 17 00:00:00 2001 From: Victor Hugo Avelar Ossorio Date: Thu, 10 Aug 2023 15:37:16 +0000 Subject: [PATCH 3/4] chore(docs): update generated docs --- docs/README.md | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/docs/README.md b/docs/README.md index 53438690..1eaceecd 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 From da233ae2dc6f623b2b4738126b26374f9ea48ec2 Mon Sep 17 00:00:00 2001 From: Victor Hugo Avelar Ossorio Date: Thu, 10 Aug 2023 15:41:35 +0000 Subject: [PATCH 4/4] feat(terminals): ensure testmode is enabled based on config --- mollie/terminals.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mollie/terminals.go b/mollie/terminals.go index 4dd5a1c4..27e89d17 100644 --- a/mollie/terminals.go +++ b/mollie/terminals.go @@ -75,6 +75,10 @@ func (ts *TerminalsService) List(ctx context.Context, options *TerminalListOptio 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