본문 바로가기

Dev/PHP

[PHP] Laravel - PayPal 결제 모듈 연동하기 (2) - 백엔드 처리

 

 

 

본 포스팅은 [PHP Laravel - PayPal 결제 모듈 연동하기 (1) - 프런트 엔드 처리에서 이어집니다.

만약 이전 포스팅을 보지 않았다면 먼저 보고 오시는 걸 권장드립니다.

 

2020/12/30 - [Dev/PHP] - [PHP] Laravel - PayPal 결제 모듈 연동하기 (1) - 프런트엔드 처리

 

[PHP] Laravel - PayPal 결제 모듈 연동하기 (1) - 프런트엔드 처리

이번 포스팅에서는 Laravel 프로젝트에서 PayPal 결제 모듈 연동하는 방법에 대해 알아보겠습니다. 연동 방법은 매우 다양하게 있지만, 여기서는 화면에서 javascript를 통해 결제 모델을 생성하고 서

dev-overload.tistory.com

 

이전 포스팅에서 프런트엔드에서 결제 모델 생성, 서버에 유효성 검사 및 캡처(결제 승인) 요청을 구현했습니다.

이전 포스팅에서는 "요청"을 구현했다면 이번 포스팅은 "응답"을 구현하는 단계라고 표현할 수 있습니다.

이전 포스팅에서 이어지는 내용으로 구성했으므로 각 항목의 넘버링을 이전 포스팅 기준에 맞추겠습니다.

 

10. /verified-product 응답 컨트롤러 구현

createOrder: function(data, actions){ //결제 요청
    return fetch('/verified-product', {
        method:'POST',
        headers:{
            "Content-Type": "application/json;charset=utf-8",
            "X-CSRF-TOKEN": document.getElementById("csrf-token").getAttribute("content")
        },
        body: JSON.stringfy({
            pd_id: document.getElementById("pd_id").value
        })
    ...
}

 

이전 포스팅에서 javascript를 통해 /verified-product에 pd_id를 보내 id값이 유효한지 검증을 기대하는 코드를 작성했었습니다.

이것에 대한 응답 코드를 작성합니다.

 

PayPalController.php를 열고 아래의 코드를 작성합니다.

 

<?php

...

class PaypalController extends Controller
{
    ...

    public function verified_product(Request $request){

        //파라미터가 전달 되었는지 체크, 없다면 valid = false 반환
        if(empty($request->pd_id){
            $result = array(
                'valid' => false,
                'name' => 'Parameter Pass Error',
                'message' => 'Missing paramters.',
            );
            
            return response()->json($request, 201);
        }
    
         //DB에 아래와 같은 상품 정보가 준비 되어 있다고 가정한다.
         
         $product_info = array(
             'pd_id' => '1',
             'description => 'paypal test product',
             'amount => array(
                 'value => '0.01',
                 'currency => config('paypal.currency')
             )
         );
     
         // ============================================
         
         try{
             
             //전달 받은 pd_id와 준비된 상품의 id값과 일치하는지 체크
             //올바르다면 valid =true와 함께 결제에 필요한 상품 정보를 반환한다.
             if($product_info['pd_id'] == $request->pd_id){
                 $result = array(
                     'valid' => true,
                     'purchase_units => $product_info',
                 );
                 
                 return response()->json($result, 201);
             } else {
                 //일치하지 않는다면 올바르지 않은 접근이므로 valid = false 반환
                 $result = array(
                     'valid' => false,
                     'name' => 'Unknown Server Error',
                     'message' => 'Process Faild'
                 );
             
                 return response()->json($result, 201);
             }
         
         } catch( \Exception $e){
             //진행 중 프로그램적 예외가 발생했을때 반환.
             $result = array(
                 'valid' => false,
                 'name' => 'Unknown Server Error',
                 'message' => 'Process Faild'
             );
             
             return response()->json($result, 201);
         }
    } 
}

 

본래 위 함수의 목적은 DB와 의 값을 대조해보고 그 무결성을 검증하는것이 목적입니다.

하지만 본 포스팅에서 DB까지 다루기에는 너무 분량이 많아지는 관계로 $product_info 변수를 DB 데이터라고 가정하고 진행합니다.

위 함수는 올바르지 않은 접근, 파라미터 누락, 등 상황에 맞게 값을 반환하는 비교적 단순한 함수입니다.

다음으로 onApprove에서 /order-capture 요청에 대한 응답 컨트롤 함수를 작성해야 하는데 그전에 선행 단계가 있습니다.

 

11. 서버 사이드 PayPal 통신 구축 - 개요

PHP 에서 PayPal과 연동하기 위해서는 페이팔 요청과 그 요청을 실행을 전담하는 각각의 클래스 파일로 별도 정의하는 것이 좋습니다.

 

PHP에서의 PayPal Api 요청 구상도

결제 승인, 결제 정보 획득, 환불 등 거래 종류별 함수를 관리할 PayPalService 클래스를 정의하고 각 요청 처리를 전담할 PayPalClient 클래스를 작성해 연결하는 것으로 명료하게 합니다.

 

 

12. 서버 사이드 PayPal 통신 구축 - PayPalClient.php 작성

먼저, 모든 요청에 대한 실행을 전담할 PayPalClient.php를 작성하도록 하겠습니다.

 

PayPalClient.php

app 폴더에 Service 폴더를 생성하고 그 안에 PayPalClient.php 파일을 생성합니다.

아래의 내용을 PayPalClient.php에 작성합니다.

 

<?php

namespace App\Services;

use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Core\SandboxEnvironment;;
use PayPalCheckoutSdk\Core\PayPalEnvironment;

use Illuminate\Support\Facades\Log;

ini_set('error_reporting', E_ALL);
ini_set('display_errors', '1');
ini_set('display_startup_errors','1');

class PayPalClient
{
    public static function client()
    {
        return new PayPalHttpClient(self::environment());
    }
    
    public static function environment()
    {
        $clientId = config('paypal.client_id');
        $clientSecret = config('paypal.secret');
        
        //만약 페이팔 모드 설정이 live 인 경우 PayPalEnvironment 클래스를 사용한다.
        if(config('paypal.mode') == 'live'){
            return new PayPalEnvironment($clientId, $clientSecret);
        } else {
            return new SandboxEnvironment($clientId, $clientSecret);
        }
    }
}

?>

위 코드에서 필요한 정보는 1장에서 정의한 PayPal.php 파일 내 Client Id와 Sercret입니다.

config함수로 이 둘 정보를 받아오도록 합니다.

 

그리고 결제 요청할 때 샌드박스일때와 라이브일 때 서로 사용하는 클래스가 다릅니다.

따라서 PayPal.php에 설정한 mode값에 따라 실행하는 객체를 다르게 지정해주었습니다.

 

위 코드는 페이팔측에서 PayPalClient.php 작성 방법에 대해 깃허브에 자료를 공유하고 있으므로 이를 참고했습니다.

github.com/paypal/Checkout-PHP-SDK/blob/develop/samples/PayPalClient.php

 

paypal/Checkout-PHP-SDK

PHP SDK for Checkout RESTful APIs. Contribute to paypal/Checkout-PHP-SDK development by creating an account on GitHub.

github.com

 

 

13. 서버 사이드 PayPal 통신 구축 - PayPalService.php (captureOrder) 작성

본 예제에서는 PayPalService.php에 캡처 기능만 정의합니다.

이후 필요에 따라 결제 내역, 환불 등의 함수를 이 파일에서 관리할 수 있습니다. 

 

PayPalService.php

아래의 내용을 PayPalService.php에 작성합니다.

 

<?php

namespace App\Services;

use Illuminate\Support\Facades\Log;

use App\Services\PayPalClient;
use PayPalCheckoutSdk\Orders\OrdersCaptureRequest;

class PayPalService
{
  
   # 거래 캡쳐
    public static function captureOrder($orderId, $debug=false)
    {
        Log::debug('fnc Contect captureOrder');
        $request = new OrdersCaptureRequest($orderId);    
    
        $client = PayPalClient::client();
        $response = $client->execute($request);
    
        if ($debug)
        {
             Log::info("Status Code: {$response->statusCode}\n");
             Log::info("Status: {$response->result->status}\n");
             Log::info("Order ID: {$response->result->id}\n");
             Log::info("Links:\n");
             foreach($response->result->links as $link)
             {
                  Log::info("\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n");
             }

             Log::info("Capture Ids:");
             foreach($response->result->purchase_units as $purchase_unit)
             {
                  foreach($purchase_unit->payments->captures as $capture)
                  {
                       Log::info("\t{$capture->id}");
                  }
             }
            // To print the whole response body, uncomment the following line
            Log::info(json_encode($response->result, JSON_PRETTY_PRINT));
            Log::debug('fnc End captureOrder');
        }
        return $response->result;
    }
}

if (!count(debug_backtrace()))
{
  PayPalService::captureOrder('REPLACE-WITH-APPORVED-ORDER-ID', true);
}

?>

 

위 코드의 핵심은 $response = $client->execute($request); 이 부분입니다.

$request는 페이팔 클래스 중 하나인 OrdersCaptureRequest에 매개변수 orderId를 적용한 후, 이 객체를 PayPalClient.php가 상속받은 execute 함수가 최종적으로 페이팔 서버에 요청을 보냅니다.

이때, execute 함수는 페이팔로부터 어떠한 반환 값을 받게 되는데, 성공 시에는 아래와 같은 데이터 구조체를 반환받습니다.

 

{
    "id": "",
    "status": "COMPLETED",
    "purchase_units": [
        {
            "reference_id": "default",
            "shipping": {
                "name": {
                    "full_name": "John Doe"
                },
                "address": {
                    "address_line_1": "",
                    "admin_area_2": "",
                    "admin_area_1": "",
                    "postal_code": "",
                    "country_code": "KR"
                }
            },
            "payments": {
                "captures": [
                    {
                        "id": "",
                        "status": "COMPLETED",
                        "amount": {
                            "currency_code": "USD",
                            "value": "30.00"
                        },
                        "final_capture": true,
                        "seller_protection": {
                            "status": "ELIGIBLE",
                            "dispute_categories": [
                                "ITEM_NOT_RECEIVED",
                                "UNAUTHORIZED_TRANSACTION"
                            ]
                        },
                        "seller_receivable_breakdown": {
                            "gross_amount": {
                                "currency_code": "USD",
                                "value": "30.00"
                            },
                            "paypal_fee": {
                                "currency_code": "USD",
                                "value": "1.32"
                            },
                            "net_amount": {
                                "currency_code": "USD",
                                "value": "28.68"
                            }
                        },
                        "links": [
                            {
                                "href": "https:\/\/api.sandbox.paypal.com\/v2\/payments\/captures\/{id}",
                                "rel": "self",
                                "method": "GET"
                            },
                            {
                                "href": "https:\/\/api.sandbox.paypal.com\/v2\/payments\/captures\/{id}\/refund",
                                "rel": "refund",
                                "method": "POST"
                            },
                            {
                                "href": "https:\/\/api.sandbox.paypal.com\/v2\/checkout\/orders\/{id}",
                                "rel": "up",
                                "method": "GET"
                            }
                        ],
                        "create_time": "2020-12-29T02:39:29Z",
                        "update_time": "2020-12-29T02:39:29Z"
                    }
                ]
            }
        }
    ],
    "payer": {
        "name": {
            "given_name": "John",
            "surname": "Doe"
        },
        "email_address": "",
        "payer_id": "",
        "address": {
            "country_code": "KR"
        }
    },
    "links": [
        {
            "href": "https:\/\/api.sandbox.paypal.com\/v2\/checkout\/orders\/46W78238YV410522N",
            "rel": "self",
            "method": "GET"
        }
    ]
}

 

DB에 결제와 관련한 어떠한 기록을 남겨야 한다면 위 반환 값으로부터 취할 수 있습니다.

위 코드는 아래의 소스코드에서 확인할 수 있습니다.

 

github.com/paypal/Checkout-PHP-SDK/blob/develop/samples/CaptureIntentExamples/CaptureOrder.php

 

paypal/Checkout-PHP-SDK

PHP SDK for Checkout RESTful APIs. Contribute to paypal/Checkout-PHP-SDK development by creating an account on GitHub.

github.com

 

14. /order-captuer 응답 컨트롤러 구현

이제 프런트엔드에서의 /order-capture 요청에 대한 응답 컨트롤러를 구현할 준비가 되었습니다.

PaypalController.php 파일을 열고 아래의 코드를 작성합니다.

 

<?php

...

use App\Services\PayPalService; // 클래스 추가

class PaypalController extends Controller
{
    ...

    public function order_capture(Request $request)
    {
        Log::debug('fnc Contect order_capture');
        
        //파라미터가 전달되지 않은 경우
        if(empty($request->order_id)){
            Log::debug('Payment was not successful.');
            
            $result_process = array(
                'valid' => false,
                'name' => 'Parameter Pass Error',
                'message' => 'Missing parameters.',
            );
            return response()->json($result, 201);
        }
        
        try{
            //아래 함수를 통해 결제 승인
            $capture_info = PayPalService::captureOrder($request->order_id);
            
            //필요 할 경우, 아래의 값을 DB에 저장한다.
            $capture_id = $capture_info->purchase_units[0]->payments->captures[0]->id;
            Log::debug("capture ID : {$capture_id}");
            
            //결제 완료 반환
            $result = array(
                'valid' => true,
                'name' => 'Payment Successfuly.',
                'message' => 'Payment Successfuly.',
            );
            
            return response()->json($result, 201);
        } catch(\Exception $e){
            $result array(
                'valid' => false,
                'name' => 'Payments Error',
                'message' => 'Payments was not successful. Please Report to us by email.',
            );
            return response()->json($result, 201);
        } catch(\Throwable $e){
            $result array(
                'valid' => false,
                'name' => 'Payments Error',
                'message' => 'Payments was not successful. Please Report to us by email.',
            );
            return response()->json($result, 201);
        }
        
    }
}

 

우선 해당 함수에 요청이 왔을 때 order_id 매개변수가 없는 경우 돌려보내는 간단한 예외처리를 해주었습니다.

그리고 try문 안에 PayPalService 클래스 내 captureOrder 함수에 order_id를 넣어 호출하고 13번 항목에서 반환하는 구조체를 가져옵니다.

 

$capture_id는 거래 승인이 이루어지고 발급 받는 id입니다. 필요한 경우 이를 DB에 저장할 수 있습니다.

 

만약 captureOrder 함수 내부에서 예외가 발생한 경우 try ~ catch 문의 Throwable로 받게 되기 때문에 catch문을 2가지로 넣어주었습니다. (Exception은 위 함수 내부에서 발생할 경우에 예외를 받습니다.)

 

 

15. 응답 Route 등록

routes\web.php 파일을 열고 아래의 내용을 작성합니다.

 

<?php

...

use App\Http\Controllers\PaypalController;

...

Route::post('/verified-product', [PaypalController::class, 'verified_product']);
Route::post('/order-capture', [PaypalController::class, 'order_capture']);

이제 결제를 위한 준비가 완료되었습니다.

 

 

16. 결제 테스트

브라우저에서 결제 테스트를 해보겠습니다.

 

paypal 결제 테스트

 

javascript에서 정의한대로 결제가 성공적으로 이루어졌습니다.

이제 로그 파일을 살펴보겠습니다.

 

capture id

 

storage\logs\laravel.log 파일을 열어보면 컨트롤러의 코드에서 캡처 아이디 로그가 기록된 것을 확인할 수 있습니다.

 

이것으로 PHP - Laravel에서의 PayPal 결제 모듈 연동에 대한 포스팅을 마치겠습니다.