Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.97% covered (danger)
18.97%
11 / 58
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
BkashDriver
18.97% covered (danger)
18.97%
11 / 58
33.33% covered (danger)
33.33%
3 / 9
279.55
0.00% covered (danger)
0.00%
0 / 1
 key
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 label
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configSchema
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 charge
10.53% covered (danger)
10.53%
2 / 19
0.00% covered (danger)
0.00%
0 / 1
53.84
 verify
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 isImplemented
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 grantToken
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 headers
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 endpoint
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace App\Services\Payment\Drivers;
4
5use App\Models\Order;
6use App\Services\Payment\PaymentResult;
7use Illuminate\Http\Request;
8use Illuminate\Support\Facades\Http;
9
10/**
11 * bKash Tokenized Checkout (merchant API). Implemented per bKash's publicly
12 * documented v1.2.0-beta flow (grant token -> create payment -> customer
13 * redirected to bkashURL -> execute payment on return). bKash has revised
14 * this API's version path before — confirm the base URL against the
15 * operator's current merchant onboarding docs and run a real sandbox
16 * transaction before enabling this live; this has not been tested against
17 * a live bKash sandbox.
18 */
19class BkashDriver extends AbstractDriver
20{
21    public function key(): string
22    {
23        return 'bkash';
24    }
25
26    public function label(): string
27    {
28        return 'bKash';
29    }
30
31    public function configSchema(): array
32    {
33        return [
34            'app_key' => ['type' => 'text', 'label' => 'App Key', 'required' => true],
35            'app_secret' => ['type' => 'password', 'label' => 'App Secret', 'required' => true],
36            'username' => ['type' => 'text', 'label' => 'Username', 'required' => true],
37            'password' => ['type' => 'password', 'label' => 'Password', 'required' => true],
38            'sandbox' => ['type' => 'toggle', 'label' => 'Sandbox mode', 'default' => true],
39        ];
40    }
41
42    public function charge(Order $order, array $config): PaymentResult
43    {
44        if (empty($config['app_key']) || empty($config['app_secret']) || empty($config['username']) || empty($config['password'])) {
45            return PaymentResult::failed('bKash is not configured — fill in the App Key/Secret and Username/Password in Settings → Payment Gateways.');
46        }
47
48        $token = $this->grantToken($config);
49
50        if (! $token) {
51            return PaymentResult::failed('Could not authenticate with bKash — check the credentials.');
52        }
53
54        $response = Http::withHeaders($this->headers($config, $token))
55            ->post($this->endpoint($config, 'tokenized/checkout/create'), [
56                'mode' => '0011',
57                'payerReference' => (string) $order->customer_id,
58                'callbackURL' => route('payment.callback', ['gateway' => 'bkash', 'result' => 'return', 'order' => $order->bar_code]),
59                'amount' => number_format((float) $order->due, 2, '.', ''),
60                'currency' => 'BDT',
61                'intent' => 'sale',
62                'merchantInvoiceNumber' => (string) $order->bar_code,
63            ]);
64
65        $data = $response->json() ?? [];
66
67        if (empty($data['bkashURL']) || empty($data['paymentID'])) {
68            return PaymentResult::failed($data['statusMessage'] ?? 'Could not create the bKash payment.', $data);
69        }
70
71        return PaymentResult::redirect($data['bkashURL'], $data['paymentID']);
72    }
73
74    public function verify(Request $request, array $config): PaymentResult
75    {
76        $paymentId = $request->query('paymentID');
77        $status = $request->query('status');
78
79        if ($status !== 'success' || ! $paymentId) {
80            return PaymentResult::failed('bKash payment was not completed.', ['status' => $status]);
81        }
82
83        $token = $this->grantToken($config);
84
85        if (! $token) {
86            return PaymentResult::failed('Could not authenticate with bKash to execute the payment.');
87        }
88
89        $response = Http::withHeaders($this->headers($config, $token))
90            ->post($this->endpoint($config, 'tokenized/checkout/execute'), ['paymentID' => $paymentId]);
91
92        $data = $response->json() ?? [];
93
94        if (($data['transactionStatus'] ?? null) === 'Completed') {
95            return PaymentResult::paid($data['trxID'] ?? $paymentId, $data);
96        }
97
98        return PaymentResult::failed($data['statusMessage'] ?? 'bKash could not complete this payment.', $data);
99    }
100
101    public function isImplemented(): bool
102    {
103        return true;
104    }
105
106    private function grantToken(array $config): ?string
107    {
108        $response = Http::withHeaders([
109            'username' => $config['username'] ?? '',
110            'password' => $config['password'] ?? '',
111        ])->post($this->endpoint($config, 'tokenized/checkout/token/grant'), [
112            'app_key' => $config['app_key'] ?? '',
113            'app_secret' => $config['app_secret'] ?? '',
114        ]);
115
116        return $response->successful() ? $response->json('id_token') : null;
117    }
118
119    private function headers(array $config, string $token): array
120    {
121        return [
122            'Authorization' => $token,
123            'X-App-Key' => $config['app_key'] ?? '',
124        ];
125    }
126
127    private function endpoint(array $config, string $path): string
128    {
129        $host = (bool) ($config['sandbox'] ?? true)
130            ? 'https://tokenized.sandbox.bka.sh/v1.2.0-beta/'
131            : 'https://tokenized.pay.bka.sh/v1.2.0-beta/';
132
133        return $host . $path;
134    }
135}