Passed
Push — main ( 853831...e0857a )
by Acho
02:36
created

campay.*Client.Withdraw   A

Complexity

Conditions 5

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
nop 2
dl 0
loc 22
rs 9.2333
c 0
b 0
f 0
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, params *CollectParams) (*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/", params)
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
// Withdraw funds to a mobile money account
119
// POST /withdraw/
120
// API Doc: https://documenter.getpostman.com/view/2391374/T1LV8PVA#885dbde0-b0dd-4514-a0f9-f84fc83df12d
121
func (client *Client) Withdraw(ctx context.Context, params *WithdrawParams) (*WithdrawResponse, *Response, error) {
122
	err := client.refreshToken(ctx)
123
	if err != nil {
124
		return nil, nil, err
125
	}
126
127
	request, err := client.newRequest(ctx, http.MethodPost, "/withdraw/", params)
128
	if err != nil {
129
		return nil, nil, err
130
	}
131
132
	response, err := client.do(request)
133
	if err != nil {
134
		return nil, response, err
135
	}
136
137
	var withdrawResponse WithdrawResponse
138
	if err = json.Unmarshal(*response.Body, &withdrawResponse); err != nil {
139
		return nil, response, err
140
	}
141
142
	return &withdrawResponse, response, nil
143
}
144
145
// newRequest creates an API request. A relative URL can be provided in uri,
146
// in which case it is resolved relative to the BaseURL of the Client.
147
// URI's should always be specified without a preceding slash.
148
func (client *Client) newRequest(ctx context.Context, method, uri string, body interface{}) (*http.Request, error) {
149
	var buf io.ReadWriter
150
	if body != nil {
151
		buf = &bytes.Buffer{}
152
		enc := json.NewEncoder(buf)
153
		enc.SetEscapeHTML(false)
154
		err := enc.Encode(body)
155
		if err != nil {
156
			return nil, err
157
		}
158
	}
159
160
	req, err := http.NewRequestWithContext(ctx, method, client.environment.String()+uri, buf)
161
	if err != nil {
162
		return nil, err
163
	}
164
165
	req.Header.Set("Content-Type", "application/json")
166
167
	if len(client.token) > 0 {
168
		req.Header.Set("Authorization", "Token "+client.token)
169
	}
170
171
	return req, nil
172
}
173
174
// do carries out an HTTP request and returns a Response
175
func (client *Client) do(req *http.Request) (*Response, error) {
176
	if req == nil {
177
		return nil, fmt.Errorf("%T cannot be nil", req)
178
	}
179
180
	httpResponse, err := client.httpClient.Do(req)
181
	if err != nil {
182
		return nil, err
183
	}
184
185
	defer func() { _ = httpResponse.Body.Close() }()
186
187
	resp, err := client.newResponse(httpResponse)
188
	if err != nil {
189
		return resp, err
190
	}
191
192
	_, err = io.Copy(ioutil.Discard, httpResponse.Body)
193
	if err != nil {
194
		return resp, err
195
	}
196
197
	return resp, nil
198
}
199
200
// refreshToken refreshes the authentication Token
201
func (client *Client) refreshToken(ctx context.Context) error {
202
	if client.tokenExpirationTime > time.Now().UTC().Unix() {
203
		return nil
204
	}
205
206
	client.mutex.Lock()
207
	defer client.mutex.Unlock()
208
209
	client.token = ""
210
211
	token, _, err := client.Token(ctx)
212
	if err != nil {
213
		return err
214
	}
215
216
	client.token = token.Token
217
	client.tokenExpirationTime = time.Now().UTC().Unix() + token.ExpiresIn - 1000 // Give extra 100 second buffer
218
219
	return nil
220
}
221
222
// newResponse converts an *http.Response to *Response
223
func (client *Client) newResponse(httpResponse *http.Response) (*Response, error) {
224
	if httpResponse == nil {
225
		return nil, fmt.Errorf("%T cannot be nil", httpResponse)
226
	}
227
228
	resp := new(Response)
229
	resp.HTTPResponse = httpResponse
230
231
	buf, err := ioutil.ReadAll(resp.HTTPResponse.Body)
232
	if err != nil {
233
		return nil, err
234
	}
235
	resp.Body = &buf
236
237
	return resp, resp.Error()
238
}
239