From 329edb354d3bdf0d60845542bfb85a90375257de Mon Sep 17 00:00:00 2001 From: Djery-Tom Date: Wed, 7 Jun 2023 20:56:04 +0100 Subject: [PATCH] Implement Stripe PayIn , Payout and Refund --- .env.example | 1 + ...State.php => PaymentTransactionStatus.php} | 2 +- app/Http/Controllers/CinetpayController.php | 12 +- app/Http/Controllers/PaymentController.php | 30 +- app/Http/Controllers/StripeController.php | 377 +++++++++++++++++- app/Http/Controllers/YoomeeController.php | 8 +- app/Http/Controllers/YoomeeV2Controller.php | 38 +- app/Models/PaymentRefund.php | 11 + app/Traits/Helper.php | 4 +- composer.json | 1 + composer.lock | 299 +++++++++++++- config/variables.php | 1 + ...05_111933_create_payment_refunds_table.php | 40 ++ ...tate_to_status_in_payment_transactions.php | 32 ++ ...r_phone_number_in_payment_transactions.php | 36 ++ routes/web.php | 8 +- 16 files changed, 848 insertions(+), 52 deletions(-) rename app/Enums/{PaymentTransactionState.php => PaymentTransactionStatus.php} (85%) create mode 100644 app/Models/PaymentRefund.php create mode 100644 database/migrations/2023_06_05_111933_create_payment_refunds_table.php create mode 100644 database/migrations/2023_06_07_070215_rename_state_to_status_in_payment_transactions.php create mode 100644 database/migrations/2023_06_07_102900_change_customer_id_and_customer_phone_number_in_payment_transactions.php diff --git a/.env.example b/.env.example index 5ae9d86..6301951 100644 --- a/.env.example +++ b/.env.example @@ -42,3 +42,4 @@ RECEIVER_NAME="Commune X" STRIPE_KEY=pk_test_51NAILHJ6IfmAiBwqd8t8ZL9WjTdcMOSDt46TfLT1DS1VPRTrEY0UC3RDUF0b0woE95FkiUt84JVfIXgHCco4v9MO00DcQ7LkmO STRIPE_SECRET=sk_test_51NAILHJ6IfmAiBwqgblKnBatWzIt3mtMYyw9Tc2RRFWUJJDVJ2VGKCBo3o0eTPCAigLB8lAbPiDiuvQ9Arwg0fad00fv7zIJdY STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret +STRIPE_ACCOUNT=acct_1NAILHJ6IfmAiBwq diff --git a/app/Enums/PaymentTransactionState.php b/app/Enums/PaymentTransactionStatus.php similarity index 85% rename from app/Enums/PaymentTransactionState.php rename to app/Enums/PaymentTransactionStatus.php index 8e605ba..0225288 100644 --- a/app/Enums/PaymentTransactionState.php +++ b/app/Enums/PaymentTransactionStatus.php @@ -4,7 +4,7 @@ namespace App\Enums; -abstract class PaymentTransactionState +abstract class PaymentTransactionStatus { const INITIATED = 'INITIATED'; const ACCEPTED = 'ACCEPTED'; diff --git a/app/Http/Controllers/CinetpayController.php b/app/Http/Controllers/CinetpayController.php index 84d366b..6c42cc8 100644 --- a/app/Http/Controllers/CinetpayController.php +++ b/app/Http/Controllers/CinetpayController.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers; -use App\Enums\PaymentTransactionState; +use App\Enums\PaymentTransactionStatus; use App\Models\PaymentTransaction; use GuzzleHttp\Client; use Illuminate\Http\Request; @@ -78,7 +78,7 @@ class CinetpayController extends Controller 'payment_method' => 'nullable|string|in:ALL,MOBILE_MONEY,CREDIT_CARD,WALLET', 'customer_id' => 'required|integer', 'customer_email' => 'required|email', - 'customer_name' => 'required|string', + 'customer_name' => 'nullable|string', 'customer_surname' => 'required|string', 'customer_phone_number' => 'required|string', 'customer_address' => 'required|string', @@ -140,7 +140,7 @@ class CinetpayController extends Controller "payment_method" => $payment_method, "payment_token" => $responseData->data->payment_token, "payment_url" => $responseData->data->payment_url, - 'state' => PaymentTransactionState::PENDING, + 'status' => PaymentTransactionStatus::PENDING, "reason" => $request->input('reason'), "customer_id" => $request->input('customer_id'), "customer_name" => $request->input('customer_name'), @@ -213,7 +213,7 @@ class CinetpayController extends Controller if ($responseCode == 200) { $transaction->update([ - 'state' => $responseData->data->status, + 'status' => $responseData->data->status, 'payment_method_exact' => $responseData->data->payment_method ?? null, 'aggregator_payment_ref' => $responseData->data->operator_id ?? null, 'payment_date' => $responseData->data->payment_date ?? null, @@ -225,12 +225,12 @@ class CinetpayController extends Controller Log::info("Get Payment Status Error"); Log::info($e->getMessage()); $transaction->update([ - 'state' => PaymentTransactionState::REFUSED + 'status' => PaymentTransactionStatus::REFUSED ]); } - if($transaction->state == PaymentTransactionState::ACCEPTED){ + if($transaction->status == PaymentTransactionStatus::ACCEPTED){ return redirect()->route('paymentResult',[ 'transaction_id' => $transaction->transaction_id, 'token' => $transaction->payment_token, diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 28ba232..260d973 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers; -use App\Enums\PaymentTransactionState; +use App\Enums\PaymentTransactionStatus; use App\Models\PaymentAggregator; use App\Models\PaymentTransaction; use GuzzleHttp\Client; @@ -87,17 +87,39 @@ class PaymentController extends Controller $receiver_logo = asset('assets/images/logo.jpeg'); - if($transaction->state == PaymentTransactionState::INITIATED){ + if($transaction->status == PaymentTransactionStatus::INITIATED){ return view('checkout',compact('payment_token','method','amount', 'receiver','receiver_logo')); } - if($transaction->state == PaymentTransactionState::PENDING){ + if($transaction->status == PaymentTransactionStatus::PENDING){ return view('verify-payment',compact('transaction_id','method','amount', 'receiver','receiver_logo')); } - $status = $transaction->state == PaymentTransactionState::ACCEPTED; + $status = $transaction->status == PaymentTransactionStatus::ACCEPTED; return view('payment-status',compact('transaction_id','method','amount', 'receiver','receiver_logo','status')); } + + + public function checkStatus(Request $request, $transaction_id) + { + $transaction = PaymentTransaction::where('transaction_id',$transaction_id)->firstOrFail(); + + if($transaction->status == PaymentTransactionStatus::ACCEPTED){ + return $this->successResponse([ + 'message' => 'Payment Accepted', + 'transaction_id' => $transaction->transaction_id, + 'status' => $transaction->status + ]); + + }else{ + + return $this->errorResponse([ + 'message' => 'Payment '.$transaction->status, + 'status' => $transaction->status + ]); + } + + } } diff --git a/app/Http/Controllers/StripeController.php b/app/Http/Controllers/StripeController.php index 51138ea..6738b94 100644 --- a/app/Http/Controllers/StripeController.php +++ b/app/Http/Controllers/StripeController.php @@ -2,14 +2,16 @@ namespace App\Http\Controllers; -use App\Enums\PaymentTransactionState; +use App\Enums\PaymentTransactionStatus; +use App\Models\PaymentAggregator; +use App\Models\PaymentRefund; 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\PaymentIntent; use Stripe\StripeClient; use Throwable; @@ -62,22 +64,22 @@ class StripeController extends Controller 'aggregator_id' => 'required|integer', 'amount' => 'required|numeric|min:5', 'currency' => 'required|string|size:3', - 'customer_id' => 'required|integer', + 'customer_id' => 'nullable', 'payment_method' => 'nullable|string', 'customer_email' => 'required|email', - 'customer_name' => 'required|string', + 'customer_name' => 'nullable|string', 'customer_surname' => 'required|string', - 'customer_phone_number' => 'required|string', + 'customer_phone_number' => 'nullable|string', 'customer_address' => 'required|string', - 'customer_city' => 'required_if:payment_method,CREDIT_CARD|string', + 'customer_city' => 'required_if:payment_method,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 d’Amérique (US) ou au Canada (CA) - 'customer_zip_code' => 'required_if:payment_method,CREDIT_CARD|string|size:5', + 'customer_state' => 'required_if:payment_method,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 d’Amérique (US) ou au Canada (CA) + 'customer_zip_code' => 'required_if:payment_method,CARD|string|size:5', 'reason' => 'required|string' ]); $transaction_id = $this->getTransactionID(); - $payment_method = $request->input('payment_method','CREDIT_CARD'); + $payment_method = $request->input('payment_method','CARD'); $amount = $request->input('amount'); $currency = $request->input('currency'); @@ -100,7 +102,7 @@ class StripeController extends Controller "payment_method" => $payment_method, "payment_token" => $payment_token, "payment_url" => $payment_url, - 'state' => PaymentTransactionState::INITIATED, + 'status' => PaymentTransactionStatus::INITIATED, "reason" => $request->input('reason'), "customer_id" => $request->input('customer_id'), "customer_name" => $request->input('customer_name'), @@ -136,7 +138,7 @@ class StripeController extends Controller ]); $token = $request->input('payment_token'); - $transaction = PaymentTransaction::where('payment_token', $token)->where('state', PaymentTransactionState::INITIATED)->firstOrFail(); + $transaction = PaymentTransaction::where('payment_token', $token)->where('status', PaymentTransactionStatus::INITIATED)->firstOrFail(); // Init payment @@ -168,8 +170,8 @@ class StripeController extends Controller $transaction->update([ 'aggregator_payment_ref' => $charge->id, - 'payment_date' => date('Y-m-d H:i:s', $charge->created) , - 'state' => PaymentTransactionState::ACCEPTED, + 'payment_date' => date('Y-m-d H:i:s', $charge->created), + 'status' => PaymentTransactionStatus::ACCEPTED, 'payment_method_exact' => $request->input('card_number') ]); @@ -189,4 +191,353 @@ class StripeController extends Controller } while ($codeCorrect); return $code; } + + + public function refund(Request $request) + { + $this->validate($request, [ + 'transaction_id' => 'required|string|exists:payment_transactions,transaction_id', + 'reason' => 'nullable|string|in:duplicate,fraudulent,requested_by_customer' + ]); + + $transaction = PaymentTransaction::where('transaction_id', $request->input('transaction_id'))->where('status', PaymentTransactionStatus::ACCEPTED)->firstOrFail(); + $reason = $request->input('reason','requested_by_customer'); + + try{ + $reference = $transaction->aggregator_payment_ref; + $twoFirstChars = substr($reference,0,2); + $keyToRefund = $twoFirstChars == 'ch' ? 'charge' : 'payment_intent'; + $refund = $this->client->refunds->create([ + $keyToRefund => $reference, + 'reason' => $reason + ]); + + if(!empty($refund)){ + PaymentRefund::create([ + 'refund_id' => $this->getTransactionID('payment_refunds'), + 'transaction_id' => $transaction->id, + "currency" => strtoupper($refund->currency), + "amount" => $refund->amount, + "reason" => $reason, + 'date' => date('Y-m-d H:i:s', $refund->created), + 'status' => strtoupper($refund->status) + ]); + + return $this->successResponse("Payment refunded successfully"); + } + + }catch (Throwable $e){ + Log::error("Error Stripe refund"); + $errorMessage = $e->getMessage(); + Log::error($errorMessage); + } + + return $this->errorResponse($errorMessage ?? __('errors.unexpected_error')); + } + + + + public function payIn(Request $request) + { + $this->validate($request, [ + 'card_no' => 'required', + 'exp_month' => 'required', + 'exp_year' => 'required', + 'cvc' => 'required', +// 'aggregator_id' => 'required|integer', + 'amount' => 'required|numeric|min:5', + 'currency' => 'required|string|size:3', + 'customer_id' => 'nullable', +// 'payment_method' => 'nullable|string', // Actuallu it's only CARD + 'customer_email' => 'required|email', + 'customer_name' => 'nullable|string', + 'customer_surname' => 'required|string', + 'customer_phone_number' => 'nullable|string', + 'customer_address' => 'required|string', + 'customer_city' => 'required|string', + 'customer_country' => 'required|string|size:2', + 'customer_state' => 'required_if:customer_country,US|string|size:2', //Etat du pays dans lequel se trouve le client. Cette valeur est obligatoire si le client se trouve au États Unis d’Amérique (US) ou au Canada (CA) + 'customer_zip_code' => 'required_if:customer_country,US|string|size:5', + 'reason' => 'required|string' + ]); + + + $aggregator = PaymentAggregator::where('name','like','%stripe%')->firstOrFail(); + + try{ + + $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; + } + + + $transaction = PaymentTransaction::create([ + 'aggregator_id' => $aggregator->id, + "currency" => $request->input('currency'), + "transaction_id" => $this->getTransactionID(), + "amount" => $amount, + "payment_method" => 'CARD', + 'status' => PaymentTransactionStatus::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'), + ]); + + // Create payment method in Stripe + // https://stripe.com/docs/api/payment_methods/create + $method = $this->client->paymentMethods->create([ + 'type' => 'card', + 'card' => [ + 'number' => $request->input('card_no'), + 'exp_month' => $request->input('exp_month'), + 'exp_year' => $request->input('exp_year'), + 'cvc' => $request->input('cvc'), + ] + ]); + + if($method){ + + $transaction->update([ + 'payment_method_exact' => $method->id + ]); + + + // Create payment intent + // https://stripe.com/docs/api/payment_intents/create + $paymentIntent = $this->client->paymentIntents->create([ + "amount" => $amount, + "currency" => $request->input('currency'), + "description" => $request->input('reason'), + 'payment_method_types' => ['card'], + 'payment_method' => $method->id, + 'confirm' => 'true', + 'capture_method' => 'automatic', + 'return_url' => route('stripe.webhook',['transaction_id' => $transaction->transaction_id]), // Redirect url if 3d secure required + 'payment_method_options' => [ + 'card' => [ + 'request_three_d_secure' => 'automatic' + ] + ], + ]); + + + if($paymentIntent){ + + $transaction->update([ + 'payment_date' => date('Y-m-d H:i:s', $paymentIntent->created), + 'aggregator_payment_ref' => $paymentIntent->id + ]); + + if(!empty($paymentIntent->next_action->redirect_to_url->url)){ + return $this->successResponse([ + 'message' => '3D secure redirect', + 'payment_url' => $paymentIntent->next_action->redirect_to_url->url + ], Response::HTTP_MOVED_PERMANENTLY); + } + + return $this->handlePaymentIntentResult($transaction, $paymentIntent); + } + + } + + + }catch (Throwable $e){ + Log::error("Error Stripe submit payment intent"); + $errorMessage = $e->getMessage(); + Log::error($errorMessage); + } + + return $this->errorResponse($errorMessage ?? __('errors.unexpected_error')); + } + + + public function payOut(Request $request) + { + $this->validate($request, [ + 'card_no' => 'required_if:payment_method,CARD', + 'exp_month' => 'required_if:payment_method,CARD', + 'exp_year' => 'required_if:payment_method,CARD', + 'cvc' => 'required_if:payment_method,CARD', + 'bank_country' => 'required_if:payment_method,BANK|string|size:2', + 'bank_currency' => 'required_if:payment_method,BANK|string|size:3', + 'bank_account_number' => 'required_if:payment_method,BANK|string', +// 'aggregator_id' => 'required|integer', + 'amount' => 'required|numeric|min:5', + 'currency' => 'required|string|size:3', + 'customer_id' => 'nullable', + 'payment_method' => 'required|string|in:CARD,BANK', + 'customer_email' => 'required|email', + 'customer_name' => 'nullable|string', + 'customer_surname' => 'required|string', + 'customer_phone_number' => 'nullable|string', + 'customer_address' => 'required|string', + 'customer_city' => 'required|string', + 'customer_country' => 'required|string|size:2', + 'customer_state' => 'required_if:customer_country,US|string|size:2', //Etat du pays dans lequel se trouve le client. Cette valeur est obligatoire si le client se trouve au États Unis d’Amérique (US) ou au Canada (CA) + 'customer_zip_code' => 'required_if:customer_country,US|string|size:5', + 'reason' => 'required|string' + ]); + + + $aggregator = PaymentAggregator::where('name','like','%stripe%')->firstOrFail(); + + try{ + + $amount = $request->input('amount'); + $currency = $request->input('currency'); + $payment_method = $request->input('payment_method'); + + $transaction = PaymentTransaction::create([ + 'aggregator_id' => $aggregator->id, + "currency" => $request->input('currency'), + "transaction_id" => $this->getTransactionID(), + "amount" => $amount, + "payment_method" => $payment_method, + 'status' => PaymentTransactionStatus::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'), + ]); + + // Create destination account in Stripe + + if($payment_method == 'CARD'){ + //https://stripe.com/docs/api/external_account_cards/create + + $destination = $this->client->accounts->createExternalAccount( + config('variables.stripe_account'), [ + 'external_account' => [ + 'object' => 'card', + 'number' => $request->input('card_no'), + 'exp_month' => $request->input('exp_month'), + 'exp_year' => $request->input('exp_year'), + 'cvc' => $request->input('cvc'), + ] + ] + ); + }else{ + //https://stripe.com/docs/api/external_account_bank_accounts/create#account_create_bank_account + $destination = $this->client->accounts->createExternalAccount( + config('variables.stripe_account'), [ + 'external_account' => [ + 'object' => 'bank_account', + 'country' => $request->input('bank_country'), + 'currency' => $request->input('bank_currency'), + 'account_number' => $request->input('bank_account_number'), + ] + ] + ); + } + + if($destination){ + + $transaction->update([ + 'payment_method_exact' => $destination->id + ]); + + // Create payout in Stripe + // https://stripe.com/docs/api/payouts/create + $payout = $this->client->payouts->create([ + "amount" => $amount, + "currency" => $currency, + 'description' => $request->input('reason'), + "destination" => $destination->id, + "method" => $payment_method == 'CARD' ? 'instant' : 'standard' + ]); + + if($payout) { + + $transaction->update([ + 'payment_date' => date('Y-m-d H:i:s', $payout->arrival_date), + 'aggregator_payment_ref' => $payout->id, + 'status' => strtolower($payout->status) + ]); + + return $this->successResponse([ + 'transaction_id' => $transaction->transaction_id, + 'status' => $transaction->status + ]); + } + } + + + + }catch (Throwable $e){ + Log::error("Error Stripe submit payment intent"); + $errorMessage = $e->getMessage(); + Log::error($errorMessage); + } + + return $this->errorResponse($errorMessage ?? __('errors.unexpected_error')); + } + + + public function capturePaymentResult(Request $request) + { + $this->validate($request, [ + 'payment_intent' => 'nullable|string', + 'transaction_id' => 'required|string|exists:payment_transactions,transaction_id', + ]); + + $transaction = PaymentTransaction::where('transaction_id',$request->input('transaction_id'))->firstOrFail(); + + if($request->has('payment_intent')) { + + $paymentIntent = $this->client->paymentIntents->retrieve($request->input('payment_intent')); + + return $this->handlePaymentIntentResult($transaction, $paymentIntent); + } + + return $this->errorResponse(__('errors.unexpected_error')); + } + + private function handlePaymentIntentResult(PaymentTransaction $transaction, PaymentIntent $intent){ + + if($intent->status == 'succeeded'){ + $transaction->update([ + 'status' => PaymentTransactionStatus::ACCEPTED + ]); + + return $this->successResponse([ + 'message' => 'Payment Accepted', + 'transaction_id' => $transaction->transaction_id, + 'status' => $transaction->status + ]); + + }else{ + + $transaction->update([ + 'status' => strtolower($intent->status) + ]); + + return $this->errorResponse([ + 'message' => 'Payment '.$intent->status, + 'status' => $transaction->status + ]); + } + } } diff --git a/app/Http/Controllers/YoomeeController.php b/app/Http/Controllers/YoomeeController.php index 21c2d32..99b1adc 100644 --- a/app/Http/Controllers/YoomeeController.php +++ b/app/Http/Controllers/YoomeeController.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers; -use App\Enums\PaymentTransactionState; +use App\Enums\PaymentTransactionStatus; use App\Models\PaymentTransaction; use GuzzleHttp\Client; use Illuminate\Http\Request; @@ -76,7 +76,7 @@ class YoomeeController extends Controller 'payment_method' => 'required|string', 'customer_id' => 'required|integer', 'customer_email' => 'required|email', - 'customer_name' => 'required|string', + 'customer_name' => 'nullable|string', 'customer_surname' => 'required|string', 'customer_phone_number' => 'required|string', 'customer_address' => 'required|string', @@ -113,7 +113,7 @@ class YoomeeController extends Controller "amount" => $createResponse->transactionAmount, "payment_method" => $payment_method, 'payment_token' => Str::random(32), - 'state' => $createResponse->paymentStatus, + 'status' => $createResponse->paymentStatus, "reason" => $request->input('reason'), "customer_id" => $request->input('customer_id'), "customer_name" => $request->input('customer_name'), @@ -147,7 +147,7 @@ class YoomeeController extends Controller if ($responseCode == 202) { $transaction->update([ - 'state' => PaymentTransactionState::ACCEPTED, + 'status' => PaymentTransactionStatus::ACCEPTED, 'aggregator_payment_ref' => $this->getTransactionRef($responseData->message), ]); return $this->successResponse([ diff --git a/app/Http/Controllers/YoomeeV2Controller.php b/app/Http/Controllers/YoomeeV2Controller.php index c23c211..1404f3f 100644 --- a/app/Http/Controllers/YoomeeV2Controller.php +++ b/app/Http/Controllers/YoomeeV2Controller.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers; -use App\Enums\PaymentTransactionState; +use App\Enums\PaymentTransactionStatus; use App\Models\PaymentTransaction; use GuzzleHttp\Client; use Illuminate\Http\Request; @@ -86,7 +86,7 @@ class YoomeeV2Controller extends Controller 'payment_method' => 'required|string', 'customer_id' => 'required|integer', 'customer_email' => 'required|email', - 'customer_name' => 'required|string', + 'customer_name' => 'nullable|string', 'customer_surname' => 'required|string', 'customer_phone_number' => 'required|string', 'customer_address' => 'required|string', @@ -115,7 +115,7 @@ class YoomeeV2Controller extends Controller "payment_method" => $payment_method, "payment_url" => $payment_url, 'payment_token' => $payment_token, - 'state' => PaymentTransactionState::INITIATED, + 'status' => PaymentTransactionStatus::INITIATED, "reason" => $request->input('reason'), "customer_id" => $request->input('customer_id'), "customer_name" => $request->input('customer_name'), @@ -144,7 +144,7 @@ class YoomeeV2Controller extends Controller $token = $request->input('payment_token'); $transaction = PaymentTransaction::where('payment_token', $token) - ->where('state', PaymentTransactionState::INITIATED)->firstOrFail(); + ->where('status', PaymentTransactionStatus::INITIATED)->firstOrFail(); $customer_phone_number = $request->input('phone_number'); @@ -187,7 +187,7 @@ class YoomeeV2Controller extends Controller $transaction->update([ 'aggregator_payment_ref' => $createResponse->transaction_number, 'payment_date' => $createResponse->transaction_date, - 'state' => strtoupper($createResponse->transaction_status), + 'status' => strtoupper($createResponse->transaction_status), "customer_phone_number" => $customer_phone_number, ]); @@ -208,7 +208,7 @@ class YoomeeV2Controller extends Controller 'payment_method' => 'required|string', 'customer_id' => 'required|integer', 'customer_email' => 'required|email', - 'customer_name' => 'required|string', + 'customer_name' => 'nullable|string', 'customer_surname' => 'required|string', 'customer_phone_number' => 'required|string', 'customer_address' => 'required|string', @@ -256,7 +256,7 @@ class YoomeeV2Controller extends Controller 'payment_date' => $createResponse->transaction_date, "payment_method" => $payment_method, 'payment_token' => Str::random(64), - 'state' => strtoupper($createResponse->transaction_status), + 'status' => strtoupper($createResponse->transaction_status), "reason" => $request->input('reason'), "customer_id" => $request->input('customer_id'), "customer_name" => $request->input('customer_name'), @@ -270,7 +270,7 @@ class YoomeeV2Controller extends Controller "customer_zip_code" => $request->input('customer_zip_code'), ]); - if($transaction->state == PaymentTransactionState::PENDING){ + if($transaction->status == PaymentTransactionStatus::PENDING){ return $this->successResponse([ 'verification_url' => route('yoomee.v2.webhook',['transaction_id' => $transaction_id]) ]); @@ -309,9 +309,9 @@ class YoomeeV2Controller extends Controller try { // Si le paiement fait plus de 5 min on l'annule - if($transaction->state == PaymentTransactionState::PENDING && $transaction->created_at->diffInMinutes(Carbon::now()) > 5){ + if($transaction->status == PaymentTransactionStatus::PENDING && $transaction->created_at->diffInMinutes(Carbon::now()) > 5){ $transaction->update([ - 'state' => PaymentTransactionState::CANCELLED + 'status' => PaymentTransactionStatus::CANCELLED ]); } $response = $this->client->post('status/v1', [ @@ -330,17 +330,17 @@ class YoomeeV2Controller extends Controller if ($responseCode == 400) { - $state = strtoupper($responseData->transaction_status); - if($state == 'SUCCESS'){ - $state = PaymentTransactionState::ACCEPTED; + $status = strtoupper($responseData->transaction_status); + if($status == 'SUCCESS'){ + $status = PaymentTransactionStatus::ACCEPTED; }else{ - if(str_starts_with($state,'C')){ - $state = PaymentTransactionState::REFUSED; + if(str_starts_with($status,'C')){ + $status = PaymentTransactionStatus::REFUSED; } } $transaction->update([ - 'state' => $state, + 'status' => $status, 'payment_date' => $responseData->transaction_date ?? null, ]); } @@ -349,7 +349,7 @@ class YoomeeV2Controller extends Controller Log::info("Get Yoomee Payment Status Error"); Log::info($e->getMessage()); $transaction->update([ - 'state' => PaymentTransactionState::REFUSED + 'status' => PaymentTransactionStatus::REFUSED ]); } @@ -357,7 +357,7 @@ class YoomeeV2Controller extends Controller if($verify_btn){ return redirect()->route('checkout',['payment_token' => $transaction->payment_token]); }else { - if ($transaction->state == PaymentTransactionState::ACCEPTED) { + if ($transaction->status == PaymentTransactionStatus::ACCEPTED) { return [ 'message' => "Payment accepted", 'status' => 1, @@ -383,7 +383,7 @@ class YoomeeV2Controller extends Controller $transaction = PaymentTransaction::where('transaction_id',$request->input('transaction_id'))->first(); - if ($transaction->state == PaymentTransactionState::ACCEPTED) { + if ($transaction->status == PaymentTransactionStatus::ACCEPTED) { return redirect()->route('paymentResult', [ 'transaction_id' => $transaction->transaction_id, 'token' => $transaction->payment_token, diff --git a/app/Models/PaymentRefund.php b/app/Models/PaymentRefund.php new file mode 100644 index 0000000..ecc614a --- /dev/null +++ b/app/Models/PaymentRefund.php @@ -0,0 +1,11 @@ + $code])); + $result = collect(DB::select("SELECT * FROM $table WHERE transaction_id = :code", ['code' => $code])); $codeCorrect = sizeof($result) < 0; } while ($codeCorrect); return $code; diff --git a/composer.json b/composer.json index 8c7534c..bd4e7f6 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "ext-json": "*", "cknow/laravel-money": "^7.0", "darkaonline/swagger-lume": "^9.0", + "doctrine/dbal": "^3.6", "flipbox/lumen-generator": "^9.1", "guzzlehttp/guzzle": "^7.4", "illuminate/session": "^9.52", diff --git a/composer.lock b/composer.lock index 561363b..8bfcc07 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "415cc3a989344fb30c3fecd2cdff4999", + "content-hash": "f0ebadd8749412a4cd14d3ae29e1d742", "packages": [ { "name": "brick/math", @@ -339,6 +339,211 @@ }, "time": "2023-02-01T09:20:38+00:00" }, + { + "name": "doctrine/cache", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2022-05-20T20:07:39+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.6.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "9a747d29e7e6b39509b8f1847e37a23a0163ea6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/9a747d29e7e6b39509b8f1847e37a23a0163ea6a", + "reference": "9a747d29e7e6b39509b8f1847e37a23a0163ea6a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2022.3", + "phpstan/phpstan": "1.10.14", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "9.6.7", + "psalm/plugin-phpunit": "0.18.4", + "squizlabs/php_codesniffer": "3.7.2", + "symfony/cache": "^5.4|^6.0", + "symfony/console": "^4.4|^5.4|^6.0", + "vimeo/psalm": "4.30.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.6.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2023-06-01T05:46:46+00:00" + }, { "name": "doctrine/deprecations", "version": "v1.0.0", @@ -382,6 +587,98 @@ }, "time": "2022-05-02T15:47:09+00:00" }, + { + "name": "doctrine/event-manager", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/95aa4cb529f1e96576f3fda9f5705ada4056a520", + "reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^0.5.3 || ^1", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^10", + "phpstan/phpstan": "~1.4.10 || ^1.8.8", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/1.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2022-10-12T20:51:15+00:00" + }, { "name": "doctrine/inflector", "version": "2.0.6", diff --git a/config/variables.php b/config/variables.php index 4506449..d9792e5 100644 --- a/config/variables.php +++ b/config/variables.php @@ -19,4 +19,5 @@ return [ 'stripe_key' => env('STRIPE_KEY', ''), 'stripe_secret' => env('STRIPE_SECRET', ''), 'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', ''), + 'stripe_account' => env('STRIPE_ACCOUNT', ''), ]; diff --git a/database/migrations/2023_06_05_111933_create_payment_refunds_table.php b/database/migrations/2023_06_05_111933_create_payment_refunds_table.php new file mode 100644 index 0000000..55d8d53 --- /dev/null +++ b/database/migrations/2023_06_05_111933_create_payment_refunds_table.php @@ -0,0 +1,40 @@ +bigIncrements('id'); + $table->string('refund_id')->unique(); + $table->bigInteger('transaction_id'); + $table->string('aggregator_payment_ref')->nullable(); + $table->decimal('amount'); + $table->string('currency',3); + $table->string('date')->nullable(); + $table->text('reason'); + $table->string('status'); + $table->text('metadata')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('payment_refunds'); + } +}; diff --git a/database/migrations/2023_06_07_070215_rename_state_to_status_in_payment_transactions.php b/database/migrations/2023_06_07_070215_rename_state_to_status_in_payment_transactions.php new file mode 100644 index 0000000..fbe8171 --- /dev/null +++ b/database/migrations/2023_06_07_070215_rename_state_to_status_in_payment_transactions.php @@ -0,0 +1,32 @@ +renameColumn('state','status'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('payment_transactions', function (Blueprint $table) { + $table->renameColumn('status','state'); + }); + } +}; diff --git a/database/migrations/2023_06_07_102900_change_customer_id_and_customer_phone_number_in_payment_transactions.php b/database/migrations/2023_06_07_102900_change_customer_id_and_customer_phone_number_in_payment_transactions.php new file mode 100644 index 0000000..a2b88ec --- /dev/null +++ b/database/migrations/2023_06_07_102900_change_customer_id_and_customer_phone_number_in_payment_transactions.php @@ -0,0 +1,36 @@ +string('customer_id')->nullable()->change(); + $table->string('customer_phone_number')->nullable()->change(); + $table->string('customer_name')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('payment_transactions', function (Blueprint $table) { + $table->string('customer_id')->change(); + $table->string('customer_phone_number')->change(); + $table->string('customer_name')->change(); + }); + } +}; diff --git a/routes/web.php b/routes/web.php index 4650a38..3efc625 100644 --- a/routes/web.php +++ b/routes/web.php @@ -39,7 +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'],'/stripe/webhook', ['as' => 'stripe.webhook' , 'uses' => 'StripeController@capturePaymentResult']); $router->addRoute(['GET','POST'],'/paymentResult', ['as' => 'paymentResult' , 'uses' => 'PaymentController@paymentResult']); @@ -50,6 +50,7 @@ $router->group(['middleware' => 'auth'], function () use ($router) { */ $router->get('methods','PaymentController@getMethods'); $router->post('pay','PaymentController@pay'); + $router->get('checkStatus/{transaction_id}','PaymentController@checkStatus'); /** @@ -77,8 +78,11 @@ $router->group(['middleware' => 'auth'], function () use ($router) { * Stripe Endpoints */ $router->group(['prefix' => 'stripe'], function () use ($router) { + $router->post('refund',['as' => 'stripe.refund', 'uses' => 'StripeController@refund']); $router->get('methods',['as' => 'stripe.methods', 'uses' => 'StripeController@getMethods']); - $router->addRoute(['POST'],'pay',['as' => 'stripe.pay', 'uses' => 'StripeController@pay']); + $router->post('pay',['as' => 'stripe.pay', 'uses' => 'StripeController@pay']); + $router->post('payIn',['as' => 'stripe.submit', 'uses' => 'StripeController@payIn']); + $router->post('payOut',['as' => 'stripe.submit', 'uses' => 'StripeController@payOut']); }); });