Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
19.23% |
10 / 52 |
|
42.86% |
3 / 7 |
CRAP | |
0.00% |
0 / 1 |
| StripeDriver | |
19.23% |
10 / 52 |
|
42.86% |
3 / 7 |
150.89 | |
0.00% |
0 / 1 |
| key | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| label | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| configSchema | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
| charge | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
20 | |||
| verify | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
| refund | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
| isImplemented | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Services\Payment\Drivers; |
| 4 | |
| 5 | use App\Models\Order; |
| 6 | use App\Models\Payment; |
| 7 | use App\Services\Payment\PaymentResult; |
| 8 | use Illuminate\Http\Request; |
| 9 | use Stripe\Checkout\Session as CheckoutSession; |
| 10 | use Stripe\Refund as StripeRefund; |
| 11 | use 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 | */ |
| 28 | class 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 | } |