Yii2 贝宝支付集成

Yii2 paypal payment integration

我正在使用此 https://www.yiiframework.com/extension/bitcko/yii2-bitcko-paypal-api#usage 和 yii2 来启用付款我的代码如下所示。

public function actionMakePayment(){
          if(!Yii::$app->user->getIsGuest()){
               // Setup order information array
              $params = [
                  'order'=>[
                      'description'=>'Payment description',
                      'subtotal'=>45,
                      'shippingCost'=>0,
                      'total'=>45,
                      'currency'=>'USD',
                  ]
              ];
            // In case of payment success this will return the payment object that contains all information about the order
            // In case of failure it will return Null

            Yii::$app->PayPalRestApi->processPayment($params);
        }else{
          Yii::$app->response->redirect(Url::to(['site/signup'], true));
        }

一切都按照我的预期进行,此调用将类似这样的内容返回给 dom。

{ "id": "PAYID-LTKUAVA8WK14445NN137182H", "intent": "sale", "state": "approved", "cart": "9RE74926AX5730813", "payer": { "payment_method": "paypal", "status": "UNVERIFIED", "payer_info": { "first_name": "Susi", "last_name": "Flo", "payer_id": "KWPDGYRP2KCK4", "shipping_address": { "recipient_name": "Susi Flo", "line1": "Suso", "line2": "bldg", "city": "Spring hill", "state": "FL", "postal_code": "34604", "country_code": "US" }, "phone": "3526003902", "country_code": "US" } }, "transactions": [ { "amount": { "total": "45.00", "currency": "USD", "details": { "subtotal": "45.00", "shipping": "0.00", "insurance": "0.00", "handling_fee": "0.00", "shipping_discount": "0.00" } }, "payee": { "merchant_id": "NHN6S6KT4FF6N", "email": "arunwebber2-facilitator@gmail.com" }, "description": "Payment description", "invoice_number": "5cd5404d624a9", "soft_descriptor": "PAYPAL *TESTFACILIT", "item_list": { "items": [ { "name": "Item one", "price": "45.00", "currency": "USD", "tax": "0.00", "quantity": 1 } ], "shipping_address": { "recipient_name": "Susi Flo", "line1": "Suso", "line2": "bldg", "city": "Spring hill", "state": "FL", "postal_code": "34604", "country_code": "US" } }, "related_resources": [ { "sale": { "id": "6LN25215GP1183020", "state": "completed", "amount": { "total": "45.00", "currency": "USD", "details": { "subtotal": "45.00", "shipping": "0.00", "insurance": "0.00", "handling_fee": "0.00", "shipping_discount": "0.00" } }, "payment_mode": "INSTANT_TRANSFER", "protection_eligibility": "ELIGIBLE", "protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE", "transaction_fee": { "value": "2.43", "currency": "USD" }, "receipt_id": "3896118010137330", "parent_payment": "PAYID-LTKUAVA8WK14445NN137182H", "create_time": "2019-05-10T09:30:10Z", "update_time": "2019-05-10T09:30:10Z", "links": [ { "href": "https://api.sandbox.paypal.com/v1/payments/sale/6LN25215GP1183020", "rel": "self", "method": "GET" }, { "href": "https://api.sandbox.paypal.com/v1/payments/sale/6LN25215GP1183020/refund", "rel": "refund", "method": "POST" }, { "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-LTKUAVA8WK14445NN137182H", "rel": "parent_payment", "method": "GET" } ], "soft_descriptor": "PAYPAL *TESTFACILIT" } } ] } ], "create_time": "2019-05-10T09:11:48Z", "update_time": "2019-05-10T09:30:10Z", "links": [ { "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-LTKUAVA8WK14445NN137182H", "rel": "self", "method": "GET" } ] }

如何将其存储到我的数据库中?对于特定用户 ID,我可以通过此获取用户 ID。

echo Yii::$app->user->id;

我想将此值与用户 ID 一起存储,我该怎么做?并向用户发送支付成功消息:)

更新

看起来组件 class 需要完全复制和编辑才能正确覆盖 checkOut() 方法,因为在该方法中访问的 属性 $apiContextprivate 而不是 $protected 所以你要么复制整个组件并将它放在你的 frontend/components 目录中并相应地更改它然后使用。

