Client   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 381
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 119
c 1
b 0
f 0
dl 0
loc 381
rs 9.6
wmc 35

14 Methods

Rating   Name   Duplication   Size   Complexity  
A verifyCustomer() 0 12 2
A authenticate() 0 20 2
A fundBettingAccount() 0 8 1
A purchaseAirtime() 0 8 1
A getBalance() 0 3 1
A requeryOrder() 0 5 1
B request() 0 35 10
A purchaseCableTV() 0 8 1
A purchaseData() 0 8 1
A purchaseElectricity() 0 9 1
A getDataVariations() 0 8 2
A purchaseEPins() 0 8 1
A getCableVariations() 0 8 2
B __construct() 0 59 9
1
<?php
2
3
namespace HenryEjemuta\Vtung;
4
5
use GuzzleHttp\Client as GuzzleClient;
6
use GuzzleHttp\Exception\GuzzleException;
7
8
class Client
9
{
10
    /**
11
     * The base URL for the VTU.ng API.
12
     */
13
    private const BASE_URL = 'https://vtu.ng/wp-json/';
14
15
    /**
16
     * The API Token.
17
     *
18
     * @var string|null
19
     */
20
    private $token;
21
22
    /**
23
     * The Guzzle HTTP Client instance.
24
     *
25
     * @var GuzzleClient
26
     */
27
    private $httpClient;
28
29
    /**
30
     * Client constructor.
31
     *
32
     * @param string|null $token The API Token.
33
     * @param array $config Configuration options (base_url, timeout, etc.).
34
     */
35
    public function __construct(?string $token = null, array $config = [])
36
    {
37
        $this->token = $token;
38
39
        $baseUrl = $config['base_url'] ?? self::BASE_URL;
40
        // Ensure base URL ends with a slash
41
        if (substr($baseUrl, -1) !== '/') {
42
            $baseUrl .= '/';
43
        }
44
45
        $timeout = $config['timeout'] ?? 30;
46
        $retries = $config['retries'] ?? 3;
47
48
        $handlerStack = $config['handler_stack'] ?? \GuzzleHttp\HandlerStack::create();
49
50
        $handlerStack->push(\GuzzleHttp\Middleware::retry(
51
            function ($retriesCount, $request, $response = null, $exception = null) use ($retries) {
52
                // Retry on connection exceptions
53
                if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
54
                    return true;
55
                }
56
57
                if ($exception instanceof \GuzzleHttp\Exception\RequestException && $exception->hasResponse()) {
58
                    $response = $exception->getResponse();
59
                }
60
61
                // Retry on server errors (5xx)
62
                if ($response && $response->getStatusCode() >= 500) {
63
                    // Check retries count before deciding to retry
64
                    if ($retriesCount >= $retries) {
65
                        return false;
66
                    }
67
68
                    return true;
69
                }
70
71
                return false;
72
            },
73
            function ($retriesCount) {
74
                // Exponential backoff
75
                return pow(2, $retriesCount - 1) * 1000;
76
            }
77
        ));
78
79
        $guzzleConfig = [
80
            'base_uri' => $baseUrl,
81
            'timeout' => $timeout,
82
            'handler' => $handlerStack,
83
            'headers' => [
84
                'Accept' => 'application/json',
85
                'Content-Type' => 'application/json',
86
            ],
87
        ];
88
89
        if ($this->token) {
90
            $guzzleConfig['headers']['Authorization'] = 'Bearer '.$this->token;
91
        }
92
93
        $this->httpClient = new GuzzleClient($guzzleConfig);
94
    }
95
96
    /**
97
     * Authenticate and retrieve a token.
98
     *
99
     * @param string $username
100
     * @param string $password
101
     * @return array
102
     * @throws VtungException
103
     */
104
    public function authenticate(string $username, string $password): array
105
    {
106
        $response = $this->request('POST', 'jwt-auth/v1/token', [
107
            'json' => [
108
                'username' => $username,
109
                'password' => $password,
110
            ],
111
        ], false);
112
113
        if (isset($response['token'])) {
114
            $this->token = $response['token'];
115
            // Re-initialize client to include token in headers for future requests
116
            // Or mostly users might just use the returned token to instantiate a new Client or we can update a property if we made headers mutable.
117
            // But Guzzle clients are immutable regarding config.
118
            // However, we can simply pass the token in each request if we stored it, or tell the user to re-instantiate.
119
            // For now, let's just return the response. Ideally, the user should instantiate with the token.
120
            // But for convenience, let's allow updating the token on the fly if we want, but simpler is returning it.
121
        }
122
123
        return $response;
124
    }
125
126
    /**
127
     * Make a request to the API.
128
     *
129
     * @param string $method
130
     * @param string $uri
131
     * @param array $options
132
     * @param bool $authRequired
133
     * @return array
134
     * @throws VtungException
135
     */
