diff --git a/app/Enums/PaymentMethod.php b/app/Enums/PaymentMethod.php index dfc8163..66f5468 100644 --- a/app/Enums/PaymentMethod.php +++ b/app/Enums/PaymentMethod.php @@ -6,6 +6,6 @@ namespace App\Enums; abstract class PaymentMethod { - const CARD = 'CARD'; // Les remboursements ou recharges vers des clients - const WALLET = 'WALLET'; // Les paiements effectués par les clients + const CARD = 'CARD'; + const WALLET = 'WALLET'; } diff --git a/app/Enums/PaymentTransactionStatus.php b/app/Enums/PaymentTransactionStatus.php index 0225288..f38794d 100644 --- a/app/Enums/PaymentTransactionStatus.php +++ b/app/Enums/PaymentTransactionStatus.php @@ -12,4 +12,6 @@ abstract class PaymentTransactionStatus const PENDING_OTP = 'PENDING_OTP'; const REFUSED = 'REFUSED'; const CANCELLED = 'CANCELLED'; + + const EXPIRED = 'EXPIRED'; } diff --git a/app/Http/Controllers/FlutterwaveController.php b/app/Http/Controllers/FlutterwaveController.php index fcba048..60a32fe 100644 --- a/app/Http/Controllers/FlutterwaveController.php +++ b/app/Http/Controllers/FlutterwaveController.php @@ -103,7 +103,7 @@ class FlutterwaveController extends Controller // Init payment $createResponse = (new Client())->post('https://api.flutterwave.com/v3/payments', [ 'headers' => [ - "Authorization" => config('variables.flw_secret_key') + "Authorization" => 'Bearer '.config('variables.flw_secret_key') ], 'json' => [ "tx_ref" => $transaction_id, @@ -232,4 +232,188 @@ class FlutterwaveController extends Controller } + + public function payOut(Request $request) + { + $this->validate($request, [ +// 'aggregator_id' => 'required|integer', + 'amount' => 'required|numeric|min:5', + 'currency' => 'required|string|size:3', + 'customer_id' => 'nullable', +// 'payment_method' => 'required|string|in:WALLET', + 'customer_email' => 'required|email', + 'customer_name' => 'nullable|string', + 'customer_surname' => 'required|string', + 'customer_phone_number' => ['nullable','string',(new Phone())->country(['CI','SN','ML','CM','TG','BF','CD','GN','BJ'])], + 'customer_address' => 'nullable|string', + 'customer_city' => 'nullable|string', + 'customer_country' => 'required|string|size:2|in:CI,SN,ML,CM,TG,BF,CD,GN,BJ', + 'reason' => 'required|string' + ]); + + $aggregator = PaymentAggregator::where('name','like','%flutterwave%')->firstOrFail(); + + try{ + + $customer_surname = $request->input('customer_surname'); + $customer_name = $request->input('customer_name') ?? $customer_surname; + $customer_email = $request->input('customer_email'); + + $country_code = $request->input('customer_country'); + $phoneNumber = str_replace(' ','',$request->input('customer_phone_number')); + + $phone = new PhoneNumber($phoneNumber, $country_code); + $phoneNumber = str_replace(' ','',$phone->formatInternational()); + $nationalPhone = str_replace(' ','',$phone->formatNational()); + $phonePrefix = substr($phoneNumber, 1, strlen($phoneNumber) - strlen($nationalPhone) - 1); + + $amount = $request->input('amount'); + $payment_method = 'WALLET'; + +// if($amount < 500){ +// return $this->errorResponse('Minimun amount is 500'); +// } + + + $transactionId = $this->getTransactionID(); + + $transaction = PaymentTransaction::create([ + 'aggregator_id' => $aggregator->id, + "currency" => $request->input('currency'), + "transaction_id" => $transactionId, + "amount" => $amount, + "payment_method" => $payment_method, + 'status' => PaymentTransactionStatus::INITIATED, + "reason" => $request->input('reason'), + "customer_id" => $request->input('customer_id'), + "customer_name" => $customer_name, + "customer_surname" => $customer_surname, + "customer_email" => $customer_email, + "customer_phone_number" => $phoneNumber, + "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'), + ]); + + // Transfert Fund + $transfertResponse = (new Client())->post('https://api.flutterwave.com/v3/transfers', [ + 'headers' => [ + "Authorization" => 'Bearer '.config('variables.flw_secret_key') + ], + 'json' => [ + "account_bank" => 'orangemoney', + "account_number" => $nationalPhone, + 'amount' => $amount, + 'narration' => $request->input('reason'), + 'currency' => $request->input('currency'), + 'reference' => $transactionId, + 'callback_url' => route('cinetpay.transfert.webhook'), + 'debit_currency' => $request->input('currency'), + ], + 'timeout' => $this->timeout, + 'http_errors' => false + ]); + + $responseData = json_decode($transfertResponse->getBody()->getContents()); + $responseCode = $transfertResponse->getStatusCode(); + + if ($responseCode == 200) { + + Log::info("Response of transfert"); + Log::info(json_encode($responseData)); +// $transaction->update([ +// 'aggregator_payment_ref' => $responseData->data[0][0]?->transaction_id, +// 'status' => $this->convertTransfertStatus($responseData->data[0][0]?->treatment_status) , +// ]); + + return $this->successResponse([ + 'message' => 'Transfert is pending', + 'transaction_id' => $transactionId, + 'transaction_status' => $transaction->status + ]); + }else{ + Log::error("Error Flutterwave make transfert payment"); + Log::error(json_encode($responseData)); + return $this->errorResponse(__('errors.service_unavailable_try_later')); + } + +// $errorMessage = $responseData?->description ?? $responseData?->message; + + }catch (Throwable $e){ + Log::error("Error CinetPay transfert payment"); + $errorMessage = $e->getMessage(); + Log::error("Response data :: ".json_encode($responseData ?? '')); + Log::error($errorMessage); + } + + return $this->errorResponse($errorMessage ?? __('errors.unexpected_error')); + } + + public function captureTransfertResult(Request $request) + { + Log::info("Transfert wevbook"); + Log::info(json_encode($request->all())); + $this->validate($request, [ + 'transaction_id' => 'nullable|string', + 'client_transaction_id' => 'nullable|string|exists:payment_transactions,transaction_id' + ]); + +// if($request->has('transaction_id') && $request->has('client_transaction_id')){ +// $transaction = PaymentTransaction::where('transaction_id',$request->input('client_transaction_id'))->firstOrFail(); +// try { +// +// $client = new Client([ +// 'base_uri' => config('variables.cinetpay_transfert_url') +// ]); +// +// // Login +// $loginResponse = $client->post('auth/login', [ +// 'form_params' => [ +// "apikey" => config('variables.cinetpay_api_key'), +// "password" => config('variables.cinetpay_transfert_password'), +// ], +// 'timeout' => $this->timeout +// ]); +// +// $responseData = json_decode($loginResponse->getBody()->getContents()); +// $token = $responseData->data->token; +// $responseCode = $loginResponse->getStatusCode(); +// if ($responseCode == 200 && !empty($token)) { +// +// $response = $client->get('transfer/check/money', [ +// 'query' => [ +// 'token' => $token, +// 'transaction_id' => $request->input('transaction_id') +// ], +// ]); +// +// $responseData = json_decode($response->getBody()->getContents()); +// $responseCode = $response->getStatusCode(); +// if ($responseCode == 200) { +// +// $transaction->update([ +// 'aggregator_payment_ref' => $responseData->data[0]?->transaction_id, +// 'status' => $this->convertTransfertStatus($responseData->data[0]?->treatment_status), +// ]); +// +// } +// } +// +// } catch (Throwable $e) { +// Log::info("Get Cinetpay Transfert Status Error"); +// $errorMessage = $e->getMessage(); +// Log::error("Response data :: ".json_encode($responseData ?? '')); +// Log::info($errorMessage); +// $transaction->update([ +// 'status' => PaymentTransactionStatus::REFUSED +// ]); +// } +// +// return $this->errorResponse($errorMessage ?? __('errors.unexpected_error')); +// }else{ + return response("OK"); +// } + } } diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 300d1a8..95e2ca5 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -92,7 +92,8 @@ class PaymentController extends Controller $networkName = strtolower($request->input('network_name')); if(str_contains($networkName,'orange') || str_contains($networkName,'mtn')){ - return app(CinetpayController::class)->payOut($request); +// return app(CinetpayController::class)->payOut($request); + return app(FlutterwaveController::class)->payOut($request); } } @@ -119,14 +120,16 @@ class PaymentController extends Controller $amount = money($transaction->amount, $transaction->currency)->format(app()->getLocale()); $receiver = config('variables.receiver_name'); $receiver_logo = asset('assets/images/logo.jpeg'); + $paymentEndpoint = $transaction->payment_endpoint; + $paymentVerifyEndpoint = $transaction->payment_verify_endpoint; if($transaction->status == PaymentTransactionStatus::INITIATED){ - return view('checkout',compact('payment_token','method','amount', 'receiver','receiver_logo')); + return view('checkout',compact('paymentEndpoint', 'payment_token','method','amount', 'receiver','receiver_logo')); } if($transaction->status == PaymentTransactionStatus::PENDING){ - return view('verify-payment',compact('transaction_id','method','amount', 'receiver','receiver_logo')); + return view('verify-payment',compact('paymentVerifyEndpoint','transaction_id','method','amount', 'receiver','receiver_logo')); } $status = $transaction->status == PaymentTransactionStatus::ACCEPTED; @@ -224,4 +227,28 @@ class PaymentController extends Controller { return app(CinetpayController::class)->checkBalance($request); } + + + public function webPaymentRedirection(Request $request) + { + + $this->validate($request, [ + 'transaction_id' => 'required|string|exists:payment_transactions,transaction_id' + ]); + + $transaction = PaymentTransaction::where('transaction_id',$request->input('transaction_id'))->first(); + + if ($transaction->status == PaymentTransactionStatus::ACCEPTED) { + return redirect()->route('paymentResult', [ + 'transaction_id' => $transaction->transaction_id, + 'token' => $transaction->payment_token, + 'status' => 1 + ]); + } else { + return redirect()->route('paymentResult', [ + 'message' => "Payment failed", + 'status' => 0 + ]); + } + } } diff --git a/app/Http/Controllers/YnoteOrangeController.php b/app/Http/Controllers/YnoteOrangeController.php new file mode 100644 index 0000000..d8fb691 --- /dev/null +++ b/app/Http/Controllers/YnoteOrangeController.php @@ -0,0 +1,363 @@ +client = new Client([ + 'base_uri' => $this->baseURL, + 'timeout' => $this->timeout, + 'http_errors' => false, + 'verify' => false, // Disable SSL Certification verification + ]); + } + + /** + * @OA\Get( + * path="/ynote/methods", + * summary="Afficher la liste des methodes de paiment de Ynote", + * tags={"Ynote"}, + * security={{"api_key":{}}}, + * @OA\Response( + * response=200, + * description="OK", + * @OA\JsonContent( + * ref="#/components/schemas/ApiResponse", + * example = { + * "status" : 200, + * "response" : {"hasWebview": true, "methods": {"Orange": "Orange"}}, + * "error":null + * } + * ) + * ) + * ) + */ + public function getMethods() + { + return $this->successResponse([ + 'hasWebview' => true, + 'methods' => [ + 'Orange' => 'Orange' + ] + ] + ); + } + + + /* + * Init payment and provide iLink World checkout page to handle payment + */ + public function initPay(Request $request) + { + $this->validate($request, [ +// 'aggregator_id' => 'required|integer|exists:payment_aggregators,id', + 'amount' => 'required|numeric|min:5', + 'currency' => 'required|string|size:3', + 'customer_id' => 'required|integer', + 'customer_email' => 'required|email', + 'customer_name' => 'nullable|string', + 'customer_surname' => 'required|string', + 'customer_phone_number' => 'required|string', + 'customer_address' => 'required|string', + 'customer_country' => 'required|string|size:2', + 'reason' => 'required|string', + ]); + + $aggregator = PaymentAggregator::where('name','like','%ynote%')->firstOrFail(); + + $transaction_id = $this->getTransactionID(); + $payment_method = 'Orange'; + $customer_phone_number = $request->input('customer_phone_number'); + if(str_contains($customer_phone_number,'+237')){ + $customer_phone_number = substr($customer_phone_number,4); + } + + do{ + $payment_token = Str::random(64); + }while(PaymentTransaction::where('payment_token', $payment_token)->exists()); + + + $payment_url = route('checkout',['payment_token' => $payment_token]); + + PaymentTransaction::create([ + 'aggregator_id' => $aggregator->id, + "currency" => $request->input('currency'), + "transaction_id" => $transaction_id, + "amount" => $request->input('amount'), + "payment_method" => $payment_method, + "payment_url" => $payment_url, + 'payment_token' => $payment_token, + '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" => $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'), + "payment_endpoint" => route('ynote.checkoutPay'), + "payment_verify_endpoint" => route('ynote.status'), + ]); + + return $this->successResponse([ + 'message' => 'Payment initiated', + 'payment_url' => $payment_url + ]); + } + + public function checkoutPay(Request $request) + { + $this->validate($request, [ + 'payment_token' => 'required|string|exists:payment_transactions,payment_token', + 'phone_number' => 'required|string', + ]); + + $token = $request->input('payment_token'); + $transaction = PaymentTransaction::where('payment_token', $token) + ->where('status', PaymentTransactionStatus::INITIATED)->firstOrFail(); + + + $customer_phone_number = $request->input('phone_number'); + if(str_contains($customer_phone_number,'+237')){ + $customer_phone_number = substr($customer_phone_number,4); + } + + + // Get Access Token + $accessTokenResponse = $this->client->post('/token', [ + 'headers' => [ + 'Authorization' => 'Basic '.base64_encode($this->username.':'.$this->password), + ], + 'form_params' => [ + 'grant_type' => 'client_credentials', + ] + ]); + + + $responseBody = json_decode($accessTokenResponse->getBody()->getContents()); + + if ($accessTokenResponse->getStatusCode() == 200) { + + $accessToken = $responseBody->access_token; + + // Init Payment + $initResponse = $this->client->post('/omcoreapis/1.0.2/mp/init', [ + 'headers' => [ + 'Authorization' => 'Bearer '.$accessToken, + 'X-AUTH-TOKEN' => $this->X_AUTH_TOKEN + ], + ]); + + + $responseBody = json_decode($initResponse->getBody()->getContents()); + + if ($initResponse->getStatusCode() == 200) { + + $payToken = $responseBody->data->payToken; + + // Make Payment + $makeResponse = $this->client->post('/omcoreapis/1.0.2/mp/pay', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $accessToken, + 'X-AUTH-TOKEN' => $this->X_AUTH_TOKEN + ], + 'json' => [ + 'notifUrl' => 'http://28f8-102-244-222-34.ngrok-free.app/ynote/webhook' ,// route('ynote.webhook'), + 'channelUserMsisdn' => $this->channelUserMsisdn, + 'pin' => $this->PIN, + 'amount' => ''.intval(ceil(floatval($transaction->amount))), + 'subscriberMsisdn' => $customer_phone_number, + 'orderId' => $transaction->transaction_id, + 'description' => $transaction->reason, + 'payToken' => $payToken + ] + ]); + + $responseBody = json_decode($makeResponse->getBody()->getContents()); + if ($makeResponse->getStatusCode() == 200) { + + $transaction->update([ + 'aggregator_payment_ref' => $responseBody->data->txnid, + 'aggregator_payment_token' => $payToken, + 'status' => strtoupper($responseBody->data->status), + "customer_phone_number" => $customer_phone_number, + ]); + + return redirect()->route('checkout',['payment_token' => $token]); + + + }else{ + //If error regenerate transaction id to avoid 'Numero de commande deja utilisé' + $transaction->update([ + 'transaction_id' => $this->getTransactionID() + ]); + } + } + } + + if(!empty($responseBody->error)){ + //Convert into single line + session()->flash('error',str_replace(array("\n", "\r"), '',$responseBody->error_description)); + return redirect()->route('checkout',['payment_token' => $token]); + } + + Log::error(json_encode($responseBody)); + session()->flash('error',__('errors.unexpected_error')); + return redirect()->route('checkout',['payment_token' => $token]); + } + + public function capturePaymentResult(Request $request) + { + Log::info("capture"); + Log::warning(json_encode($request->all())); + + $this->validate($request, [ + 'txnid' => 'required|string' + ]); + + $transaction = PaymentTransaction::where('aggregator_payment_ref',$request->input('txnid'))->first(); + return $this->getPaymentStatus($transaction); + } + + public function getPaymentStatus(Request $request) + { + $this->validate($request, [ + 'transaction_id' => 'required|string|exists:payment_transactions,transaction_id', + 'verify_btn' => 'nullable|boolean' + ]); + + $transaction = PaymentTransaction::where('transaction_id',$request->input('transaction_id'))->first(); + $verify_btn = $request->input('verify_btn'); + + try { + + // Si le paiement fait plus de 5 min on l'annule + if($transaction->status == PaymentTransactionStatus::PENDING && $transaction->created_at->diffInMinutes(Carbon::now()) > 5){ + $transaction->update([ + 'status' => PaymentTransactionStatus::CANCELLED + ]); + } + + // Get Access Token + $accessTokenResponse = $this->client->post('/token', [ + 'headers' => [ + 'Authorization' => 'Basic '.base64_encode($this->username.':'.$this->password), + ], + 'form_params' => [ + 'grant_type' => 'client_credentials', + ] + ]); + + $responseBody = json_decode($accessTokenResponse->getBody()->getContents()); + + if ($accessTokenResponse->getStatusCode() == 200) { + + $accessToken = $responseBody->access_token; + + $response = $this->client->get('/omcoreapis/1.0.2/mp/paymentstatus/'.$transaction->aggregator_payment_token, [ + 'headers' => [ + 'Authorization' => 'Bearer '.$accessToken, + 'X-AUTH-TOKEN' => $this->X_AUTH_TOKEN + ], + ]); + + $responseData = json_decode($response->getBody()->getContents()); + $responseCode = $response->getStatusCode(); + + Log::info(json_encode($responseData)); + Log::info(json_encode($responseCode)); + Log::info(json_encode([ + 'Authorization' => 'Bearer '.$accessToken, + 'X-AUTH-TOKEN' => $this->X_AUTH_TOKEN + ])); + + Log::info(json_encode($transaction->aggregator_payment_token)); + + Log::info("Veriication de paement"); + if ($responseCode == 200) { + + $status = strtoupper($responseData->data->status); + if(str_contains($status,'SUCCESS')){ + $status = PaymentTransactionStatus::ACCEPTED; + } + + + $transaction->update([ + 'status' => $status, + 'payment_date' => $responseData->data->transaction_date ?? null, + ]); + } + } + + + } catch (Throwable $e) { + Log::info("Get Ynote Payment Status Error"); + Log::info($e); + $transaction->update([ + 'status' => PaymentTransactionStatus::REFUSED + ]); + + } + + if($verify_btn){ + return redirect()->route('checkout',['payment_token' => $transaction->payment_token]); + }else { + Log::warning($transaction->status); + if ($transaction->status == PaymentTransactionStatus::ACCEPTED) { + return [ + 'message' => "Payment accepted", + 'status' => 1, + 'refresh' => true, + ]; + } else { + return [ + 'message' => "Payment failed", + 'status' => 0, + 'refresh' => $transaction->status == PaymentTransactionStatus::PENDING + ]; + } + + } + } + +} diff --git a/app/Http/Controllers/YoomeeV2Controller.php b/app/Http/Controllers/YoomeeV2Controller.php index 1404f3f..9cbe535 100644 --- a/app/Http/Controllers/YoomeeV2Controller.php +++ b/app/Http/Controllers/YoomeeV2Controller.php @@ -127,6 +127,8 @@ class YoomeeV2Controller extends Controller "customer_country" => $request->input('customer_country'), "customer_state" => $request->input('customer_state'), "customer_zip_code" => $request->input('customer_zip_code'), + "payment_endpoint" => route('yoomee.v2.checkoutPay'), + "payment_verify_endpoint" => route('yoomee.v2.status'), ]); return $this->successResponse([ @@ -333,15 +335,11 @@ class YoomeeV2Controller extends Controller $status = strtoupper($responseData->transaction_status); if($status == 'SUCCESS'){ $status = PaymentTransactionStatus::ACCEPTED; - }else{ - if(str_starts_with($status,'C')){ - $status = PaymentTransactionStatus::REFUSED; - } } $transaction->update([ 'status' => $status, - 'payment_date' => $responseData->transaction_date ?? null, + 'payment_date' => $responseData->data->transaction_date ?? null, ]); } @@ -374,27 +372,4 @@ class YoomeeV2Controller extends Controller } - public function merchantRedirect(Request $request) - { - - $this->validate($request, [ - 'transaction_id' => 'required|string|exists:payment_transactions,transaction_id' - ]); - - $transaction = PaymentTransaction::where('transaction_id',$request->input('transaction_id'))->first(); - - if ($transaction->status == PaymentTransactionStatus::ACCEPTED) { - return redirect()->route('paymentResult', [ - 'transaction_id' => $transaction->transaction_id, - 'token' => $transaction->payment_token, - 'status' => 1 - ]); - } else { - return redirect()->route('paymentResult', [ - 'message' => "Payment failed", - 'status' => 0 - ]); - } - } - } diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 54cff37..e2217c0 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -54,9 +54,6 @@ class VerifyCsrfToken { protected function tokensMatch($request) { $token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN'); - Log::info($token); - Log::error($request->session()->token()); - if (!$token && $header = $request->header('X-XSRF-TOKEN')) { $token = $this->encrypter->decrypt($header); } diff --git a/config/variables.php b/config/variables.php index cad8884..f47ce00 100644 --- a/config/variables.php +++ b/config/variables.php @@ -22,5 +22,7 @@ return [ 'stripe_secret' => env('STRIPE_SECRET', ''), 'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', ''), 'stripe_account' => env('STRIPE_ACCOUNT', ''), - 'flw_secret_key' => env('FLW_SECRET_KEY', '') + 'flw_secret_key' => env('FLW_SECRET_KEY', ''), + 'ynote_username' => env('YNOTE_USERNAME', ''), + 'ynote_password' => env('YNOTE_PASSWORD', ''), ]; diff --git a/database/migrations/2023_10_12_141608_add_payment_endpoint_and_payment_verify_endpoint_in_payment_transactions.php b/database/migrations/2023_10_12_141608_add_payment_endpoint_and_payment_verify_endpoint_in_payment_transactions.php new file mode 100644 index 0000000..9c86bd5 --- /dev/null +++ b/database/migrations/2023_10_12_141608_add_payment_endpoint_and_payment_verify_endpoint_in_payment_transactions.php @@ -0,0 +1,34 @@ +string('aggregator_payment_token')->after('aggregator_payment_ref')->nullable(); + $table->string('payment_endpoint')->after('status')->nullable(); + $table->string('payment_verify_endpoint')->after('payment_endpoint')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('payment_transactions', function (Blueprint $table) { + $table->dropColumn(['aggregator_payment_token','payment_endpoint','payment_verify_endpoint']); + }); + } +}; diff --git a/resources/views/checkout.blade.php b/resources/views/checkout.blade.php index cae1359..6805c53 100644 --- a/resources/views/checkout.blade.php +++ b/resources/views/checkout.blade.php @@ -187,7 +187,7 @@