最重要的是 class 的设计和编写也很糟糕,如果您使用我在我的 Yii2 项目中一直使用的以下组件会更好。我没有删除额外的代码,并按照答案中的原样粘贴了文件。您可以 remove/comment 与 BalanceHistory TransactionHistory 相关的部分和电子邮件部分。您需要通过 composer 安装 paypal checkout sdk 或在您的 composer.json

中添加以下内容

"paypal/paypal-checkout-sdk": "1.0.1"

贝宝组件

<?php

namespace frontend\components;

use Yii;
use common\models\{
    User,
    BalanceHistory,
    TransactionHistory
};
use yii\base\Component;
use common\components\Helper;
use PayPalCheckoutSdk\Core\{
    PayPalHttpClient,
    SandboxEnvironment,
    ProductionEnvironment
};
use PayPalCheckoutSdk\Orders\{
    OrdersGetRequest,
    OrdersCreateRequest,
    OrdersCaptureRequest
};

class Paypal extends Component
{

    /**
     * The Pyapal Client Id
     *
     * @var mixed
     */
    public $clientId;

    /**
     * The Paypal client Secret
     *
     * @var mixed
     */
    public $clientSecret;

    /**
     * API context object
     *
     * @var mixed
     */
    private $httpClient; // paypal's http client

    /**
     * @var mixed
     */
    private $user_id;

    /**
     * Override Yii's object init()
     *
     * @return null
     */
    public function init()
    {
        $this->httpClient = new PayPalHttpClient(
            Yii::$app->params['paypal']['mode'] == 'sandbox' ?
                new SandboxEnvironment($this->clientId, $this->clientSecret) :
                new ProductionEnvironment($this->clientId, $this->clientSecret)
        );

        $this->user_id = Yii::$app->user->id;

        Yii::info("User: {$this->user_id} Init PayPal", 'paypal');
    }

    /**
     * Returns the context object
     *
     * @return object
     */
    public function getClient()
    {
        return $this->httpClient;
    }

    /**
     * Set the payment methods and other objects necessary for making the payment
     *
     * @param decimal $price the amount to be charged
     *
     * @return string $approvalUrl
     */
    public function createOrder($price)
    {
        //create order request
        $request = new OrdersCreateRequest();
        $request->prefer('return=representation');

        setlocale(LC_MONETARY, 'en_US.UTF-8');

        $price = sprintf('%01.2f', $price);

        Yii::info("User: {$this->user_id} Setting payment for amount: {$price}", 'paypal');

        //build the request body
        $requestBody = [
            'intent' => 'CAPTURE',
            'purchase_units' =>
            [
                0 =>
                [
                    'amount' =>
                    [
                        'currency_code' => 'USD',
                        'value' => $price,
                    ],
                ],
            ],
            'application_context' => [
                'shipping_preference' => 'NO_SHIPPING'
            ]
        ];

        $request->body = $requestBody;

        //call PayPal to set up a transaction
        $client = $this->getClient();
        $response = $client->execute($request);

        return json_encode($response->result, JSON_PRETTY_PRINT);
    }

    /**
     * @param $orderId
     */
    public function getOrder($orderId)
    {

        // 3. Call PayPal to get the transaction details
        $request = new OrdersGetRequest($orderId);
        $client = $this->getClient();
        $response = $client->execute($request);

        return json_encode($response->result, JSON_PRETTY_PRINT);
    }

