Add card payment with stripe

This commit is contained in:
Djery-Tom 2023-05-30 02:37:16 +01:00
parent 51b26f9027
commit eaca44fa96
10 changed files with 2974 additions and 1698 deletions

View File

@ -38,3 +38,7 @@ CINETPAY_SITE_ID=862736
CINETPAY_API_URL=https://api-checkout.cinetpay.com/v2/
RECEIVER_NAME="Commune X"
STRIPE_KEY=pk_test_51NAILHJ6IfmAiBwqd8t8ZL9WjTdcMOSDt46TfLT1DS1VPRTrEY0UC3RDUF0b0woE95FkiUt84JVfIXgHCco4v9MO00DcQ7LkmO
STRIPE_SECRET=sk_test_51NAILHJ6IfmAiBwqgblKnBatWzIt3mtMYyw9Tc2RRFWUJJDVJ2VGKCBo3o0eTPCAigLB8lAbPiDiuvQ9Arwg0fad00fv7zIJdY
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

View File

@ -35,10 +35,23 @@ class PaymentController extends Controller
public function pay(Request $request)
{
$this->validate($request, [
'aggregator_id' => 'required|integer|exists:payment_aggregators,id',
'payment_method' => 'nullable|string',
'aggregator_id' => 'required_without:payment_method|integer|exists:payment_aggregators,id',
]);
$aggregator = PaymentAggregator::findOrFail($request->input('aggregator_id'));
$payment_method = $request->input('payment_method');
if($payment_method == 'CREDIT_CARD'){
$aggregator = PaymentAggregator::where('name','like','%stripe%')->firstOrFail();
$data = $request->all();
$request = new Request();
$request->merge(array_merge($data,[
'payment_method' => 'CREDIT_CARD',
'aggregator_id' => $aggregator->id
]));
}else{
$aggregator = PaymentAggregator::findOrFail($request->input('aggregator_id'));
}
switch (strtolower($aggregator->name)) {
case 'yoomee':
@ -47,6 +60,8 @@ class PaymentController extends Controller
return app(YoomeeV2Controller::class)->initPay($request);
case 'cinetpay':
return app(CinetpayController::class)->pay($request);
case 'stripe':
return app(StripeController::class)->pay($request);
default:
return $this->errorResponse(__('errors.unexpected_error'));
}

View File

@ -0,0 +1,192 @@
<?php
namespace App\Http\Controllers;
use App\Enums\PaymentTransactionState;
use App\Models\PaymentTransaction;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Stripe\StripeClient;
use Throwable;
class StripeController extends Controller
{
private $client;
private $timeout = 60; //In seconds
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
// Create a client with a base URI
$this->client = new StripeClient(config('variables.stripe_secret'));
}
public function getMethods()
{
$data = $this->client->paymentMethods->all()->toArray();
return $this->successResponse([
'hasWebview' => true,
'methods' => $data,
]);
}
public function getCheckout($payment_token)
{
$transaction = PaymentTransaction::where('payment_token', $payment_token)->first();
if(empty($transaction)){
return $this->errorResponse(__('errors.model_not_found', ['model' => 'transaction']), Response::HTTP_NOT_FOUND);
}
$amount = $transaction->amount;
$receiver = config('variables.receiver_name');
$receiver_logo = asset('assets/images/logo.jpeg');
return view('stripe-checkout', compact('amount','receiver','receiver_logo','payment_token'));
}
public function pay(Request $request)
{
try {
$this->validate($request, [
'aggregator_id' => 'required|integer',
'amount' => 'required|numeric|min:5',
'currency' => 'required|string|size:3',
'customer_id' => 'required|integer',
'payment_method' => 'nullable|string',
'customer_email' => 'required|email',
'customer_name' => 'required|string',
'customer_surname' => 'required|string',
'customer_phone_number' => 'required|string',
'customer_address' => 'required|string',
'customer_city' => 'required_if:payment_method,CREDIT_CARD|string',
'customer_country' => 'required|string|size:2',
'customer_state' => 'required_if:payment_method,CREDIT_CARD|string|size:2', //Etat du pays dans lequel se trouve le client. Cette valeur est obligatoire si le client se trouve au États Unis dAmérique (US) ou au Canada (CA)
'customer_zip_code' => 'required_if:payment_method,CREDIT_CARD|string|size:5',
'reason' => 'required|string'
]);
$transaction_id = $this->getTransactionID();
$payment_method = $request->input('payment_method','CREDIT_CARD');
$amount = $request->input('amount');
$currency = $request->input('currency');
if ($currency != 'USD') {
// Convertir en multiple de 5
$amount = $this->roundUpToAny($amount);
}
if($amount < 325 && $currency == 'XAF'){
$amount = 325;
}
$payment_token = $this->getTransactionToken();
$payment_url = route('stripe.checkout',['payment_token' =>$payment_token]);
PaymentTransaction::create([
'aggregator_id' => $request->input('aggregator_id'),
"currency" => $request->input('currency'),
"transaction_id" => $transaction_id,
"amount" => $amount,
"payment_method" => $payment_method,
"payment_token" => $payment_token,
"payment_url" => $payment_url,
'state' => PaymentTransactionState::INITIATED,
"reason" => $request->input('reason'),
"customer_id" => $request->input('customer_id'),
"customer_name" => $request->input('customer_name'),
"customer_surname" => $request->input('customer_surname'),
"customer_email" => $request->input('customer_email'),
"customer_phone_number" => $request->input('customer_phone_number'),
"customer_address" => $request->input('customer_address'),
"customer_city" => $request->input('customer_city'),
"customer_country" => $request->input('customer_country'),
"customer_state" => $request->input('customer_state'),
"customer_zip_code" => $request->input('customer_zip_code'),
]);
return $this->successResponse([
'message' => 'Payment initiated',
'payment_url' => $payment_url
]);
}catch (Throwable $e){
Log::error($e->getMessage());
}
return $this->errorResponse(trans('errors.unexpected_error'));
}
public function post(Request $request)
{
$this->validate($request, [
'payment_token' => 'required|string|exists:payment_transactions,payment_token',
'stripeToken' => 'required|string',
]);
$token = $request->input('payment_token');
$transaction = PaymentTransaction::where('payment_token', $token)->where('state', PaymentTransactionState::INITIATED)->firstOrFail();
// Init payment
$charge = $this->client->charges->create([
"amount" => round($transaction->amount,2),
"currency" => $transaction->currency,
"description" => $transaction->reason,
// "customer" => 15,
"source" => $request->input('stripeToken'),
"receipt_email" => $transaction->customer_email,
"metadata" => [
"transaction_id" => $transaction->transaction_id,
'lang' => app()->getLocale(),
"customer_id" => $transaction->customer_id,
"customer_name" => $transaction->customer_name,
"customer_surname" => $transaction->customer_surname,
"customer_email" => $transaction->customer_email,
"customer_phone_number" => $transaction->customer_phone_number,
"customer_address" => $transaction->customer_address,
"customer_city" => $transaction->customer_city,
"customer_country" => $transaction->customer_country,
"customer_state" => $transaction->customer_state,
"customer_zip_code" => $transaction->customer_zip_code,
],
]);
if($charge){
$transaction->update([
'aggregator_payment_ref' => $charge->id,
'payment_date' => date('Y-m-d H:i:s', $charge->created) ,
'state' => PaymentTransactionState::ACCEPTED,
'payment_method_exact' => $request->input('card_number')
]);
return redirect()->route('checkout',['payment_token' => $token]);
}
session()->flash('error',__('errors.unexpected_error'));
return redirect()->to(route('stripe.checkout',['payment_token' => $token]));
}
private function getTransactionToken()
{
do {
$code = 'STP-'.Str::random(64);
$result = collect(DB::select('SELECT * FROM payment_transactions WHERE payment_token = :code', ['code' => $code]));
$codeCorrect = sizeof($result) < 0;
} while ($codeCorrect);
return $code;
}
}

