Issues (2)

src/Client.php (2 issues)

Labels
Severity
1
<?php
2
3
namespace HenryEjemuta\Peyflex;
4
5
use GuzzleHttp\Client as GuzzleClient;
6
use GuzzleHttp\Exception\GuzzleException;
7
8
class Client
9
{
10
    /**
11
     * The base URL for the Peyflex API.
12
     */
13
    private const BASE_URL = 'https://client.peyflex.com.ng/api/';
14
15
    /**
16
     * The API Token.
17
     *
18
     * @var string
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 $token The API Token.
33
     * @param array $config Configuration options (base_url, timeout, etc.).
34
     */
35
    public function __construct(string $token, 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
        if (! isset($config['handler_stack'])) {
51
            // Only add retry middleware if we are using the default stack
52
            // OR we can add it regardless, but we need to be careful about duplication if the passed stack has it.
53
            // Let's append it regardless, assuming the test usage knows what it's doing.
54
        }
55
56
        $handlerStack->push(\GuzzleHttp\Middleware::retry(
57
            function ($retriesCount, $request, $response = null, $exception = null) use ($retries) {
58
                // Retry on connection exceptions
59
                if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
60
                    return true;
61
                }
62
63
                if ($exception instanceof \GuzzleHttp\Exception\RequestException && $exception->hasResponse()) {
64
                    $response = $exception->getResponse();
65
                }
66
67
                // Retry on server errors (5xx)
68
                if ($response && $response->getStatusCode() >= 500) {
69
                    // Check retries count before deciding to retry
70
                    if ($retriesCount >= $retries) {
71
                        return false;
72
                    }
73
74
                    return true;
75
                }
76
77
                return false;
78
            },
79
            function ($retriesCount) {
80
                // Exponential backoff
81
                return pow(2, $retriesCount - 1) * 1000;
82
            }
83
        ));
84
85
        $guzzleConfig = [
86
            'base_uri' => $baseUrl,
87
            'timeout' => $timeout,
88
            'handler' => $handlerStack,
89
            'headers' => [
90
                'Accept' => 'application/json',
91
                'Authorization' => 'Bearer '.$this->token,
92
                'Content-Type' => 'application/json',
93
            ],
94
        ];
95
96
        $this->httpClient = new GuzzleClient($guzzleConfig);
97
    }
98
99
    /**
100
     * Make a request to the API.
101
     *
102
     * @param string $method
103
     * @param string $uri
104
     * @param array $options
105
     * @return array
106
     * @throws PeyflexException
107
     */
108
    private function request(string $method, string $uri, array $options = []): array