136
    private function request(string $method, string $uri, array $options = [], bool $authRequired = true): array
137
    {
138
        if ($authRequired && ! $this->token) {
139
            throw new VtungException('API Token is required for this request.');
140
        }
141
142
        // If we acquired a token after construction via authenticate() and didn't rebuild the client,
143
        // we need to inject the Authorization header manually if it's missing.
144
        // But since we can't easily modify the client default headers, we merge it here.
145
        if ($authRequired && $this->token) {
146
            $options['headers']['Authorization'] = 'Bearer '.$this->token;
147
        }
148
149
        try {
150
            $response = $this->httpClient->request($method, $uri, $options);
151
            $content = $response->getBody()->getContents();
152
            $data = json_decode($content, true);
153
154
            if (json_last_error() !== JSON_ERROR_NONE) {
155
                throw new VtungException('Failed to decode JSON response: '.json_last_error_msg());
156
            }
157
158
            return $data;
159
        } catch (GuzzleException $e) {
160
            $message = $e->getMessage();
161
            if ($e->hasResponse()) {
0 ignored issues
show
Bug introduced by
The method hasResponse() does not exist on GuzzleHttp\Exception\GuzzleException. It seems like you code against a sub-type of GuzzleHttp\Exception\GuzzleException such as GuzzleHttp\Exception\RequestException. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

161
            if ($e->/** @scrutinizer ignore-call */ hasResponse()) {
Loading history...
162
                $responseBody = $e->getResponse()->getBody()->getContents();
0 ignored issues
show
Bug introduced by
The method getResponse() does not exist on GuzzleHttp\Exception\GuzzleException. It seems like you code against a sub-type of GuzzleHttp\Exception\GuzzleException such as GuzzleHttp\Exception\RequestException. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

162
                $responseBody = $e->/** @scrutinizer ignore-call */ getResponse()->getBody()->getContents();
Loading history...
163
                $errorData = json_decode($responseBody, true);
164
                if (isset($errorData['message'])) {
165
                    $message = $errorData['message'];
166
                } elseif (isset($errorData['code'])) {
167
                    $message = $errorData['code']; // vtu.ng sometimes returns code/message structure
168
                }
169
            }
170
            throw new VtungException('API Request Failed: '.$message, $e->getCode(), $e);
171
        }
172
    }
173
174
    /**
175
     * Get wallet balance.
176
     *
177
     * @return array
178
     * @throws VtungException
179
     */
180
    public function getBalance(): array
181
    {
182
        return $this->request('GET', 'api/v2/balance');
183
    }
184
185
    /**
186
     * Purchase airtime.
187
     *
188
     * @param string $network The network ID (e.g., 'mtn', 'glo').
189
     * @param string $phone The phone number.
190
     * @param float $amount The amount to top up.
191
     * @param string $requestId Unique request ID.
192
     * @return array
193
     * @throws VtungException
194
     */
195
    public function purchaseAirtime(string $network, string $phone, float $amount, string $requestId): array
196
    {
197
        return $this->request('POST', 'api/v2/airtime', [
198
            'json' => [
199
                'request_id' => $requestId,
200
                'service_id' => $network,
201
                'phone' => $phone,
202
                'amount' => $amount,
203
            ],
204
        ]);
205
    }
206
207
    /**
208
     * Get data variations.
209
     *
210
     * @param string|null $serviceId Optional service ID to filter (e.g., 'mtn').
211
     * @return array
212
     * @throws VtungException
213
     */
214
    public function getDataVariations(?string $serviceId = null): array
215
    {
216
        $options = [];
217
        if ($serviceId) {
218
            $options['query'] = ['service_id' => $serviceId];
219
        }
220
221
        return $this->request('GET', 'api/v2/variations/data', $options, false);
222
    }
223
224
    /**
225
     * Purchase data.
226
     *
227
     * @param string $serviceId The service ID (e.g., 'mtn').
228
     * @param string $phone The phone number.
229
     * @param string $variationId The variation ID.
230
     * @param string $requestId Unique request ID.
231
     * @return array
232
     * @throws VtungException
233
     */
234
    public function purchaseData(string $serviceId, string $phone, string $variationId, string $requestId): array
235
    {
236
        return $this->request('POST', 'api/v2/data', [
237
            'json' => [
238
                'request_id' => $requestId,
239
                'service_id' => $serviceId,
240
                'phone' => $phone,
241
                'variation_id' => $variationId,
242
            ],
243
        ]);
244
    }
245
246
    /**
247
     * Verify customer (Electricity, Cable TV, Betting).
248
     *
249
     * @param string $customerId The customer ID / Meter Number / Smartcard Number.
250
     * @param string $serviceId The service ID.
251
     * @param string|null $variationId Optional variation ID (required for electricity).
252
     * @return array
253
     * @throws VtungException
254
     */
255
    public function verifyCustomer(string $customerId, string $serviceId, ?string $variationId = null): array
256
    {
257
        $payload = [
258
            'customer_id' => $customerId,
259
            'service_id' => $serviceId,
260
        ];
261
        if ($variationId) {
262
            $payload['variation_id'] = $variationId;
263
        }
264
265
        return $this->request('POST', 'api/v2/verify-customer', [
266
            'json' => $payload,
267
        ]);
268
    }
269
270
    /**
271
     * Purchase electricity.
272
     *
273
     * @param string $requestId Unique request ID.
274
     * @param string $customerId The customer ID / Meter Number.
275
     * @param string $serviceId The service ID (e.g., 'ikeja-electric').
276
     * @param string $variationId The variation ID (e.g., 'prepaid').
277
     * @param float $amount The amount to purchase.
278
     * @return array
279
     * @throws VtungException
280
     */
281
    public function purchaseElectricity(string $requestId, string $customerId, string $serviceId, string $variationId, float $amount): array
282
    {
283
        return $this->request('POST', 'api/v2/electricity', [
284
            'json' => [
285
                'request_id' => $requestId,
286
                'customer_id' => $customerId,
287
                'service_id' => $serviceId,
288
                'variation_id' => $variationId,
289
                'amount' => $amount,
290
            ],
291
        ]);
292
    }
293
294
    /**
295
     * Fund betting account.
296
     *
297
     * @param string $requestId Unique request ID.
298
     * @param string $customerId The customer ID.
299
     * @param string $serviceId The service ID (e.g., 'Bet9ja').
300
     * @param float $amount The amount to fund.
301
     * @return array
302
     * @throws VtungException
303
     */
304
    public function fundBettingAccount(string $requestId, string $customerId, string $serviceId, float $amount): array
305
    {
306
        return $this->request('POST', 'api/v2/betting', [
307
            'json' => [
308
                'request_id' => $requestId,
309
                'customer_id' => $customerId,
310
                'service_id' => $serviceId,
311
                'amount' => $amount,
312
            ],
313
        ]);
314
    }
315
316
    /**
317
     * Get cable TV variations.
318
     *
319
     * @param string|null $serviceId The service ID (e.g., 'dstv').
320
     * @return array
321
     * @throws VtungException
322
     */
323
    public function getCableVariations(?string $serviceId = null): array
324
    {
325
        $options = [];
326
        if ($serviceId) {
327
            $options['query'] = ['service_id' => $serviceId];
328
        }
329
330
        return $this->request('GET', 'api/v2/variations/tv', $options, false);
331
    }
332
333
    /**
334
     * Purchase Cable TV subscription.
335
     *
336
     * @param string $requestId Unique request ID.
337
     * @param string $customerId The Smartcard/IUC number.
338
     * @param string $serviceId The service ID (e.g., 'dstv').
339
     * @param string $variationId The variation ID (plan).
340
     * @return array
341
     * @throws VtungException
342
     */
343
    public function purchaseCableTV(string $requestId, string $customerId, string $serviceId, string $variationId): array
344
    {
345
        return $this->request('POST', 'api/v2/tv', [
346
            'json' => [
347
                'request_id' => $requestId,
348
                'customer_id' => $customerId,
349
                'service_id' => $serviceId,
350
                'variation_id' => $variationId,
351
            ],
352
        ]);
353
    }
354
355
    /**
356
     * Purchase ePINs.
357
     *
358
     * @param string $requestId Unique request ID.
359
     * @param string $serviceId The service ID (e.g., 'mtn').
360
     * @param int $value The value of the card (e.g., 100, 200, 500).
361
     * @param int $quantity The quantity to purchase.
362
     * @return array
363
     * @throws VtungException
364
     */
365
    public function purchaseEPins(string $requestId, string $serviceId, int $value, int $quantity): array
366
    {
367
        return $this->request('POST', 'api/v2/epins', [
368
            'json' => [
369
                'request_id' => $requestId,
370
                'service_id' => $serviceId,
371
                'value' => $value,
372
                'quantity' => $quantity,
373
            ],
374
        ]);
375
    }
376
377
    /**
378
     * Requery order status.
379
     *
380
     * @param string $requestId The request ID of the order.
381
     * @return array
382
     * @throws VtungException
383
     */
384
    public function requeryOrder(string $requestId): array
385
    {
386
        return $this->request('POST', 'api/v2/requery', [
387
            'json' => [
388
                'request_id' => $requestId,
389
            ],
390
        ]);
391
    }
392
}
393