    /**
     * Retrieves Order Capture Details for the given order ID
     *
     * @param string $orderId the payment id of the transaction
     *
     * @return mixed
     */
    public function captureOrder($orderId)
    {
        $request = new OrdersCaptureRequest($orderId);
        //Call PayPal to capture an authorization
        $client = $this->getClient();

        $transaction = Yii::$app->db->beginTransaction();
        try {
            $response = $client->execute($request);

            //get payment variables for email
            $paymentId = $response->result->id;
            $paymentStatus = $response->result->status;
            $paypalTransaction = $response->result->purchase_units[0]->payments->captures[0];
            $payedAmount = $paypalTransaction->amount->value;
            $txnId = $paypalTransaction->id;
            $userId = $this->user_id;

            //get the user
            $model = User::findOne($userId);
            $profile = $model->businessProfile;
            $prevBalance = $profile->balance;

            if ($paymentStatus == 'COMPLETED') {
                Yii::info("User: {$userId} payment amount:{$payedAmount} approved updating balance.", 'paypal');
                //update balance
                $newBalance = $profile->updateBalance($payedAmount);

                Yii::info("User: {$userId} balance updated.", 'paypal');

                $data = [
                    'amount' => $payedAmount,
                    'type' => TransactionHistory::BALANCE_ADDED,
                    'description' => "Funds added to account",
                    'user' => [
                        'id' => $userId,
                        'balance' => $newBalance,
                    ],
                ];

                Yii::info("User: {$userId} adding transaction history.", 'paypal');

                TransactionHistory::add($data);

                //update subscription status if required
                if ($profile->subscription_status !== 'active') {
                    $profile->updateStatus('active');
                }

                Yii::info("User: {$userId} adding balance history:{$payedAmount}.", 'paypal');

                //send the success email to the user and admin
                $this->sendNotification($model, $response->result);

                //set session flash with success
                Yii::$app->session->setFlash(
                    'success',
                    'Your Payment is processed and you will receive an email with the details shortly'
                );
            } else {
                Yii::warning("User: {$userId} payment amount:{$payedAmount} NOT approved.", 'paypal');
                //send the error email to the user and admin

                $this->sendNotification($model, $response->result, 'error');
                //set session flash with error
                Yii::$app->session->setFlash(
                    'danger',
                    'Your Payment was not approved, an email has been sent with the details.'
                );
            }

            //update balance history
            BalanceHistory::add(
                $profile->user_id,
                $prevBalance,
                $payedAmount,
                $paymentId,
                $paymentStatus,
                $txnId,
                $response
            );

            //commit the transaction
            $transaction->commit();

            Yii::info(
                "User: {$userId} payment Success prevBalance: {$prevBalance} payedAmount:{$payedAmount}.",
                'paypal'
            );
            return json_encode($response->result, JSON_PRETTY_PRINT);
        } catch (\Exception $e) {
            //roll back the transaction
            $transaction->rollBack();

            Yii::error("ERROR EXCEPTION", 'paypal');
            Yii::error($e->getMessage(), 'paypal');
            Yii::error($e->getTraceAsString(), 'paypal');

            //send error email to the developers
            Helper::sendExceptionEmail(
                "TC : Exception on PayPal Balance",
                $e->getMessage(),
                $e->getTraceAsString()
            );

            //set session flash with error
            Yii::$app->session->setFlash('danger', $e->getMessage());
        }
    }