View File

@ -191,8 +191,7 @@ class YoomeeV2Controller extends Controller
"customer_phone_number" => $customer_phone_number,
]);
redirect()->route('checkout',['payment_token' => $token]);
return redirect()->route('checkout',['payment_token' => $token]);
}
session()->flash('error',__('errors.unexpected_error'));

View File

@ -5,14 +5,15 @@
"license": "MIT",
"type": "project",
"require": {
"php": "^7.3|^8.0",
"php": "^8.0",
"ext-json": "*",
"cknow/laravel-money": "^7.0",
"darkaonline/swagger-lume": "^9.0",
"flipbox/lumen-generator": "^9.1",
"guzzlehttp/guzzle": "^7.4",
"illuminate/session": "^8.83",
"laravel/lumen-framework": "^8.0"
"illuminate/session": "^9.52",
"laravel/lumen-framework": "^9.0",
"stripe/stripe-php": "^10.13"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",

3988
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,4 +16,7 @@ return [
'cinetpay_secret_key' => env('CINETPAY_SECRET_KEY', ''),
'cinetpay_site_id' => env('CINETPAY_SITE_ID', ''),
'cinetpay_api_url' => env('CINETPAY_API_URL', ''),
'stripe_key' => env('STRIPE_KEY', ''),
'stripe_secret' => env('STRIPE_SECRET', ''),
'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', ''),
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1,437 @@
<!DOCTYPE html>
<html lang="fr" data-kantu="1">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Effectuer un paiement Stripe</title>
<meta name="theme-color" content="#000000">
<meta name="csrf-token" content="">
<link rel="icon" href="{{asset('assets/images/favicon.ico')}}">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="{{asset('assets/app.css')}}">
<style>
.option-content1::after {
left: 42%;
}
</style>
<style type="text/css">
#page {
display: none;
}
#loading {
display: block;
position: absolute;
z-index: 100;
background-position: center;
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
}
@keyframes ldio-5owbnf6l9j7 {
0% {
transform: translate(12px, 80px) scale(0);
}
25% {
transform: translate(12px, 80px) scale(0);
}
50% {
transform: translate(12px, 80px) scale(1);
}
75% {
transform: translate(80px, 80px) scale(1);
}
100% {
transform: translate(148px, 80px) scale(1);
}
}
@keyframes ldio-5owbnf6l9j7-r {
0% {
transform: translate(148px, 80px) scale(1):
}
100% {
transform: translate(148px, 80px) scale(0);
}
}
@keyframes ldio-5owbnf6l9j7-c {
0% {
background: var(--loader-color1)
}
25% {
background: var(--loader-color2)
}
50% {
background: var(--loader-color1)
}
75% {
background: var(--loader-color2)
}
100% {
background: var(--loader-color1)
}
}
.ldio-5owbnf6l9j7 div {
position: absolute;
width: 40px;
height: 40px;
border-radius: 50%;
transform: translate(80px, 80px) scale(1);
background: var(--loader-color1);
animation: ldio-5owbnf6l9j7 1.2048192771084336s infinite cubic-bezier(0, 0.5, 0.5, 1);
}
.ldio-5owbnf6l9j7 div:nth-child(1) {
background: var(--loader-color2);
transform: translate(148px, 80px) scale(1);
animation: ldio-5owbnf6l9j7-r 0.3012048192771084s infinite cubic-bezier(0, 0.5, 0.5, 1), ldio-5owbnf6l9j7-c 1.2048192771084336s infinite step-start;
}
.ldio-5owbnf6l9j7 div:nth-child(2) {
animation-delay: -0.3012048192771084s;
background: var(--loader-color1);
}
.ldio-5owbnf6l9j7 div:nth-child(3) {
animation-delay: -0.6024096385542168s;
background: var(--loader-color2);
}
.ldio-5owbnf6l9j7 div:nth-child(4) {
animation-delay: -0.9036144578313252s;
background: var(--loader-color1);
}
.ldio-5owbnf6l9j7 div:nth-child(5) {
animation-delay: -1.2048192771084336s;
background: var(--loader-color2);
}
.loadingio-spinner-ellipsis-99po1h19hjs {
width: 200px;
height: 200px;
display: inline-block;
overflow: hidden;
background: none;
}
.ldio-5owbnf6l9j7 {
width: 100%;
height: 100%;
position: relative;
transform: translateZ(0) scale(1);
backface-visibility: hidden;
transform-origin: 0 0; /* see note above */
}
.ldio-5owbnf6l9j7 div {
box-sizing: content-box;
}
.toggle-content {
display: none;
height: 0;
opacity: 0;
overflow: hidden;
transition: height 350ms ease-in-out, opacity 750ms ease-in-out;
}
.toggle-content.is-visible {
display: block;
height: auto;
opacity: 1;
}
.hide{
display: none;
}
</style>
</head>
<body id="body">
<div id="loading">
<div class="loadingio-spinner-ellipsis-99po1h19hjs">
<div class="ldio-5owbnf6l9j7">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
<div id="page" class="container">
<div class="row h-100 justify-content-center">
<div class="col-md-8 col-lg-6 my-auto">
<div class="desk bg-white shadow-sm">
<div class="desk-head">
<div class="row">
<div class="col-12">
<div class="">
<div class="media align-items-center">
<div class="media-head">
<img src="{{$receiver_logo}}" class="rounded media-body mr-2 marchand-logo" alt="{{$receiver}}">
</div>
<div class="media-body">
<p class="marchand-name p-0 m-0">{{$receiver}}</p>
<h4 class="due-amount p-0 m-0">{{$amount}}</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<hr class="ml-2 mr-2">
<div class="desk-body">
<form
role="form"
action="{{ route('stripe.post') }}"
method="post"
class="require-validation"
data-cc-on-file="false"
data-stripe-publishable-key="{{ config('variables.stripe_key') }}"
id="payment-form">
{{-- @csrf--}}
<input type="hidden" name="_token" value="{{ app('request')->session()->get('_token') }}">
<input type="hidden" name="payment_token" value="{{ $payment_token }}">
<div class="desk-content">
<p class="text-center">Payer avec</p>
<div class="choose-payment-type">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<svg xmlns="http://www.w3.org/2000/svg" width="60px" height="60px" viewBox="0 0 100 100">
<g transform="translate(-2978 2535)">
<rect width="100" height="100" transform="translate(2978 -2535)" fill="var()" opacity="0.003"></rect>
<g transform="translate(2403.753 -3004.222)">
<path d="M65.889,1.928H12.373A5.964,5.964,0,0,0,6.427,7.874V91.122a5.964,5.964,0,0,0,5.946,5.946H65.889a5.964,5.964,0,0,0,5.946-5.946V7.874A5.964,5.964,0,0,0,65.889,1.928ZM39.131,92.774a4.625,4.625,0,1,1,4.625-4.625A4.625,4.625,0,0,1,39.131,92.774ZM65.889,79.23H12.373V13.821H65.889Z" transform="translate(584.82 469.294)" fill="var(--cinetpay1)"></path>
<path d="M21.585,0V3.6H0V7.2H21.585v3.6l7.2-5.4ZM7.2,14.39,0,19.787l7.2,5.4v-3.6H28.78v-3.6H7.2Z" transform="translate(609.43 500.731)" fill=" var(--cinetpay1)"></path>
</g>
</g>
</svg>
<p class="descrip" style="font-size:medium; margin-top: 5px">Stripe</p>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade active show" id="mobile-money" role="tabpanel" aria-labelledby="mobile-money-tab">
<div class="option-content1">
<div class="p-1">
<div class="desk-form">
<div class="row">
<div class="col-md-12 col-md-offset-3">
<div class="panel panel-default credit-card-box">
<div class="panel-body">
@if (app('request')->session()->has('success'))
<div class="alert alert-success text-center">
<a href="#" class="close" data-dismiss="alert" aria-label="close">×</a>
<p>{{ app('request')->session()->get('success') }}</p>
</div>
@endif
<div class='form-row row'>
<div class='col-12 form-group required'>
<label class='control-label'>Name on Card</label> <input
class='form-control' type='text'>
</div>
</div>
<div class='form-row row'>
<div class='col-12 form-group required'>
<label class='control-label'>Card Number</label>
<input autocomplete='off' class='form-control card-number' name="card_number" maxlength='20' type='text'>
</div>
</div>
<div class='form-row row'>
<div class='col-12 col-md-5 form-group expiration required'>
<label class='control-label'>Expiration Month</label> <input
class='form-control card-expiry-month' placeholder='MM' maxlength='2'
type='text'>
</div>
<div class='col-12 col-md-5 form-group expiration required'>
<label class='control-label'>Expiration Year</label> <input
class='form-control card-expiry-year' placeholder='YYYY' maxlength='4'
type='text'>
</div>
<div class='col-12 col-md-2 form-group cvc required'>
<label class='control-label'>CVC</label> <input autocomplete='off'
class='form-control card-cvc' placeholder='ex. 311' maxlength='4'
type='text'>
</div>
</div>
<div class='form-row row'>
<div class='col-md-12 error form-group hide'>
<div class='alert-danger alert'>Please correct the errors and try
again.</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<div class="col-12 px-0">
<div>
<p id="methodscm" class="payment-method-box operator-box">
<img
src="{{url('assets/images/stripe.png')}}"
class="rounded mx-1 payment-method-logo"
alt="ORANGE">
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="desk-action p-3" id="desk-action">
<button type="submit" class="btn btn-next btn-block" id="del">
Payer {{$amount}}
</button>
</div>
</form>
</div>
</div>
@include('footer')
</div>
</div>
</div>
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script src="{{asset('assets/sweetalert2/sweetalert2.all.min.js')}}"></script>
<script src="{{asset('assets/app.js')}}"></script>
<script>
document.addEventListener('readystatechange', function (event) {
if (event.target.readyState === 'complete') {
ready();
}
});
function ready() {
setTimeout(function () {
fadeIn(document.getElementById('page'))
}, 217)
fadeOut(document.getElementById('loading'));
}
// ** FADE OUT FUNCTION **
function fadeOut(el) {
el.style.opacity = 1;
(function fade() {
if ((el.style.opacity -= .1) < 0) {
el.style.display = "none";
} else {
requestAnimationFrame(fade);
}
})();
}
// ** FADE IN FUNCTION **
function fadeIn(el, display) {
el.style.opacity = 0;
el.style.display = display || "block";
(function fade() {
var val = parseFloat(el.style.opacity);
if (!((val += .1) > 1)) {
el.style.opacity = val;
requestAnimationFrame(fade);
}
})();
}
function empty(item) {
return (item === undefined || item === null || item === false || item === '');
}
</script>
<script type="text/javascript">
$(function() {
/*------------------------------------------
--------------------------------------------
Stripe Payment Code
--------------------------------------------
--------------------------------------------*/
var $form = $(".require-validation");
$('form.require-validation').bind('submit', function(e) {
var $form = $(".require-validation"),
inputSelector = ['input[type=email]', 'input[type=password]',
'input[type=text]', 'input[type=file]',
'textarea'].join(', '),
$inputs = $form.find('.required').find(inputSelector),
$errorMessage = $form.find('div.error'),
valid = true;
$errorMessage.addClass('hide');
$('.has-error').removeClass('has-error');
$inputs.each(function(i, el) {
var $input = $(el);
if ($input.val() === '') {
$input.parent().addClass('has-error');
$errorMessage.removeClass('hide');
e.preventDefault();
}
});
if (!$form.data('cc-on-file')) {
e.preventDefault();
Stripe.setPublishableKey($form.data('stripe-publishable-key'));
Stripe.createToken({
number: $('.card-number').val(),
cvc: $('.card-cvc').val(),
exp_month: $('.card-expiry-month').val(),
exp_year: $('.card-expiry-year').val()
}, stripeResponseHandler);
}
});
/*------------------------------------------
--------------------------------------------
Stripe Response Handler
--------------------------------------------
--------------------------------------------*/
function stripeResponseHandler(status, response) {
if (response.error) {
$('.error')
.removeClass('hide')
.find('.alert')
.text(response.error.message);
} else {
/* token contains id, last4, and card type */
var token = response['id'];
$form.find('input[type=text]').empty();
$form.append("<input type='hidden' name='stripeToken' value='" + token + "'/>");
$form.get(0).submit();
}
}
});
</script>
</body>
</html>

