在這裡, 我們將從整個請求的生命週期來分析, 一步步實現整個過程.
一. 生命週期
1. Checkout - 收銀臺支付
拆解流程如圖所示(過程類似支付寶的收銀臺):
流程詳解:
-
本地應用組裝好引數並請求Checkout介面, 介面同步返回一個支付URL;
-
本地應用重定向至這個URL, 登陸PayPal賬戶並確認支付, 使用者支付後跳轉至設定好的本地應用地址;
-
PayPal傳送非同步通知至本地應用, 本地拿到資料包後進行驗籤操作;
-
驗籤成功則進行支付完成後的業務(修改本地訂單狀態、增加銷量、傳送郵件等).
2. Subscription - 訂閱支付
拆解流程:
流程詳解:
-
建立一個計劃;
-
啟用該計劃;
-
用已經啟用的計劃去建立一個訂閱申請;
-
本地跳轉至訂閱申請連結獲取使用者授權並完成第一期付款, 使用者支付後攜帶token跳轉至設定好的本地應用地址;
-
回跳後請求執行訂閱;
-
收到訂閱授權非同步回撥結果, 收到支付結果的非同步回撥, 驗證支付非同步回撥成功則進行支付完成後的業務.
二. 具體實現
瞭解了以上流程, 接下來開始Coding.
github上有很多SDK, 這裡使用的是官方的SDK.
Checkout
在專案中安裝擴充套件
$ composer require paypal/rest-api-sdk-php:* // 這裡使用的最新版本
建立paypal配置檔案
$ touch config/paypal.php
配置內容如下(沙箱和生產兩套配置):
<?php
return [
/*
|--------------------------------------------------------------------------
| PayPal sandbox config
|--------------------------------------------------------------------------
|
|
*/
'sandbox' => [
'client_id' => env('PAYPAL_SANDBOX_CLIENT_ID', ''),
'secret' => env('PAYPAL_SANDBOX_SECRET', ''),
'notify_web_hook_id' => env('PAYPAL_SANDBOX_NOTIFY_WEB_HOOK_ID', ''), // 全域性回撥的鉤子id(可不填)
'checkout_notify_web_hook_id' => env('PAYPAL_SANDBOX_CHECKOUT_NOTIFY_WEB_HOOK_ID', ''), // 收銀臺回撥的鉤子id
'subscription_notify_web_hook_id' => env('PAYPAL_SANDBOX_SUBSCRIPTION_NOTIFY_WEB_HOOK_ID', ''), // 訂閱回撥的鉤子id
],
/*
|--------------------------------------------------------------------------
| PayPal live config
|--------------------------------------------------------------------------
|
|
*/
'live' => [
'client_id' => env('PAYPAL_CLIENT_ID', ''),
'secret' => env('PAYPAL_SECRET', ''),
'notify_web_hook_id' => env('PAYPAL_NOTIFY_WEB_HOOK_ID', ''),
'checkout_notify_web_hook_id' => env('PAYPAL_CHECKOUT_NOTIFY_WEB_HOOK_ID', ''),
'subscription_notify_web_hook_id' => env('PAYPAL_SUBSCRIPTION_NOTIFY_WEB_HOOK_ID', ''),
],
];
建立一個PayPal服務類
$ mkdir -p app/Services && touch app/Services/PayPalService.php
編寫Checkout的方法
可以參考官方給的DEMO
<?php
namespace App\Services;
use App\Models\Order;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use PayPal\Api\Currency;
use PayPal\Auth\OAuthTokenCredential;
use PayPal\Rest\ApiContext;
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 Symfony\Component\HttpKernel\Exception\HttpException;
class PayPalService
{
/*
* array
*/
protected $config;
/*
* string
*/
protected $notifyWebHookId;
/*
* obj ApiContext
*/
public $apiContext;
public function __construct($config)
{
// 金鑰配置
$this->config = $config;
$this->notifyWebHookId = $this->config['web_hook_id'];
$this->apiContext = new ApiContext(
new OAuthTokenCredential(
$this->config['client_id'],
$this->config['secret']
)
);
$this->apiContext->setConfig([
'mode' => $this->config['mode'],
'log.LogEnabled' => true,
'log.FileName' => storage_path('logs/PayPal.log'),
'log.LogLevel' => 'DEBUG', // PLEASE USE `INFO` LEVEL FOR LOGGING IN LIVE ENVIRONMENTS
'cache.enabled' => true,
]);
}
/**
* @Des 收銀臺支付
* @Author Mars
* @param Order $order
* @return string|null
*/
public function checkout(Order $order)
{
try {
$payer = new Payer();
$payer->setPaymentMethod('paypal');
$item = new Item();
$item->setName($order->product->title) // 子訂單的名稱
->setDescription($order->no) // 子訂單描述
->setCurrency($order->product->currency) // 幣種
->setQuantity(1) // 數量
->setPrice($order->total_amount); // 價格
$itemList = new ItemList();
$itemList->setItems([$item]); // 設定子訂單列表
// 這裡是設定運費等
$details = new Details();
$details->setShipping(0)
->setSubtotal($order->total_amount);
// 設定總計費用
$amount = new Amount();
$amount->setCurrency($order->product->currency)
->setTotal($order->total_amount)
->setDetails($details);
// 建立交易
$transaction = new Transaction();
$transaction->setAmount($amount)
->setItemList($itemList)
->setDescription($order->no)
->setInvoiceNumber(uniqid());
// 這裡設定支付成功和失敗後的跳轉連結
$redirectUrls = new RedirectUrls();
$redirectUrls->setReturnUrl(route('payment.paypal.return', ['success' => 'true', 'no' => $order->no]))
->setCancelUrl(route('payment.paypal.return', ['success' => 'false', 'no' => $order->no]));
$payment = new Payment();
$payment->setIntent('sale')
->setPayer($payer)
->setRedirectUrls($redirectUrls)
->setTransactions([$transaction]);
$payment->create($this->apiContext);
// 得到支付連結
return $payment->getApprovalLink();
} catch (HttpException $e) {
Log::error('PayPal Checkout Create Failed', ['msg' => $e->getMessage(), 'code' => $e->getStatusCode(), 'data' => ['order' => ['no' => $order->no]]]);
return null;
}
}
將PayPal服務類註冊在容器中
開啟檔案 app/Providers/AppServiceProvider.php
<?php
namespace App\Providers;
.
.
.
use App\Services\PayPalService;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
.
.
.
// 註冊PayPalService開始
$this->app->singleton('paypal', function () {
// 測試環境
if (app()->environment() !== 'production') {
$config = [
'mode' => 'sandbox',
'client_id' => config('paypal.sandbox.client_id'),
'secret' => config('paypal.sandbox.secret'),
'web_hook_id' => config('paypal.sandbox.notify_web_hook_id'),
];
}
// 生產環境
else {
$config = [
'mode' => 'live',
'client_id' => config('paypal.live.client_id'),
'secret' => config('paypal.live.secret'),
'web_hook_id' => config('paypal.live.notify_web_hook_id'),
];
}
return new PayPalService($config);
});
// 註冊PayPalService結束
}
.
.
.
建立控制器
由於訂單系統要視具體業務需求, 在這裡就不贅述了. 下面直接根據訂單去直接請求checkout支付
$ php artisan make:controller PaymentsController
<?php
namespace App\Http\Controllers;
use App\Models\Order;
class PaymentController extends Controller
{
/**
* @Des PayPal-Checkout
* @Author Mars
* @param Order $order
*/
public function payByPayPalCheckout(Order $order)
{
// 判斷訂單狀態
if ($order->paid_at || $order->closed) {
return json_encode(['code' => 422, 'msg' => 'Order Status Error.', 'url' => '']);
}
// 得到支付的連結
$approvalUrl = app('paypal')->checkout($order);
if (!$approvalUrl) {
return json_encode(['code' => 500, 'msg' => 'Interval Error.', 'url' => '']);
}
// 支付連結
return json_encode(['code' => 201, 'msg' => 'success.', 'url' => $approvalUrl]);
}
}
支付完的回跳方法
app/Http/Controllers/PaymentController.php
<?php
.
.
.
use Illuminate\Http\Request;
class PaymentController extends Controller
{
.
.
.
/**
* @Des 支付完的回跳入口
* @Author Mars
* @param Request $request
*/
public function payPalReturn(Request $request)
{
if ($request->has('success') && $request->success == 'true') {
// TODO: 這裡編寫支付後的具體業務(如: 跳轉到訂單詳情等...)
} else {
// TODO: 這裡編寫失敗後的業務
}
}
}
驗籤方法
在PayPalService中加入驗籤方法app/Services/PayPalService.php
<?php
namespace App\Services;
.
.
.
use PayPal\Api\VerifyWebhookSignature;
class PayPalService
{
.
.
.
/**
* @des 回撥驗籤
* @author Mars
* @param Request $request
* @param $webHookId
* @return VerifyWebhookSignature|bool
*/
public function verify(Request $request, $webHookId = null)
{
try {
$headers = $request->header();
$headers = array_change_key_case($headers, CASE_UPPER);
$content = $request->getContent();
$signatureVerification = new VerifyWebhookSignature();
$signatureVerification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO'][0]);
$signatureVerification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID'][0]);
$signatureVerification->setCertUrl($headers['PAYPAL-CERT-URL'][0]);
$signatureVerification->setWebhookId($webHookId ?: $this->notifyWebHookId);
$signatureVerification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG'][0]);
$signatureVerification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME'][0]);
$signatureVerification->setRequestBody($content);
$result = clone $signatureVerification;
$output = $signatureVerification->post($this->apiContext);
if ($output->getVerificationStatus() == "SUCCESS") {
return $result;
}
throw new HttpException(400, 'Verify Failed.');
} catch (HttpException $e) {
Log::error('PayPal Notification Verify Failed', ['msg' => $e->getMessage(), 'code' => $e->getStatusCode(), 'data' => ['request' => ['header' => $headers, 'body' => $content]]]);
return false;
}
}
}
非同步回撥
app/Http/Controllers/PaymentController.php
<?php
.
.
.
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class PaymentController extends Controller
{
.
.
.
/**
* @des PayPal-Checkout-Notify
* @author Mars
* @param Request $request
* @return string
*/
public function payPalNotify(Request $request)
{
// 這裡記錄下日誌, 本地測試回撥時會用到
Log::info('PayPal Checkout Notification', ['request' => ['header' => $request->header(), 'body' => $request->getContent()]]);
$response = app('paypal')->verify($request, config('paypal.live.checkout_notify_web_hook_id'));
// 驗證失敗
if (!$response) {
return 'fail';
}
// 回撥包的請求體
$data = json_decode($response->request_body, true);
// 驗證回撥事件型別和狀態
if (Arr::get($data, 'event_type') == 'PAYMENTS.PAYMENT.CREATED' && strcasecmp(Arr::get($data, 'resource.state'), 'CREATED') == 0) {
// 包中會有買家的資訊
$payerInfo = Arr::get($data, 'resource.payer.payer_info');
// TODO: 這裡寫具體的支付完成後的流程(如: 更新訂單的付款時間、狀態 & 增加商品銷量 & 傳送郵件業務 等)
.
.
.
return 'success';
}
return 'fail';
}
}
建立路由
route/web.php
<?php
.
.
.
// PayPal-Checkout
Route::get('payment/{order}/paypal', 'PaymentController@payByPayPalCheckout')
->name('payment.paypal_checkout');
// PayPal-Checkout-Return
Route::get('payment/paypal/return', 'PaymentController@payPalReturn')
->name('payment.paypal.return');
// PayPal-Checkout-Notify
Route::post('payment/paypal/notify', 'PaymentController@payPalNotify')
->name('payment.paypal.notify');
由於非同步回撥是POST請求, 因為Laravel的CSRF機制, 所以我們需要在相應的中介軟體中將其路由加入到白名單中才能被PayPal訪問.
app/Http/MiddlewareVerifyCsrfToken.php
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
.
.
.
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
// PayPal-Checkout-Notify
'payment/paypal/notify',
];
}
設定PayPal-WebHookEvent
開啟PayPal開發者中心進行配置
以沙箱環境為例, 生產一樣
沒有賬號的新建一個, 如果有就點進去, 拉至最下面, 點選Add Webhook
建立一個事件, 輸入回撥地址 https://yoursite.com/payment/paypal/notify
, 把Payments payment created
勾選, 然後確認即可.
PayPal的回撥地址只支援HTTPS協議, 可以參考下Nginx官方給的配置HTTPS方法, 耐心照著步驟一步一步來很好配, 這裡不做贅述.
PayPal提供的事件型別有很多,
PayPal-Checkout
只用到了Payments payment created
.
配置完記得將Webhook ID
新增到我們專案的配置中!
測試Checkout支付
複製連結瀏覽器訪問
登陸後進行支付. (這裡不得不吐槽, 沙箱環境真的真的真的很慢很慢很慢...)
在開發者中心的沙箱環境中可以一鍵建立測試賬號(支付用個人賬號), 這裡就不做演示了.
從線上的日誌中拿到資料包進行本地測試
請求頭:
在控制器中先列印驗簽結果app/Http/Controllers/PaymentController.php
<?php
namespace App\Http\Controllers;
use App\Events\OrderPaid;
use App\Models\Order;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
class PaymentController extends Controller
{
.
.
.
public function payPalNotify(Request $request)
{
$response = app('paypal')->verify($request, config('paypal.sandbox.checkout_notify_web_hook_id'));
dd($response);
.
.
.
}
}
列印結果如下, 接下來就可以編寫支付成功後的業務程式碼了.
至此, Checkout流程就結束了.
Subscription
建立計劃並啟用計劃
以下方法均參考官方DEMO
app/Services/PayPalService.php
<?php
namespace App\Services;
.
.
.
use PayPal\Api\Plan;
use PayPal\Api\PaymentDefinition;
use PayPal\Api\ChargeModel;
use PayPal\Api\MerchantPreferences;
use PayPal\Api\Patch;
use PayPal\Common\PayPalModel;
use PayPal\Api\PatchRequest;
use PayPal\Api\Agreement;
class PayPalService
{
.
.
.
/**
* @des 建立計劃並啟用計劃
* @author Mars
* @param Order $order
* @return Plan|false
*/
public function createPlan(Order $order)
{
try {
$plan = new Plan();
$plan->setName($order->no)
->setDescription($order->product->title)
->setType('INFINITE'); // 可選(FIXED | INFINITE)
$paymentDefinition = new PaymentDefinition();
$paymentDefinition->setName('Regular Payments')
->setType('REGULAR')
->setFrequency('MONTH') // 設定頻率, 可選(DAY | WEEK | MONTH | YEAR)
// ->setFrequency('DAY')
->setFrequencyInterval($order->product->effective_months) // 設定頻率間隔
->setCycles(0) // 設定週期(如果Plan的Type為FIXED的, 對應這裡填99表示無限期. 或Plan的Type為INFINITE, 這裡設定0)
->setAmount(new Currency([
'value' => $order->product->price, // 價格
'currency' => $order->product->currency // 幣種
]));
// Charge Models 這裡可設定稅和運費等
$chargeModel = new ChargeModel();
$chargeModel->setType('TAX')
// ->setType('SHIPPING')
->setAmount(new Currency([
'value' => $order->product->tax ?? 0,
'currency' => $order->product->currency
]));
$paymentDefinition->setChargeModels([$chargeModel]);
$merchantPreferences = new MerchantPreferences();
// 這裡設定支付成功和失敗的回跳URL
$merchantPreferences->setReturnUrl(route('subscriptions.paypal.return', ['success' => 'true', 'no' => $order->no]))
->setCancelUrl(route('subscriptions.paypal.return', ['success' => 'false', 'no' => $order->no]))
->setAutoBillAmount("yes")
->setInitialFailAmountAction("CONTINUE")
->setMaxFailAttempts("0")
->setSetupFee(new Currency([
'value' => $order->product->price, // 設定第一次訂閱扣款金額***, 預設0表示不扣款
'currency' => $order->product->currency // 幣種
]));
$plan->setPaymentDefinitions([$paymentDefinition]);
$plan->setMerchantPreferences($merchantPreferences);
$output = $plan->create($this->apiContext);
// 啟用計劃
$patch = new Patch();
$value = new PayPalModel('{"state":"ACTIVE"}');
$patch->setOp('replace')
->setPath('/')
->setValue($value);
$patchRequest = new PatchRequest();
$patchRequest->addPatch($patch);
$output->update($patchRequest, $this->apiContext);
$result = Plan::get($output->getId(), $this->apiContext);
if (!$result) {
throw new HttpException(500, 'PayPal Interval Error.');
}
return $result;
} catch (HttpException $e) {
Log::error('PayPal Create Plan Failed', ['msg' => $e->getMessage(), 'code' => $e->getStatusCode(), 'data' => ['order' => ['no' => $order->no]]]);
return false;
}
}
建立訂閱申請
接上面的程式碼 ↑
.
.
.
/**
* @des 建立訂閱申請
* @author Mars
* @param Plan $param
* @param Order $order
* @return string|null
*/
public function createAgreement(Plan $param, Order $order)
{
try {
$agreement = new Agreement();
$agreement->setName($param->getName())
->setDescription($param->getDescription())
->setStartDate(Carbon::now()->addMonths($order->product->effective_months)->toIso8601String()); // 設定下次扣款的時間, 測試的時候可以用下面的 ↓, 第二天扣款
// ->setStartDate(Carbon::now()->addDays(1)->toIso8601String());
$plan = new Plan();
$plan->setId($param->getId());
$agreement->setPlan($plan);
$payer = new Payer();
$payer->setPaymentMethod('paypal');
$agreement->setPayer($payer);
// $request = clone $agreement;
// Please note that as the agreement has not yet activated, we wont be receiving the ID just yet.
$agreement = $agreement->create($this->apiContext);
// ### Get redirect url
// The API response provides the url that you must redirect
// the buyer to. Retrieve the url from the $agreement->getApprovalLink()
// method
$approvalUrl = $agreement->getApprovalLink();
// 跳轉到 $approvalUrl 等待使用者同意
return $approvalUrl;
} catch (HttpException $e) {
Log::error('PayPal Create Agreement Failed', ['msg' => $e->getMessage(), 'code' => $e->getStatusCode(), 'data' => ['plan' => $param]]);
return null;
}
}
執行訂閱
接上面 ↑
.
.
.
/**
* @Des 執行訂閱
* @Date 2019-10-30
* @Author Mars
* @param $token
* @return Agreement|bool
*/
public function executeAgreement($token)
{
try {
$agreement = new Agreement();
$agreement->execute($token, $this->apiContext);
return $agreement;
} catch (HttpException $e) {
Log::error('PayPal Execute Agreement Failed', ['msg' => $e->getMessage(), 'code' => $e->getStatusCode(), 'data' => ['token' => $token]]);
return false;
}
}
控制器呼叫
這裡為了跟Checkout區別開來, 我們新建一個專門負責訂閱的控制器
$ php artisan make:controller SubscriptionsController
<?php
namespace App\Http\Controllers;
use App\Models\Order;
class SubscriptionsController extends Controller
{
/**
* @Des PayPal-CreatePlan
* @Author Mars
* @param Order $order
*/
public function createPlan(Order $order)
{
if ($order->paid_at || $order->closed) {
return json_encode(['code' => 422, 'msg' => 'Order Status Error.', 'url' => '']);
}
// 建立計劃並升級計劃
$plan = app('paypal')->createPlan($order);
if (!$plan) {
return json_encode(['code' => 500, 'msg' => 'Create Plan Failed.', 'url' => ''])
}
// 建立訂閱申請
$approvalUrl = app('paypal')->createAgreement($plan, $order);
if (!$approvalUrl) {
return json_encode(['code' => 500, 'msg' => 'Create Agreement Failed.', 'url' => '']);
}
// 跳轉至PayPal授權訂閱申請的連結
return json_encode(['code' => 201, 'msg' => 'success.', 'url' => $approvalUrl]);
}
}
支付完的回跳方法
app/Http/Controllers/SubscriptionsController.php
<?php
namespace App\Http\Controllers;
.
.
.
use Carbon\Carbon;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
class SubscriptionsController extends Controller
{
.
.
.
/**
* @Des 執行訂閱
* @Author Mars
* @param Request $request
* @return void|\Illuminate\View\View
*/
public function executeAgreement(Request $request)
{
if ($request->has('success') && $request->success == 'true') {
$token = $request->token;
try {
// 執行訂閱
// PayPal\Api\Agreement
$agreement = app('paypal')->executeAgreement($token);
if (!$agreement) {
throw new HttpException(500, 'Execute Agreement Failed');
}
// TODO: 這裡寫支付回跳後的業務, 比如跳轉至訂單詳情頁或訂閱成功頁等
.
.
.
// 這裡舉例
$order = Order::where('no', $request->no)->first();
return view('orders.show', $order);
} catch (HttpException $e) {
return abort($e->getStatusCode(), $e->getMessage());
}
}
return abort(401, '非法請求');
}
非同步回撥
訂閱過程中的回撥事件共有四種, 分別是
Billing plan created
、Billing plan updated
、Billing subscription created
、Billing subscription updated
和Payment sale completed
, 而我們更新本地訂單的業務只需要用到最後一個(Payment sale completed
)即可, 其他的視具體業務而定, 所以我們在建立WebHookEvent
的時候需要跟其他回撥業務區分開來.
app/Http/Controllers/SubscriptionsController.php
<?php
namespace App\Http\Controllers;
.
.
.
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
class SubscriptionsController extends Controller
{
.
.
.
/**
* @Des 訂閱的非同步回撥處理
* @Author Mars
* @param Request $request
* @return string
*/
public function payPalNotify(Request $request)
{
Log::info('PayPal Subscription Notification', ['request' => ['header' => $request->header(), 'body' => $request->getContent()]]);
$response = app('paypal')->verify($request, config('paypal.sanbox.subscription_notify_web_hook_id'));
if (!$response) {
return 'fail';
}
$requestBody = json_decode($response->request_body, true);
$eventType = Arr::get($requestBody, 'event_type');
$resourceState = Arr::get($requestBody, 'resource.state');
if ($eventType == 'PAYMENT.SALE.COMPLETED' && strcasecmp($resourceState, 'completed') == 0) {
$billingAgreementId = Arr::get($requestBody, 'resource.billing_agreement_id');
$billingAgreement = app('paypal')->getBillingAgreement($billingAgreementId);
if (!$billingAgreement) {
return 'fail';
}
// 獲取買家資訊
$payerInfo = $billingAgreement->getPayer()->getPayerInfo();
// 買家地址
$shippingAddress = $billingAgreement->getShippingAddress();
// 收錄買家資訊到使用者表
$email = $payerInfo->getEmail();
$user = User::where('email', $email)->first();
if (!$user) {
$user = User::create([
'email' => $email,
'name' => $payerInfo->getLastName() . ' ' . $payerInfo->getFirstName(),
'password' => bcrypt($payerInfo->getPayerId())
]);
}
// 獲取訂單號(因為我在建立計劃的時候把本地訂單號追加到了description屬性裡, 大家可以視情況而定)
$description = $billingAgreement->getDescription();
$tmp = explode(' - ', $description);
$orderNo = array_pop($tmp);
$order = Order::where('no', $orderNo)->first();
if (!$order) {
return 'fail';
}
// 訂閱續費訂單(如果查到的本地訂單已經付過了且包中的'完成周期數`不是0, 則說明是續費訂單, 本地可以新建一個訂單標記是續費的. 這部分僅供參考, 具體視大家的業務而定)
if ($order->paid_at && $billingAgreement->getAgreementDetails()->getCyclesCompleted() != 0) {
// 產品
$sku = $order->product;
// 新建一個本地訂單
$order = new Order([
'address' => $shippingAddress->toArray(),
'paid_at' => Carbon::now(),
'payment_method' => 'paypal-subscription',
'payment_no' => $billingAgreementId,
'total_amount' => $billingAgreement->getAgreementDetails()
->getLastPaymentAmount()
->getValue(),
'remark' => '訂閱續費訂單 - ' . $billingAgreement->getAgreementDetails()->getCyclesCompleted() . '期',
]);
// 訂單關聯到當前使用者
$order->user()->associate($user);
$order->save();
} else {
// 首次付款
$order->update([
'paid_at' => Carbon::now(),
'payment_method' => 'paypal-subscription',
'payment_no' => $billingAgreementId,
'user_id' => $user->id,
'address' => $shippingAddress->toArray(),
]);
// TODO: 增加銷量、傳送郵件等業務
.
.
.
}
return 'success';
}
return 'fail';
}
}
建立路由
上面的方法中一共需要三個路由, 分別是'建立計劃'、'執行訂閱'、'訂閱付款非同步回撥'
routes\web.php
<?php
.
.
.
// PayPal-Subscription-CreatePlan
Route::get('subscriptions/{order}/paypal/plan', 'SubscriptionsController@createPlan')
->name('subscriptions.paypal.createPlan');
// PayPal-Subscription-Return
Route::get('subscriptions/paypal/return', 'SubscriptionsController@execute')
->name('subscriptions.paypal.return');
// PayPal-Subscription-Notify
Route::post('subscriptions/paypal/notify', 'SubscriptionsController@payPalNotify')
->name('subscriptions.paypal.notify');
同樣的, 不要忘記把非同步回撥路由加入到白名單中
app/Http/MiddlewareVerifyCsrfToken.php
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
.
.
.
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
.
.
.
// PayPal-Subscription-Notify
'subscriptions/paypal/notify',
];
}
設定PayPal-WebHookEvent
同上面提到的設定方法, 我們這裡只將
Payment sale completed
事件勾選即可, 具體過程這裡不再贅述.
測試Subscription
複製連結到瀏覽器開啟, 登陸後如下
訂閱完成.
本地測試非同步回撥
同上面提到的, 這裡不再贅述.
至此, 兩種支付的整個過程就算完結啦. 第一次寫博文, 文中如有不恰當的地方歡迎各位指點.