Test Setup Failed
Push — main ( 8bfd66...090df7 )
by Acho
01:29
created

campay.*Client.ValidateCallback   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
dl 0
loc 5
c 0
b 0
f 0
rs 10
nop 2
1
package campay
2
3
import (
4
	"bytes"
5
	"context"
6
	"encoding/json"
7
	"fmt"
8
	"io"
9
	"io/ioutil"
10
	"net/http"
11
	"sync"
12
	"time"
13
14
	"github.com/golang-jwt/jwt"
15
)
16
17
type service struct {
18
	client *Client
19
}
20
21
// Client is the campay API client.
22
// Do not instantiate this client with Client{}. Use the New method instead.
23
type Client struct {
24
	httpClient          *http.Client
25
	common              service
26
	environment         Environment
27
	mutex               sync.Mutex
28
	apiUsername         string
29
	apiPassword         string
30
	token               string
31
	tokenExpirationTime int64
32
	Transaction         *TransactionService
33
}
34
35
// New creates and returns a new campay.Client from a slice of campay.ClientOption.
36
func New(options ...ClientOption) *Client {
37
	config := defaultClientConfig()
38
39
	for _, option := range options {
40
		option.apply(config)
41
	}
42
43
	client := &Client{
44
		httpClient:  config.httpClient,
45
		environment: config.environment,
46
		apiUsername: config.apiUsername,
47
		apiPassword: config.apiPassword,
48
		mutex:       sync.Mutex{},
49
	}
50
51
	client.common.client = client
52
	client.Transaction = (*TransactionService)(&client.common)
53
	return client
54
}
55
56
// Token Gets the access token
57
// POST /token/
58
// API Doc: https://documenter.getpostman.com/view/2391374/T1LV8PVA#8803168b-d451-4d65-b8cc-85e385bc3050
59
func (client *Client) Token(ctx context.Context) (*Token, *Response, error) {
60
	payload := map[string]string{
61
		"username": client.apiUsername,
62
		"password": client.apiPassword,
63
	}
64
65
	request, err := client.newRequest(ctx, http.MethodPost, "/token/", payload)
66
	if err != nil {
67
		return nil, nil, err
68
	}
69
70
	resp, err := client.do(request)
71
	if err != nil {
72
		return nil, resp, err
73
	}
74
75
	var token Token
76
	if err = json.Unmarshal(*resp.Body, &token); err != nil {
77
		return nil, resp, err
78
	}
79
80
	return &token, resp, nil
81
}
82
83
// ValidateCallback checks if the signature was encrypted with the webhook key
84
func (client *Client) ValidateCallback(signature string, webhookKey []byte) error {
85
	_, err := jwt.Parse(signature, func(token *jwt.Token) (interface{}, error) {
86
		return webhookKey, nil
87
	})
88
	return err
89
}
90
91
// Collect Requests a Payment
92
// POST /collect/
93
// API Doc: https://documenter.getpostman.com/view/2391374/T1LV8PVA#31757962-2e07-486b-a6f4-a7cc7a06d032
94
func (client *Client) Collect(ctx context.Context, options *CollectOptions) (*CollectResponse, *Response, error) {
95
	err := client.refreshToken(ctx)
96
	if err != nil {
97
		return nil, nil, err
98
	}
99
100
	request, err := client.newRequest(ctx, http.MethodPost, "/collect/", options)
101
	if err != nil {
102
		return nil, nil, err
103
	}
104
105
	response, err := client.do(request)
106
	if err != nil {
107
		return nil, response, err
108
	}
109
110
	var collectResponse CollectResponse
111
	if err = json.Unmarshal(*response.Body, &collectResponse); err != nil {
112
		return nil, response, err
113
	}
114
115
	return &collectResponse, response, nil
116
}
117
118
// newRequest creates an API request. A relative URL can be provided in uri,
119
// in which case it is resolved relative to the BaseURL of the Client.
120
// URI's should always be specified without a preceding slash.
121
func (client *Client) newRequest(ctx context.Context, method, uri string, body interface{}) (*http.Request, error) {
122
	var buf io.ReadWriter
123
	if body != nil {
124
		buf = &bytes.Buffer{}
125
		enc := json.NewEncoder(buf)
126
		enc.SetEscapeHTML(false)
127
		err := enc.Encode(body)
128
		if err != nil {
129
			return nil, err
130
		}
131
	}
132
133
	req, err := http.NewRequestWithContext(ctx, method, client.environment.String()+uri, buf)
134
	if err != nil {
135
		return nil, err
136
	}
137
138
	req.Header.Set("Content-Type", "application/json")
139
140
	if len(client.token) > 0 {
141
		req.Header.Set("Authorization", "Token "+client.token)
142
	}
143
144
	return req, nil
145
}
146
147
// do carries out an HTTP request and returns a Response
148
func (client *Client) do(req *http.Request) (*Response, error) {
149
	if req == nil {
150
		return nil, fmt.Errorf("%T cannot be nil", req)
151
	}
152
153
	httpResponse, err := client.httpClient.Do(req)
154
	if err != nil {
155
		return nil, err
156
	}
157
158
	defer func() { _ = httpResponse.Body.Close() }()
159
160
	resp, err := client.newResponse(httpResponse)
161
	if err != nil {
162
		return resp, err
163
	}
164
165
	_, err = io.Copy(ioutil.Discard, httpResponse.Body)
166
	if err != nil {
167
		return resp, err
168
	}
169
170
	return resp, nil
171
}
172
173
// refreshToken refreshes the authentication Token
174
func (client *Client) refreshToken(ctx context.Context) error {
175
	if client.tokenExpirationTime > time.Now().UTC().Unix() {
176
		return nil
177
	}
178
179
	client.mutex.Lock()
180
	defer client.mutex.Unlock()
181
182
	token, _, err := client.Token(ctx)
183
	if err != nil {
184
		return err
185
	}
186
187
	client.token = token.Token
188
	client.tokenExpirationTime = time.Now().UTC().Unix() + token.ExpiresIn - 100 // Give extra 100 second buffer
189
190
	return nil
191
}
192
193
// newResponse converts an *http.Response to *Response
194
func (client *Client) newResponse(httpResponse *http.Response) (*Response, error) {
195
	if httpResponse == nil {
196
		return nil, fmt.Errorf("%T cannot be nil", httpResponse)
197
	}
198
199
	resp := new(Response)
200
	resp.HTTPResponse = httpResponse
201
202
	buf, err := ioutil.ReadAll(resp.HTTPResponse.Body)
203
	if err != nil {
204
		return nil, err
205
	}
206
	resp.Body = &buf
207
208
	return resp, resp.Error()
209
}
210