Passed
Push — main ( 206f54...06bb6a )
by Acho
02:26
created

campay.*Client.WithdrawSync   B

Complexity

Conditions 7

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 11
nop 2
dl 0
loc 15
rs 8
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
	Utilities           *utilitiesService
34
}
35
36
// New creates and returns a new campay.Client from a slice of campay.ClientOption.
37
func New(options ...ClientOption) *Client {
38
	config := defaultClientConfig()
39
40
	for _, option := range options {
41
		option.apply(config)
42
	}
43
44
	client := &Client{
45
		httpClient:  config.httpClient,
46
		environment: config.environment,
47
		apiUsername: config.apiUsername,
48
		apiPassword: config.apiPassword,
49
		mutex:       sync.Mutex{},
50
	}
51
52
	client.common.client = client
53
	client.Transaction = (*transactionService)(&client.common)
54
	client.Utilities = (*utilitiesService)(&client.common)
55
	return client
56
}
57
58
// Token Gets the access token
59
// POST /token/
60
// API Doc: https://documenter.getpostman.com/view/2391374/T1LV8PVA#8803168b-d451-4d65-b8cc-85e385bc3050
61
func (client *Client) Token(ctx context.Context) (*Token, *Response, error) {
62
	payload := map[string]string{
63
		"username": client.apiUsername,
64
		"password": client.apiPassword,
65
	}
66
67
	request, err := client.newRequest(ctx, http.MethodPost, "/api/token/", payload)
68
	if err != nil {
69
		return nil, nil, err
70
	}
71
72
	resp, err := client.do(request)
73
	if err != nil {
74
		return nil, resp, err
75
	}
76
77
	var token Token
78
	if err = json.Unmarshal(*resp.Body, &token); err != nil {
79
		return nil, resp, err
80
	}
81
82
	return &token, resp, nil
83
}
84
85
// ValidateCallback checks if the signature was encrypted with the webhook key
86
func (client *Client) ValidateCallback(signature string, webhookKey []byte) error {
87
	_, err := jwt.Parse(signature, func(token *jwt.Token) (interface{}, error) {
88
		return webhookKey, nil
89
	})
90
	return err
91
}
92
93
// Collect Requests a Payment
94
// POST /collect/
95
// API Doc: https://documenter.getpostman.com/view/2391374/T1LV8PVA#31757962-2e07-486b-a6f4-a7cc7a06d032
96
func (client *Client) Collect(ctx context.Context, params *CollectParams) (*CollectResponse, *Response, error) {
97
	err := client.refreshToken(ctx)
98
	if err != nil {
99
		return nil, nil, err
100
	}
101
102
	request, err := client.newRequest(ctx, http.MethodPost, "/api/collect/", params)
103
	if err != nil {
104
		return nil, nil, err
105
	}
106
107
	response, err := client.do(request)
108
	if err != nil {
109
		return nil, response, err
110
	}
111
112
	var collectResponse CollectResponse
113
	if err = json.Unmarshal(*response.Body, &collectResponse); err != nil {
114
		return nil, response, err
115
	}
116
117
	return &collectResponse, response, nil
118
}
119
120
// Withdraw funds to a mobile money account
121
// POST /withdraw/
122
// API Doc: https://documenter.getpostman.com/view/2391374/T1LV8PVA#885dbde0-b0dd-4514-a0f9-f84fc83df12d
123
func (client *Client) Withdraw(ctx context.Context, params *WithdrawParams) (*WithdrawResponse, *Response, error) {
124
	err := client.refreshToken(ctx)
125
	if err != nil {
126
		return nil, nil, err
127
	}
128
129
	request, err := client.newRequest(ctx, http.MethodPost, "/api/withdraw/", params)
130
	if err != nil {
131
		return nil, nil, err
132
	}
133
134
	response, err := client.do(request)
135
	if err != nil {
136
		return nil, response, err
137
	}
138
139
	var withdrawResponse WithdrawResponse
140
	if err = json.Unmarshal(*response.Body, &withdrawResponse); err != nil {
141
		return nil, response, err
142
	}
143
144
	return &withdrawResponse, response, nil
145
}
146
147
// WithdrawSync transfers money to a mobile number and waits for the transaction to be completed.
148
// POST /withdraw/
149
// API Doc: https://documenter.getpostman.com/view/2391374/T1LV8PVA#885dbde0-b0dd-4514-a0f9-f84fc83df12d
150
func (client *Client) WithdrawSync(ctx context.Context, params *WithdrawParams) (*Transaction, *Response, error) {
151
	transaction, response, err := client.Withdraw(ctx, params)
152
	if err != nil {
153
		return nil, response, err
154
	}
155
156
	// wait for completion in 2 minutes
157
	counter := 1
158
	for {
159
		status, response, err := client.Transaction.Get(ctx, transaction.Reference)
160
		if err != nil || !status.IsPending() || ctx.Err() != nil || counter == 12 {
161
			return status, response, err
162
		}
163
		time.Sleep(10 * time.Second)
164
		counter++
165
	}
166
}
167
168
// newRequest creates an API request. A relative URL can be provided in uri,
169
// in which case it is resolved relative to the BaseURL of the Client.
170
// URI's should always be specified without a preceding slash.
171
func (client *Client) newRequest(ctx context.Context, method, uri string, body interface{}) (*http.Request, error) {
172
	var buf io.ReadWriter
173
	if body != nil {
174
		buf = &bytes.Buffer{}
175
		enc := json.NewEncoder(buf)
176
		enc.SetEscapeHTML(false)
177
		err := enc.Encode(body)
178
		if err != nil {
179
			return nil, err
180
		}
181
	}
182
183
	req, err := http.NewRequestWithContext(ctx, method, client.environment.String()+uri, buf)
184
	if err != nil {
185
		return nil, err
186
	}
187
188
	req.Header.Set("Content-Type", "application/json")
189
190
	if len(client.token) > 0 {
191
		req.Header.Set("Authorization", "Token "+client.token)
192
	}
193
194
	return req, nil
195
}
196
197
// do carries out an HTTP request and returns a Response
198
func (client *Client) do(req *http.Request) (*Response, error) {
199
	if req == nil {
200
		return nil, fmt.Errorf("%T cannot be nil", req)
201
	}
202
203
	httpResponse, err := client.httpClient.Do(req)
204
	if err != nil {
205
		return nil, err
206
	}
207
208
	defer func() { _ = httpResponse.Body.Close() }()
209
210
	resp, err := client.newResponse(httpResponse)
211
	if err != nil {
212
		return resp, err
213
	}
214
215
	_, err = io.Copy(ioutil.Discard, httpResponse.Body)
216
	if err != nil {
217
		return resp, err
218
	}
219
220
	return resp, nil
221
}
222
223
// refreshToken refreshes the authentication Token
224
func (client *Client) refreshToken(ctx context.Context) error {
225
	if client.tokenExpirationTime > time.Now().UTC().Unix() {
226
		return nil
227
	}
228
229
	client.mutex.Lock()
230
	defer client.mutex.Unlock()
231
232
	client.token = ""
233
234
	token, _, err := client.Token(ctx)
235
	if err != nil {
236
		return err
237
	}
238
239
	client.token = token.Token
240
	client.tokenExpirationTime = time.Now().UTC().Unix() + token.ExpiresIn - 1000 // Give extra 100 second buffer
241
242
	return nil
243
}
244
245
// newResponse converts an *http.Response to *Response
246
func (client *Client) newResponse(httpResponse *http.Response) (*Response, error) {
247
	if httpResponse == nil {
248
		return nil, fmt.Errorf("%T cannot be nil", httpResponse)
249
	}
250
251
	resp := new(Response)
252
	resp.HTTPResponse = httpResponse
253
254
	buf, err := ioutil.ReadAll(resp.HTTPResponse.Body)
255
	if err != nil {
256
		return nil, err
257
	}
258
	resp.Body = &buf
259
260
	return resp, resp.Error()
261
}
262