Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
19.23% covered (danger)
19.23%
10 / 52
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
StripeDriver
19.23% covered (danger)
19.23%
10 / 52
42.86% covered (danger)
42.86%
3 / 7
150.89
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%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 charge
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 verify
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 refund
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 isImplemented
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Services\Payment\Drivers;
4
5use App\Models\Order;
6use App\Models\Payment;
7use App\Services\Payment\PaymentResult;
8use Illuminate\Http\Request;
9use Stripe\Checkout\Session as CheckoutSession;
10use Stripe\Refund as StripeRefund;
11use Stripe\Stripe;
12
13/**
14 * Stripe Checkout (hosted page, redirect-based — no card element needed on
15 * our side). Uses the official stripe/stripe-php SDK, already a dependency.
16 *
17 * IMPORTANT — currency: this app tracks money in BDT only (a deliberate,
18 * store-wide "no multi-currency" constraint — see PROJECT_BRAIN.md). Stripe
19 * does not settle in BDT at all, so this driver cannot just send the
20 * order's BDT total to Stripe as-is. The operator must set a settlement
21 * currency (e.g. usd) here; the order's numeric total is sent unconverted
22 * in that currency — this app does no FX conversion. In practice this
23 * gateway only makes sense for a store whose real prices are already in
24 * that settlement currency (e.g. an international-facing shop that also
25 * happens to record BDT-denominated totals internally); flagged clearly on
26 * the settings card, not silently wrong.
27 */
28class StripeDriver extends AbstractDriver
29{
30    public function key(): string
31    {
32        return 'stripe';
33    }
34
35    public function label(): string
36    {
37        return 'Stripe (cards, international)';
38    }
39
40    public function configSchema(): array
41    {
42        return [
43            'publishable_key' => ['type' => 'text', 'label' => 'Publishable key', 'required' => true],
44            'secret_key' => ['type' => 'password', 'label' => 'Secret key', 'required' => true],
45            'currency' => [
46                'type' => 'text', 'label' => 'Settlement currency (e.g. usd)', 'required' => true,
47                'help' => 'Stripe cannot settle in BDT. This app does not convert currency — the order total is sent to Stripe as-is in whatever currency you set here.',
48            ],
49        ];
50    }
51
52    public function charge(Order $order, array $config): PaymentResult
53    {
54        if (empty($config['secret_key']) || empty($config['currency'])) {
55            return PaymentResult::failed('Stripe is not configured — add the Secret key and settlement currency in Settings → Payment Gateways.');
56        }
57
58        Stripe::setApiKey($config['secret_key']);
59
60        try {
61            $session = CheckoutSession::create([
62                'mode' => 'payment',
63                'payment_method_types' => ['card'],
64                'client_reference_id' => (string) $order->bar_code,
65                'line_items' => [[
66                    'price_data' => [
67                        'currency' => strtolower($config['currency']),
68                        'product_data' => ['name' => 'Order ' . $order->bar_code],
69                        'unit_amount' => (int) round(((float) $order->due) * 100),
70                    ],
71                    'quantity' => 1,
72                ]],
73                'success_url' => route('payment.callback', ['gateway' => 'stripe', 'result' => 'success', 'order' => $order->bar_code]) . '&session_id={CHECKOUT_SESSION_ID}',
74                'cancel_url' => route('payment.callback', ['gateway' => 'stripe', 'result' => 'cancel', 'order' => $order->bar_code]),
75            ]);
76        } catch (\Throwable $e) {
77            return PaymentResult::failed('Stripe rejected the checkout session: ' . $e->getMessage());
78        }
79
80        return PaymentResult::redirect($session->url, $session->id);
81    }
82
83    public function verify(Request $request, array $config): PaymentResult
84    {
85        $sessionId = $request->query('session_id');
86
87        if (! $sessionId) {
88            return PaymentResult::failed('Missing Stripe session id.');
89        }
90
91        Stripe::setApiKey($config['secret_key'] ?? '');
92
93        try {
94            $session = CheckoutSession::retrieve($sessionId);
95        } catch (\Throwable $e) {
96            return PaymentResult::failed('Could not verify the Stripe session: ' . $e->getMessage());
97        }
98
99        if ($session->payment_status === 'paid') {
100            return PaymentResult::paid($session->payment_intent, ['session_id' => $session->id]);
101        }
102
103        return PaymentResult::failed('Stripe session was not completed.', ['status' => $session->payment_status]);
104    }
105
106    public function refund(Payment $payment, float $amount, array $config): bool
107    {
108        if (empty($config['secret_key']) || ! $payment->transaction_id) {
109            return false;
110        }
111
112        Stripe::setApiKey($config['secret_key']);
113
114        try {
115            StripeRefund::create([
116                'payment_intent' => $payment->transaction_id,
117                'amount' => (int) round($amount * 100),
118            ]);
119
120            return true;
121        } catch (\Throwable) {
122            return false;
123        }
124    }
125
126    public function isImplemented(): bool
127    {
128        return true;
129    }
130}