campay.*Client.newRequest   A
last analyzed

Complexity

Conditions 5

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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