orangemoney.*Client.do   B
last analyzed

Complexity

Conditions 6

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nop 1
dl 0
loc 23
rs 8.6666
c 0
b 0
f 0
1
package orangemoney
2
3
import (
4
	"bytes"
5
	"context"
6
	"encoding/json"
7
	"fmt"
8
	"io"
9
	"net/http"
10
	"net/url"
11
	"strconv"
12
	"strings"
13
	"sync"
14
	"time"
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
27
	baseURL   string
28
	username  string
29
	password  string
30
	authToken string
31
32
	accessToken         string
33
	tokenExpirationTime int64
34
	mutex               sync.Mutex
35
36
	MerchantPayment *merchantPaymentService
37
}
38
39
// New creates and returns a new campay.Client from a slice of campay.ClientOption.
40
func New(options ...Option) *Client {
41
	config := defaultClientConfig()
42
43
	for _, option := range options {
44
		option.apply(config)
45
	}
46
47
	client := &Client{
48
		httpClient: config.httpClient,
49
		baseURL:    config.baseURL,
50
		username:   config.username,
51
		password:   config.password,
52
		authToken:  config.authToken,
53
		mutex:      sync.Mutex{},
54
	}
55
56
	client.common.client = client
57
	client.MerchantPayment = (*merchantPaymentService)(&client.common)
58
	return client
59
}
60
61
// AccessToken fetches the access token used to authenticate api requests.
62
func (client *Client) AccessToken(ctx context.Context) (*AccessTokenResponse, *Response, error) {
63
	data := url.Values{}
64
	data.Set("grant_type", "client_credentials")
65
66
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, client.baseURL+"/token", strings.NewReader(data.Encode()))
67
	if err != nil {
68
		return nil, nil, err
69
	}
70
71
	request.SetBasicAuth(client.username, client.password)
72
	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
73
	request.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
74
75
	resp, err := client.do(request)
76
	if err != nil {
77
		return nil, resp, err
78
	}
79
80
	var token AccessTokenResponse
81
	if err = json.Unmarshal(*resp.Body, &token); err != nil {
82
		return nil, resp, err
83
	}
84
85
	return &token, resp, nil
86
}
87
88
// refreshToken refreshes the authentication AccessTokenResponse
89
func (client *Client) refreshToken(ctx context.Context) error {
90
	client.mutex.Lock()
91
	defer client.mutex.Unlock()
92
93
	if client.tokenExpirationTime > time.Now().UTC().Unix() {
94
		return nil
95
	}
96
97
	client.accessToken = ""
98
99
	token, _, err := client.AccessToken(ctx)
100
	if err != nil {
101
		return err
102
	}
103
104
	client.accessToken = token.AccessToken
105
	client.tokenExpirationTime = time.Now().UTC().Unix() + token.ExpiresIn - 100 // Give extra 100 second buffer
106
107
	return nil
108
}
109
110
// newRequest creates an API request. A relative URL can be provided in uri,
111
// in which case it is resolved relative to the BaseURL of the Client.
112
// URI's should always be specified without a preceding slash.
113
func (client *Client) newRequest(ctx context.Context, method, uri string, body any) (*http.Request, error) {
114
	var buf io.ReadWriter
115
	if body != nil {
116
		buf = &bytes.Buffer{}
117
		enc := json.NewEncoder(buf)
118
		enc.SetEscapeHTML(false)
119
		err := enc.Encode(body)
120
		if err != nil {
121
			return nil, err
122
		}
123
	}
124
125
	req, err := http.NewRequestWithContext(ctx, method, client.baseURL+uri, buf)
126
	if err != nil {
127
		return nil, err
128
	}
129
130
	req.Header.Set("X-AUTH-TOKEN", client.authToken)
131
	req.Header.Set("Authorization", "Bearer "+client.accessToken)
132
	req.Header.Set("Content-Type", "application/json")
133
	req.Header.Set("Accept", "application/json")
134
135
	return req, nil
136
}
137
138
// do carries out an HTTP request and returns a Response
139
func (client *Client) do(req *http.Request) (*Response, error) {
140
	if req == nil {
141
		return nil, fmt.Errorf("%T cannot be nil", req)
142
	}
143
144
	httpResponse, err := client.httpClient.Do(req)
145
	if err != nil {
146
		return nil, err
147
	}
148
149
	defer func() { _ = httpResponse.Body.Close() }()
150
151
	resp, err := client.newResponse(httpResponse)
152
	if err != nil {
153
		return resp, err
154
	}
155
156
	_, err = io.Copy(io.Discard, httpResponse.Body)
157
	if err != nil {
158
		return resp, err
159
	}
160
161
	return resp, nil
162
}
163
164
// newResponse converts an *http.Response to *Response
165
func (client *Client) newResponse(httpResponse *http.Response) (*Response, error) {
166
	if httpResponse == nil {
167
		return nil, fmt.Errorf("%T cannot be nil", httpResponse)
168
	}
169
170
	resp := new(Response)
171
	resp.HTTPResponse = httpResponse
172
173
	buf, err := io.ReadAll(resp.HTTPResponse.Body)
174
	if err != nil {
175
		return nil, err
176
	}
177
	resp.Body = &buf
178
179
	return resp, resp.Error()
180
}
181