View File

@ -16,6 +16,7 @@
* Session endpoints
*/
$router->group(['middleware' => 'session'], function () use ($router) {
// $router->get('/', function (){
// return 'Payment Service';
// });
@ -23,6 +24,14 @@ $router->group(['middleware' => 'session'], function () use ($router) {
$router->post('checkoutPay', ['as' => 'yoomee.v2.checkoutPay', 'uses' => 'YoomeeV2Controller@checkoutPay','middleware' => 'csrf']);
$router->post('status', ['as' => 'yoomee.v2.verify', 'uses' => 'YoomeeV2Controller@getPaymentStatus']);
$router->get('merchantRedirect', ['as' => 'yoomee.v2.merchantRedirect', 'uses' => 'YoomeeV2Controller@merchantRedirect']);
/**
* Stripe Endpoints
*/
$router->group(['prefix' => 'stripe'], function () use ($router) {
$router->get('checkout/{payment_token}',['as' => 'stripe.checkout', 'uses' => 'StripeController@getCheckout']);
$router->post('post',['as' => 'stripe.post', 'uses' => 'StripeController@post']);
});
});
/**
@ -30,6 +39,7 @@ $router->group(['middleware' => 'session'], function () use ($router) {
*/
$router->addRoute(['GET','POST'],'/yoomee/v2/webhook', ['as' => 'yoomee.v2.webhook' , 'uses' => 'YoomeeV2Controller@capturePaymentResult']);
$router->addRoute(['GET','POST'],'/cinetpay/webhook', ['as' => 'cinetpay.webhook' , 'uses' => 'CinetpayController@capturePaymentResult']);
$router->addRoute(['GET','POST'],'/stripe/webhook', ['as' => 'cinetpay.webhook' , 'uses' => 'CinetpayController@capturePaymentResult']);
$router->addRoute(['GET','POST'],'/paymentResult', ['as' => 'paymentResult' , 'uses' => 'PaymentController@paymentResult']);
@ -56,10 +66,19 @@ $router->group(['middleware' => 'auth'], function () use ($router) {
});
/**
* Cinetpay Endpoints
*/
$router->group(['prefix' => 'cinetpay'], function () use ($router) {
$router->get('methods',['as' => 'cinetpay.methods', 'uses' => 'CinetpayController@getMethods']);
$router->addRoute(['GET','POST'],'pay',['as' => 'cinetpay.pay', 'uses' => 'CinetpayController@pay']);
});
/**
* Stripe Endpoints
*/
$router->group(['prefix' => 'stripe'], function () use ($router) {
$router->get('methods',['as' => 'stripe.methods', 'uses' => 'StripeController@getMethods']);
$router->addRoute(['POST'],'pay',['as' => 'stripe.pay', 'uses' => 'StripeController@pay']);
});
});