    /**
     * Sends Success Email for the transaction
     *
     * @param \common\models\User $model the user model object
     * @param  $response the paypal Order Capture object
     * @param string $type the type of the notification to be sent
     *
     * @return null
     */
    public function sendNotification(
        \common\models\User $model,
        $response,
        $type = 'success'
    ) {
        Yii::info("User: {$this->user_id} Sending notifications type:{$type}", 'paypal');

        $paymentId = $response->id;
        $paymentStatus = $response->status;
        $paypalTransaction = $response->purchase_units[0]->payments->captures[0];
        $payedAmount = $paypalTransaction->amount->value;

        //payment creation time
        $paymentCreateTime = new \DateTime(
            $paypalTransaction->create_time,
            new \DateTimeZone('UTC')
        );

        //payment update time
        $paymentUpdateTime = new \DateTime(
            $paypalTransaction->update_time,
            new \DateTimeZone('UTC')
        );

        //payer/billing info for email
        $payerInfo = $response->payer;
        $payerEmail = $payerInfo->email_address;
        $payerFirstName = $payerInfo->name->given_name;
        $payerLastName = $payerInfo->name->surname;
        $billingInfo = [
            'billing_info' => [
                'email' => $payerEmail,
                'full_name' => "$payerFirstName $payerLastName",
            ],
        ];

        if (property_exists($response->purchase_units[0], 'shipping')) {
            $payerAddress = property_exists($response->purchase_units[0]->shipping->address, 'address_line_1');
            $isStateAvailable = property_exists($response->purchase_units[0]->shipping->address, 'admin_area_1');
            $isPostCodeAvailable = property_exists($response->purchase_units[0]->shipping->address, 'postal_code');
            $iscountryCodeAvailable = property_exists($response->purchase_units[0]->shipping->address, 'country_code');
            //@codingStandardsIgnoreStart
            $payerState =  $isStateAvailable ? $response->purchase_units[0]->shipping->address->admin_area_1 : 'NA';
            $payerPostalCode = $isPostCodeAvailable ? $response->purchase_units[0]->shipping->address->postal_code : 'NA';
            $payerCountryCode = $iscountryCodeAvailable ? $response->purchase_units[0]->shipping->address->country_code : 'NA';
            //@codingStandardsIgnoreEnd
            $billingInfo['billing_info'] = array_merge(
                $billingInfo['billing_info'],
                [
                    'address' => $payerAddress,
                    'state' => $payerState,
                    'country' => $payerCountryCode,
                    'post_code' => $payerPostalCode,
                ]
            );
        }

        //email params
        $data = [
            'user' => [
                'email' => $model->email,
                'name' => $model->username,
            ],
            'payment_id' => $paymentId,
            'amount' => $payedAmount,
            'status' => $paymentStatus,
            'create_time_utc' => $paymentCreateTime,
            'update_time_utc' => $paymentUpdateTime,
        ];

        $data = array_merge($data, $billingInfo);

        //check the notification email type and set params accordingly
        if ($type == 'success') {
            $txnId = $paypalTransaction->id;
            $data['txn_id'] = $txnId;
            $subject = Yii::$app->id . ': Your Account has been recharged.';
            $view = '@frontend/views/user/mail/payment-complete';
        } else {
            $subject = Yii::$app->id . ': Transaction failed.';
            $view = '@frontend/views/user/mail/payment-failed';
        }

        Yii::info("User: {$this->user_id} Sending email to user:{$model->email} type: {$type}", 'paypal');
        //send email to user
        $model->sendEmail($subject, $view, $data, $model->email);

        //send notification to admin for Payment Received
        $data['user']['email'] = Yii::$app->params['adminEmail'];

        $subject = ($type == 'success') ?
            Yii::$app->id . ': New Transaction in Account.' :
            Yii::$app->user->id . ': A Transaction Failed for the user.';

        Yii::info(
            "User: {$this->user_id} Sending email to admin " . Yii::$app->params['adminEmail'] . " type: {$type}",
            'paypal'
        );
        //send admin email
        $model->sendEmail($subject, $view, $data, Yii::$app->params['adminEmail']);
    }
}

用法

您可以分别调用 createOrdercaptureOrder。我用 ajax 方法使用它,所以我有如下定义的单独操作

/**
     * Displays fail message to the user
     *
     * @param string $token the cancel token
     *
     * @return mixed
     * @throws \Exception
     */
    public function actionPaymentCancel($token)
    {
        Yii::warning("Payment Cancel : token: {$token}.", 'paypal');

        return $this->render(
            'payment-cancelled',
            [
                'data' => $token,
            ]
        );
    }

    /**
     * Shows the payment details & success message to the user
     *
     * @param string $paymentId the payment id
     *
     * @return mixed
     * @throws \Exception
     */
    public function actionPaymentComplete($paymentId)
    {
        $history = BalanceHistory::findOne(['payment_id' => $paymentId]);

        return $this->render(
            'payment-complete',
            [
                'data' => $history,
            ]
        );
    }

    /**
     * Captures the Paypal order and verifies it
     *
     * @param string $orderId the Paypal order object's id
     *
     * @return mixed
     */
    public function actionCaptureOrder($orderId)
    {
        $orderInfo = Yii::$app->paypal->captureOrder($orderId);
        return $orderInfo;
    }

    /**
     * Creates the order and
     *
     * @param string $amount the price of the order
     *
     * @return mixed
     */
    public function actionCreateOrder($amount)
    {
        if (!Yii::$app->user->isGuest) {
            $order = Yii::$app->paypal->createOrder($amount);
            return $order;
        }
        throw new Exception("You are not logged in.", 404);
    }

    /**
     * Executes the payement and checkouts to the paypal to confirm
     *
     * @param string $token     the paypal token
     *
     * @return mixed
     */
    public function actionPaymentExecute($orderId)
    {
        //get transaction details
        $details = Yii::$app->paypal->getOrder($orderId);

        $details = json_decode($details);

        //added check for duplicate hits to return url from Paypal
        if (null !== BalanceHistory::transactionExists($orderId)) {
            //redirect to payment complete
            return $this->redirect(['payment-complete', 'paymentId' => $orderId]);
        }

        if ($details->status == 'COMPLETED') {
            //redirect to payment complete
            return $this->redirect(['payment-complete', 'paymentId' => $orderId]);
        } else {
            //redirect to the payment failed page
            return $this->redirect(['payment-failed', 'paymentId' => $orderId]);
        }
    }

