Issues (2)

src/Client.php (2 issues)

Labels
Severity
1
<?php
2
3
namespace HenryEjemuta\Vtpass;
4
5
use GuzzleHttp\Client as GuzzleClient;
6
use GuzzleHttp\Exception\GuzzleException;
7
8
class Client
9
{
10
    /**
11
     * The base URL for the VTpass API.
12
     */
13
    private const BASE_URL = 'https://vtpass.com/api/';
14
    private const SANDBOX_BASE_URL = 'https://sandbox.vtpass.com/api/';
15
16
    /**
17
     * @var string
18
     */
19
    private $apiKey;
20
21
    /**
22
     * @var string
23
     */
24
    private $publicKey;
25
26
    /**
27
     * @var string
28
     */
29
    private $secretKey;
30
31
    /**
32
     * @var GuzzleClient
33
     */
34
    private $httpClient;
35
36
    /**
37
     * Client constructor.
38
     *
39
     * @param string $apiKey
40
     * @param string $publicKey
41
     * @param string $secretKey
42
     * @param array $config
43
     */
44
    public function __construct(string $apiKey, string $publicKey, string $secretKey, array $config = [])
45
    {
46
        $this->apiKey = $apiKey;
47
        $this->publicKey = $publicKey;
48
        $this->secretKey = $secretKey;
49
50
        $baseUrl = $config['base_url'] ?? self::BASE_URL;
51
        if (isset($config['sandbox']) && $config['sandbox'] === true) {
52
            $baseUrl = self::SANDBOX_BASE_URL;
53
        }
54
55
        // Ensure base URL ends with a slash
56
        if (substr($baseUrl, -1) !== '/') {
57
            $baseUrl .= '/';
58
        }
59
60
        $timeout = $config['timeout'] ?? 30;
61
62
        // Standard Guzzle configuration
63
        $guzzleConfig = [
64
            'base_uri' => $baseUrl,
65
            'timeout' => $timeout,
66
            'headers' => [
67
                'Accept' => 'application/json',
68
                'Content-Type' => 'application/json',
69
            ],
70
        ];
71
72
        // Merge user config into Guzzle config (preserving keys, user config overwrites defaults if needed, 
73
        // but typically we just want to add things like 'handler')
74
        $guzzleConfig = array_merge($guzzleConfig, $config);
75
76
        /*
77
         * Note: VTpass documentation specifies:
78
         * GET request: api-key and public-key
79
         * POST request: api-key and secret-key
80
         * We will add these dynamically in the request method.
81
         */
82
83
        $this->httpClient = new GuzzleClient($guzzleConfig);
84
    }
85
86
    /**
87
     * Generate a unique Request ID.
88
     * Format: YYYYMMDDHHII + random string.
89
     *
90
     * @return string
91
     */
92
    public function generateRequestId(): string
93
    {
94
        // Set timezone to Lagos as per docs
95
        $date = new \DateTime("now", new \DateTimeZone('Africa/Lagos'));
96
        $prefix = $date->format('YmdHi'); // YYYYMMDDHHII
97
        $random = bin2hex(random_bytes(5)); // Random string to ensure uniqueness
98
        return $prefix . $random;
99
    }
100
101
    /**
102
     * Make a request to the API.
103
     *
104
     * @param string $method
105
     * @param string $uri
106
     * @param array $options
107
     * @return array
108
     * @throws VtpassException
109
     */
110
    private function request(string $method, string $uri, array $options = []): array
111
    {
112
        $headers = [
113
            'api-key' => $this->apiKey,
114
        ];
115
116
        if (strtoupper($method) === 'GET') {
117
            $headers['public-key'] = $this->publicKey;
118
        } else {
119
            $headers['secret-key'] = $this->secretKey;
120
        }
121
122
        // Merge headers with any existing options
123
        $options['headers'] = array_merge($options['headers'] ?? [], $headers);
124
125
        try {
126
            $response = $this->httpClient->request($method, $uri, $options);
127
            $content = $response->getBody()->getContents();
128
            $data = json_decode($content, true);
129
130
            if (json_last_error() !== JSON_ERROR_NONE) {
131
                throw new VtpassException('Failed to decode JSON response: ' . json_last_error_msg());
132
            }
133
134
            return $data;
135
        } catch (GuzzleException $e) {
136
            $message = $e->getMessage();
137
            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

137
            if ($e->/** @scrutinizer ignore-call */ hasResponse()) {
Loading history...
138
                $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

138
                $responseBody = $e->/** @scrutinizer ignore-call */ getResponse()->getBody()->getContents();
Loading history...
139
                $errorData = json_decode($responseBody, true);
140
                if (isset($errorData['response_description'])) {
141
                    $message = $errorData['response_description'];
142
                } elseif (isset($errorData['message'])) {
143
                    $message = $errorData['message'];
144
                }
145
            }
146
            throw new VtpassException('API Request Failed: ' . $message, $e->getCode(), $e);
147
        }
148
    }
149
150
    /**
151
     * Get Service Categories.
152
     *
153
     * @return array
154
     * @throws VtpassException
155
     */
156
    public function getServiceCategories(): array
157
    {
158
        return $this->request('GET', 'service-categories');
159
    }
160
161
    /**
162
     * Get Service Variations.
163
     *
164
     * @param string $serviceID
165
     * @return array
166
     * @throws VtpassException
167
     */
168
    public function getServiceVariations(string $serviceID): array
169
    {
170
        return $this->request('GET', 'service-variations', [
171
            'query' => ['serviceID' => $serviceID]
172
        ]);
173
    }
174
175
    /**
176
     * Purchase a product/service.
177
     *
178
     * @param array $payload
179
     * @return array
180
     * @throws VtpassException
181
     */
182
    public function purchase(array $payload): array
183
    {
184
        // Ensure request_id is present
185
        if (!isset($payload['request_id'])) {
186
            $payload['request_id'] = $this->generateRequestId();
187
        }
188
189
        return $this->request('POST', 'pay', [
190
            'json' => $payload
191
        ]);
192
    }
193
194
    /**
195
     * Query Transaction Status.
196
     *
197
     * @param string $requestId
198
     * @return array
199
     * @throws VtpassException
200
     */
201
    public function queryTransactionStatus(string $requestId): array
202
    {
203
        return $this->request('POST', 'requery', [
204
            'json' => ['request_id' => $requestId]
205
        ]);
206
    }
207
208
    // --- Helper Methods ---
209
210
    /**
211
     * Purchase Airtime.
212
     *
213
     * @param string $serviceID (e.g., mtn, glo, airtel, etisalat)
214
     * @param float $amount
215
     * @param string $phone
216
     * @return array
217
     * @throws VtpassException
218
     */
219
    public function purchaseAirtime(string $serviceID, float $amount, string $phone): array
220
    {
221
        return $this->purchase([
222
            'serviceID' => $serviceID,
223
            'amount' => $amount,
224
            'phone' => $phone
225
        ]);
226
    }
227
228
    /**
229
     * Purchase Data.
230
     *
231
     * @param string $serviceID (e.g., mtn-data)
232
     * @param string $billersCode (Phone number)
233
     * @param string $variationCode (Plan code)
234
     * @param float|null $amount (Optional for some plans)
235
     * @param string|null $phone (The phone number to be debited, usually same as billersCode or user phone)
236
     * @return array
237
     * @throws VtpassException
238
     */
239
    public function purchaseData(string $serviceID, string $billersCode, string $variationCode, ?float $amount = null, ?string $phone = null): array
240
    {
241
        $payload = [
242
            'serviceID' => $serviceID,
243
            'billersCode' => $billersCode,
244
            'variation_code' => $variationCode,
245
            'phone' => $phone ?? $billersCode
246
        ];
247
        
248
        if ($amount !== null) {
249
            $payload['amount'] = $amount;
250
        }
251
252
        return $this->purchase($payload);
253
    }
254
}
255