109
    {
110
        try {
111
            $response = $this->httpClient->request($method, $uri, $options);
112
            $content = $response->getBody()->getContents();
113
            $data = json_decode($content, true);
114
115
            if (json_last_error() !== JSON_ERROR_NONE) {
116
                throw new PeyflexException('Failed to decode JSON response: '.json_last_error_msg());
117
            }
118
119
            return $data;
120
        } catch (GuzzleException $e) {
121
            // Attempt to get error message from response body if available
122
            $message = $e->getMessage();
123
            if ($e->hasResponse()) {
0 ignored issues
show
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

123
            if ($e->/** @scrutinizer ignore-call */ hasResponse()) {
Loading history...
124
                $responseBody = $e->getResponse()->getBody()->getContents();
0 ignored issues
show
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

124
                $responseBody = $e->/** @scrutinizer ignore-call */ getResponse()->getBody()->getContents();
Loading history...
125
                $errorData = json_decode($responseBody, true);
126
                if (isset($errorData['message'])) {
127
                    $message = $errorData['message'];
128
                } elseif (isset($errorData['error'])) {
129
                    $message = $errorData['error'];
130
                }
131
            }
132
            throw new PeyflexException('API Request Failed: '.$message, $e->getCode(), $e);
133
        }
134
    }
135
136
    /**
137
     * Get authenticated user profile.
138
     *
139
     * @return array
140
     * @throws PeyflexException
141
     */
142
    public function getProfile(): array
143
    {
144
        return $this->request('GET', 'user/profile');
145
    }
146
147
    /**
148
     * Get wallet balance.
149
     *
150
     * @return array
151
     * @throws PeyflexException
152
     */
153
    public function getBalance(): array
154
    {
155
        return $this->request('GET', 'user/balance');
156
    }
157
158
    /**
159
     * Get airtime networks.
160
     *
161
     * @return array
162
     * @throws PeyflexException
163
     */
164
    public function getAirtimeNetworks(): array
165
    {
166
        return $this->request('GET', 'airtime/networks');
167
    }
168
169
    /**
170
     * Purchase airtime.
171
     *
172
     * @param string $network The network ID (e.g., 'mtn', 'glo').
173
     * @param string $phone The phone number.
174
     * @param float $amount The amount to top up.
175
     * @return array
176
     * @throws PeyflexException
177
     */
178
    public function purchaseAirtime(string $network, string $phone, float $amount): array
179
    {
180
        return $this->request('POST', 'airtime/purchase', [
181
            'json' => [
182
                'network' => $network,
183
                'phone' => $phone,
184
                'amount' => $amount,
185
            ],
186
        ]);
187
    }
188
189
    /**
190
     * Get data networks.
191
     *
192
     * @return array
193
     * @throws PeyflexException
194
     */
195
    public function getDataNetworks(): array
196
    {
197
        return $this->request('GET', 'data/networks');
198
    }
199
200
    /**
201
     * Get data plans for a network.
202
     *
203
     * @param string $networkId The network identifier (e.g., 'mtn_sme_data').
204
     * @return array
205
     * @throws PeyflexException
206
     */
207
    public function getDataPlans(string $networkId): array
208
    {
209
        return $this->request('GET', 'data/plans', [
210
            'query' => ['network' => $networkId],
211
        ]);
212
    }
213
214
    /**
215
     * Purchase data.
216
     *
217
     * @param string $networkId The network identifier.
218
     * @param string $phone The phone number.
219
     * @param string $planId The plan identifier (from getDataPlans).
220
     * @return array
221
     * @throws PeyflexException
222
     */
223
    public function purchaseData(string $networkId, string $phone, string $planId): array
224
    {
225
        return $this->request('POST', 'data/purchase', [
226
            'json' => [
227
                'network' => $networkId,
228
                'phone' => $phone,
229
                'plan' => $planId,
230
            ],
231
        ]);
232
    }
233
234
    /**
235
     * Get cable providers.
236
     *
237
     * @return array
238
     * @throws PeyflexException
239
     */
240
    public function getCableProviders(): array
241
    {
242
        return $this->request('GET', 'cable/providers');
243
    }
244
245
    /**
246
     * Verify cable IUC number.
247
     *
248
     * @param string $providerId The provider identifier (e.g., 'dstv').
249
     * @param string $iucNumber The IUC/Smartcard number.
250
     * @return array
251
     * @throws PeyflexException
252
     */
253
    public function verifyCable(string $providerId, string $iucNumber): array
254
    {
255
        return $this->request('POST', 'cable/verify', [
256
            'json' => [
257
                'provider' => $providerId,
258
                'iuc_number' => $iucNumber,
259
            ],
260
        ]);
261
    }
262
263
    /**
264
     * Purchase cable subscription.
265
     *
266
     * @param string $providerId The provider identifier.
267
     * @param string $iucNumber The IUC number.
268
     * @param string $planId The plan identifier.
269
     * @return array
270
     * @throws PeyflexException
271
     */
272
    public function purchaseCable(string $providerId, string $iucNumber, string $planId): array
273
    {
274
        return $this->request('POST', 'cable/purchase', [
275
            'json' => [
276
                'provider' => $providerId,
277
                'iuc_number' => $iucNumber,
278
                'plan' => $planId,
279
            ],
280
        ]);
281
    }
282
283
    /**
284
     * Get electricity plans/providers.
285
     *
286
     * @return array
287
     * @throws PeyflexException
288
     */
289
    public function getElectricityPlans(): array
290
    {
291
        return $this->request('GET', 'electricity/plans', [
292
            'query' => ['identifier' => 'electricity'],
293
        ]);
294
    }
295
296
    /**
297
     * Verify electricity meter.
298
     *
299
     * @param string $providerId The provider ID (e.g., 'ikeja_electric').
300
     * @param string $meterNumber The meter number.
301
     * @param string $type The meter type ('prepaid' or 'postpaid').
302
     * @return array
303
     * @throws PeyflexException
304
     */
305
    public function verifyMeter(string $providerId, string $meterNumber, string $type = 'prepaid'): array
306
    {
307
        return $this->request('POST', 'electricity/verify', [
308
            'json' => [
309
                'identifier' => 'electricity',
310
                'provider' => $providerId,
311
                'meter_number' => $meterNumber,
312
                'type' => $type,
313
            ],
314
        ]);
315
    }
316
317
    /**
318
     * Purchase electricity token.
319
     *
320
     * @param string $providerId The provider ID.
321
     * @param string $meterNumber The meter number.
322
     * @param float $amount The amount to purchase.
323
     * @param string $type The meter type ('prepaid' or 'postpaid').
324
     * @return array
325
     * @throws PeyflexException
326
     */
327
    public function purchaseElectricity(string $providerId, string $meterNumber, float $amount, string $type = 'prepaid'): array
328
    {
329
        return $this->request('POST', 'electricity/purchase', [
330
            'json' => [
331
                'provider' => $providerId,
332
                'meter_number' => $meterNumber,
333
                'amount' => $amount,
334
                'type' => $type,
335
            ],
336
        ]);
337
    }
338
}
339