另外请记住,您需要为 paypal 网关声明一个带有实时和本地 ENV 的参数,这会变成沙箱环境 ON/OFF。

params-local.php

<?php
    'paypal'=>[
        'sandbox'=>true
    ]
?>

params.php

<?php
    'paypal'=>[
        'sandbox'=>false
    ]
?>    

Paypal PHP-SDK 为您提供 setCustom() 添加自定义字段值,您可以使用它发送用户 ID,然后在付款后通过交易对象中的响应检索它已执行。

您使用的只是一个使用 Paypal SDK 函数的自定义组件,您应该扩展 class bitcko\paypalrestapi\PayPalRestApi.php 以覆盖函数 checkOut() 并添加 ->setCustom(Yii::$app->user->id) 到此 line 中的链,因为它不提供任何设置自定义字段的方法,因此只需将方法的整个代码复制到新的 class 并添加上面的内容。

您的 class 应该如下所示。

注意:在 common/components 文件夹中添加文件。

<?php
namespace common\components;

use bitcko\paypalrestapi\PayPalRestApi as PayPalBase;
use PayPal\Api\Amount;
use PayPal\Api\Details;
use PayPal\Api\Item;
use PayPal\Api\ItemList;
use PayPal\Api\Payer;
use PayPal\Api\Payment;
use PayPal\Api\RedirectUrls;
use PayPal\Api\Transaction;
use PayPal\Exception\PayPalConnectionException;
use yii\helpers\Url;
use Yii;

class PaypalRestApi extends PayPalBase
{

    public function checkOut($params)
    {
        $payer = new Payer();
        $payer->setPaymentMethod($params['method']);
        $orderList = [];

        foreach ($params['order']['items'] as $orderItem) {
            $item = new Item();
            $item->setName($orderItem['name'])
                ->setCurrency($orderItem['currency'])
                ->setQuantity($orderItem['quantity'])
                ->setPrice($orderItem['price']);
            $orderList[] = $item;
        }
        $itemList = new ItemList();
        $itemList->setItems($orderList);
        $details = new Details();
        $details->setShipping($params['order']['shippingCost'])
            ->setSubtotal($params['order']['subtotal']);
        $amount = new Amount();
        $amount->setCurrency($params['order']['currency'])
            ->setTotal($params['order']['total'])
            ->setDetails($details);
        $transaction = new Transaction();
        $transaction->setAmount($amount)
            ->setItemList($itemList)
            ->setDescription($params['order']['description'])
            ->setCustom(Yii::$app->user->id)
            ->setInvoiceNumber(uniqid());

        $redirectUrl = Url::to([$this->redirectUrl], true);
        $redirectUrls = new RedirectUrls();
        $redirectUrls->setReturnUrl("$redirectUrl?success=true")
            ->setCancelUrl("$redirectUrl?success=false");
        $payment = new Payment();
        $payment->setIntent($params['intent'])
            ->setPayer($payer)
            ->setRedirectUrls($redirectUrls)
            ->setTransactions(array($transaction));
        try {
            $payment->create($this->apiContext);
            return \Yii::$app->controller->redirect($payment->getApprovalLink());
        } catch (PayPalConnectionException $ex) {
            // This will print the detailed information on the exception.
            //REALLY HELPFUL FOR DEBUGGING
            \Yii::$app->response->format = \yii\web\Response::FORMAT_HTML;
            \Yii::$app->response->data = $ex->getData();
        }
    }
}

现在,将 PayPalRestApi 组件 class 中的 common/config/main.phpfrontend/config/main.php 组件的配置更改为您创建的新 class

'components'=> [
    ...
 'PayPalRestApi'=>[
      'class'=>'common\components\PayPalRestApi',
  ]
    ...
]

所以现在您可以使用

获得相同的用户 ID
$response = \yii\helpers\Json::decode( Yii::$app->PayPalRestApi->processPayment($params));
$user_id = $response['transactions'][0]['custom'];