paypal订阅与google订阅的不同之处:
1. google设置首周优惠时,用户第一次订阅会有优惠,用户退订之后再次订阅就不会执行首周订阅优惠; 而paypal设置首周优惠时,用户的每次取消后订阅都视为新订阅,都满足首周优惠条件
2. paypal订阅成功回调时不会返回 “订阅到期的时间”
订阅流程及创建计划查看:https://blog.csdn.net/weixin_39461487/article/details/125900163
1、后台创建订阅计划
正式环境创建订阅计划地址:
PayPal Subscription | Automate repeat payments | PayPal US
沙盒环境创建订阅计划计划:
- 对于您的一个沙盒业务帐户-->https://www.sandbox.paypal.com/billing/plans
- 对于您的live帐户-->https://www.paypal.com/billing/plans
2、curl请求并订阅计划
2.1 初始化参数
class PaypalService extends Service
{
public $baseUrl;
public $paypalParams;
public $access_token;
public function __construct($config = [])
{
parent::__construct();
$this->paypalParams = [
'return_url' => '支付成功跳转地址',
'cancel_url' => '支付失败跳转地址',
'client_id' => '',
'client_secret' => '',
];
if (!'test') {
$this->baseUrl = 'https://api.paypal.com';
} else {
$this->baseUrl = 'https://api.sandbox.paypal.com';
}
}
}
2.2 获取token
官方文档地址:https://developer.paypal.com/api/rest/authentication/
public function getToken($is_expires=false)
{
$redis = new RedisStore();
$key = 'paypal_token';
$token = $redis->get($key);
if($token && !$is_expires) {
$this->access_token = $token;
return true;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->baseUrl . "/v1/oauth2/token");
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERPWD, $this->paypalParams['client_id'] . ':' . $this->paypalParams['client_secret']);
curl_setopt($ch, CURLOPT_POSTFIELDS, "grant_type=client_credentials");
$result = curl_exec($ch);
if (empty($result)) {
curl_close($ch);
return false;
} else {
curl_close($ch);
$result = json_decode($result);
$redis->set($key, $result->access_token);
$redis->expireAt($key, $result->expires_in - 300);
$this->access_token = $result->access_token;
return true;
}
}
2.3 创建订阅
官方文档地址:https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_create
public function createSubscriptions($subscriptions_plan_id, $start_time = null, $is_expires=false)
{
if(!$this->access_token) $this->getToken();
if ($start_time === null) $start_time = date('c', time()+5);
$subscriptionsData = [
"plan_id" => $subscriptions_plan_id,
"start_time" => $start_time,
"quantity" => "1",
"auto_renewal" => 'true',
"application_context" => [
// "brand_name" => "Your brand name",
"locale" => "en-US",
"shipping_preference" => "NO_SHIPPING",
"user_action" => "SUBSCRIBE_NOW",
"payment_method" => [
"payer_selected" => "PAYPAL",
"payee_preferred" => "IMMEDIATE_PAYMENT_REQUIRED"
],
"return_url" => $this->paypalParams['return_url'],
"cancel_url" => $this->paypalParams['cancel_url'],
]
];
$ch1 = curl_init();
curl_setopt($ch1, CURLOPT_URL, $this->baseUrl . "/v1/billing/subscriptions");
curl_setopt($ch1, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch1, CURLOPT_POSTFIELDS, json_encode($subscriptionsData));
curl_setopt($ch1, CURLOPT_POST, true);
curl_setopt($ch1, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $this->access_token,
'Accept: application/json',
'Content-Type: application/json'
]);
$result = json_decode(curl_exec($ch1));
curl_close($ch1);
if($result->error == 'invalid_token') {
// token 失效时重新获取token
$this->getToken(true);
return $this->createSubscriptions($subscriptions_plan_id, $start_time, true);
}
if($result->debug_id) {
return ['code' => false, 'msg' => $result->details[0]->description];
}
return ['code' => true, 'url' => $result->links[0]->href, 'agreement_id' => $result->id];
}
2.4 查询用户订阅详情
官方文档地址:https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_get
public function getSubscriptionDetails($subscription_id, $is_expires=false)
{
if(!$this->access_token) $this->getToken();
$ch1 = curl_init();
curl_setopt($ch1, CURLOPT_URL, $this->baseUrl . "/v1/billing/subscriptions/" . $subscription_id);
curl_setopt($ch1, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch1, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $this->access_token,
'Accept: application/json',
'Content-Type: application/json',
]);
$result = json_decode(curl_exec($ch1));
curl_close($ch1);
if($result->error == 'invalid_token') {
// token 失效时重新获取token
$this->getToken(true);
return $this->getSubscriptionDetails($subscription_id, true);
}
return json_decode(json_encode($result), true);
}
2.5 封装完整代码
class PaypalService extends Service
{
public $baseUrl;
public $paypalParams;
public $access_token;
public function __construct($config = [])
{
parent::__construct();
$this->paypalParams = [
'return_url' => '支付成功跳转地址',
'cancel_url' => '支付失败跳转地址',
'client_id' => '',
'client_secret' => '',
];
if (!'test') {
$this->baseUrl = 'https://api.paypal.com';
} else {
$this->baseUrl = 'https://api.sandbox.paypal.com';
}
}
/**
* 获取token
* @param false $is_expires 是否强制更新token
* https://developer.paypal.com/api/rest/authentication/
* @return false|mixed
*/
public function getToken($is_expires=false)
{
$redis = new RedisStore();
$key = 'paypal_token';
$token = $redis->get($key);
if($token && !$is_expires) {
$this->access_token = $token;
return true;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->baseUrl . "/v1/oauth2/token");
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERPWD, $this->paypalParams['client_id'] . ':' . $this->paypalParams['client_secret']);
curl_setopt($ch, CURLOPT_POSTFIELDS, "grant_type=client_credentials");
$result = curl_exec($ch);
if (empty($result)) {
curl_close($ch);
return false;
} else {
curl_close($ch);
$result = json_decode($result);
$redis->set($key, $result->access_token);
$redis->expireAt($key, $result->expires_in - 300);
$this->access_token = $result->access_token;
return true;
}
}
/**
* 创建订阅
* @param $subscriptions_plan_id
* @param null $start_time
* @param false $is_expires 是否强制更新token
* https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_create
* @return array
*/
public function createSubscriptions($subscriptions_plan_id, $start_time = null, $is_expires=false)
{
if(!$this->access_token) $this->getToken();
if ($start_time === null) $start_time = date('c', time()+5);
$subscriptionsData = [
"plan_id" => $subscriptions_plan_id,
"start_time" => $start_time,
"quantity" => "1",
"auto_renewal" => 'true',
"application_context" => [
// "brand_name" => "Your brand name",
"locale" => "en-US",
"shipping_preference" => "NO_SHIPPING",
"user_action" => "SUBSCRIBE_NOW",
"payment_method" => [
"payer_selected" => "PAYPAL",
"payee_preferred" => "IMMEDIATE_PAYMENT_REQUIRED"
],
"return_url" => $this->paypalParams['return_url'],
"cancel_url" => $this->paypalParams['cancel_url'],
]
];
$ch1 = curl_init();
curl_setopt($ch1, CURLOPT_URL, $this->baseUrl . "/v1/billing/subscriptions");
curl_setopt($ch1, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch1, CURLOPT_POSTFIELDS, json_encode($subscriptionsData));
curl_setopt($ch1, CURLOPT_POST, true);
curl_setopt($ch1, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $this->access_token,
'Accept: application/json',
'Content-Type: application/json'
]);
$result = json_decode(curl_exec($ch1));
curl_close($ch1);
if($result->error == 'invalid_token') {
// token 失效时重新获取token
$this->getToken(true);
return $this->createSubscriptions($subscriptions_plan_id, $start_time, true);
}
if($result->debug_id) {
return ['code' => false, 'msg' => $result->details[0]->description];
}
return ['code' => true, 'url' => $result->links[0]->href, 'agreement_id' => $result->id];
}
/**
* 获取订阅计划详情
* @param $subscription_id
* @param false $is_expires 是否强制获取token
* https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_get
* @return mixed
*/
public function getSubscriptionDetails($subscription_id, $is_expires=false)
{
if(!$this->access_token) $this->getToken();
$ch1 = curl_init();
curl_setopt($ch1, CURLOPT_URL, $this->baseUrl . "/v1/billing/subscriptions/" . $subscription_id);
curl_setopt($ch1, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch1, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $this->access_token,
'Accept: application/json',
'Content-Type: application/json',
]);
$result = json_decode(curl_exec($ch1));
curl_close($ch1);
if($result->error == 'invalid_token') {
// token 失效时重新获取token
$this->getToken(true);
return $this->getSubscriptionDetails($subscription_id, true);
}
return json_decode(json_encode($result), true);
}
}
3、订阅支付
$subscription = $this->createSubscriptions('计划id');
if($subscription['code'] === false) {
return [
'code' => ErrorCode::EC_UNKNOWN,
'msg' => 'Paypal Subscriptions Error: ' . $subscription['msg'],
'data' => [],
];
}
// 购买商品下单流程...
// 成功返回的结果
// 'url' => $subscription['url'],
// 'agreement_id' => $subscription['agreement_id'],
返回结果:
id:paypal生成的订阅ID
links[0]['href']:去支付的链接
object(stdClass)#189 (4) {
["status"]=>
string(16) "APPROVAL_PENDING"
["id"]=>
string(14) "I-BU64KX8FTG3C"
["create_time"]=>
string(20) "2022-08-09T05:32:29Z"
["links"]=>
array(3) {
[0]=>
object(stdClass)#219 (3) {
["href"]=>
string(90) "https://www.sandbox.paypal.com/webapps/billing/subscriptions?ba_token=BA-3P5459826W8205613"
["rel"]=>
string(7) "approve"
["method"]=>
string(3) "GET"
}
[1]=>
object(stdClass)#220 (3) {
["href"]=>
string(70) "https://api.sandbox.paypal.com/v1/billing/subscriptions/I-BU64KX8FTG3C"
["rel"]=>
string(4) "edit"
["method"]=>
string(5) "PATCH"
}
[2]=>
object(stdClass)#221 (3) {
["href"]=>
string(70) "https://api.sandbox.paypal.com/v1/billing/subscriptions/I-BU64KX8FTG3C"
["rel"]=>
string(4) "self"
["method"]=>
string(3) "GET"
}
}
}
4、订阅回调
回调需要配置webhook及回调地址
事件说明:Webhook event names
public function actionPaypal()
{
$payload = @file_get_contents('php://input');
$content = "=========".date('Y-m-d H:i:s',time())."==========\r\n";
file_put_contents('paypal_success.log', $content . $payload . "\r\n",FILE_APPEND);
$content = json_decode($payload);
}
首次订阅成功会回调两个事件:
1.BILLING.SUBSCRIPTION.ACTIVATED
=========2022-08-02 05:58:58==========
{"id":"WH-2JK13221SH228131A-85M888883CF992091V","event_version":"1.0","create_time":"2022-08-02T09:58:37.896Z","resource_type":"subscription","resource_version":"2.0","event_type":"BILLING.SUBSCRIPTION.ACTIVATED","summary":"Subscription activated","resource":{"quantity":"1","subscriber":{"email_address":"wwww@personal.example.com","payer_id":"BNMR2YZQJ9Q2L","name":{"given_name":"John","surname":"Doe"}},"create_time":"2022-08-02T09:58:05Z","plan_overridden":false,"shipping_amount":{"currency_code":"USD","value":"0.0"},"start_time":"2022-08-02T09:57:17Z","update_time":"2022-08-02T09:58:06Z","billing_info":{"outstanding_balance":{"currency_code":"USD","value":"0.0"},"cycle_executions":[{"tenure_type":"TRIAL","sequence":1,"cycles_completed":1,"cycles_remaining":0,"current_pricing_scheme_version":1,"total_cycles":1},{"tenure_type":"REGULAR","sequence":2,"cycles_completed":0,"cycles_remaining":0,"current_pricing_scheme_version":1,"total_cycles":0}],"last_payment":{"amount":{"currency_code":"USD","value":"0.99"},"time":"2022-08-02T09:58:05Z"},"next_billing_time":"2022-08-03T10:00:00Z","failed_payments_count":0},"links":[{"href":"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-DL5L53KM3N/cancel","rel":"cancel","method":"POST","encType":"application/json"},{"href":"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-DL5L53KM3N","rel":"edit","method":"PATCH","encType":"application/json"},{"href":"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-DL5L53KM3N","rel":"self","method":"GET","encType":"application/json"},{"href":"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-DL5L53KM3N/suspend","rel":"suspend","method":"POST","encType":"application/json"},{"href":"https://api.sandbox.paypal.com/v1/billing/subscriptions/I-DL5L53KM3N/capture","rel":"capture","method":"POST","encType":"application/json"}],"id":"I-DL5L53KM3N","plan_id":"P-4G238388SD824MLUPJNY","status":"ACTIVE","status_update_time":"2022-08-02T09:58:06Z"},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-2JK13221SH228131A-85M80963CF992091V","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-2JK13221SH228131A-85M80963CF992091V/resend","rel":"resend","method":"POST"}]}
2. PAYMENT.SALE.COMPLETED
=========2022-08-02 05:58:58==========
{"id":"WH-4AU27166UL181633V-3BU78225VU943751T","event_version":"1.0","create_time":"2022-08-02T09:58:43.670Z","resource_type":"sale","event_type":"PAYMENT.SALE.COMPLETED","summary":"Payment completed for $ 0.99 USD","resource":{"billing_agreement_id":"I-DL5L53KM3N","amount":{"total":"0.99","currency":"USD","details":{"subtotal":"0.99"}},"payment_mode":"INSTANT_TRANSFER","update_time":"2022-08-02T09:58:05Z","create_time":"2022-08-02T09:58:05Z","protection_eligibility_type":"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE","transaction_fee":{"currency":"USD","value":"0.33"},"protection_eligibility":"ELIGIBLE","links":[{"method":"GET","rel":"self","href":"https://api.sandbox.paypal.com/v1/payments/sale/4DN9460xxx61205Y"},{"method":"POST","rel":"refund","href":"https://api.sandbox.paypal.com/v1/payments/sale/4DN9460xxx61205Y/refund"}],"id":"4DN9460xxx61205Y","state":"completed","invoice_number":""},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4AU27166UL181633V-3BU78225VU943751T","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4AU27166UL181633V-3BU78225VU943751T/resend","rel":"resend","method":"POST"}]}
续订时只会回调:PAYMENT.SALE.COMPLETED
=========2022-08-03 06:25:06==========
{"id":"WH-4AU27166UL181633V-3BU78225VU943751T","event_version":"1.0","create_time":"2022-08-02T09:58:43.670Z","resource_type":"sale","event_type":"PAYMENT.SALE.COMPLETED","summary":"Payment completed for $ 0.99 USD","resource":{"billing_agreement_id":"I-DL5L53KM3N","amount":{"total":"0.99","currency":"USD","details":{"subtotal":"0.99"}},"payment_mode":"INSTANT_TRANSFER","update_time":"2022-08-02T09:58:05Z","create_time":"2022-08-02T09:58:05Z","protection_eligibility_type":"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE","transaction_fee":{"currency":"USD","value":"0.33"},"protection_eligibility":"ELIGIBLE","links":[{"method":"GET","rel":"self","href":"https://api.sandbox.paypal.com/v1/payments/sale/4DN9460xxx61205Y"},{"method":"POST","rel":"refund","href":"https://api.sandbox.paypal.com/v1/payments/sale/4DN9460xxx61205Y/refund"}],"id":"4DN9460xxx61205Y","state":"completed","invoice_number":""},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4AU27166UL181633V-3BU78225VU943751T","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4AU27166UL181633V-3BU78225VU943751T/resend","rel":"resend","method":"POST"}]}
4.1 订阅创建回调
事件:BILLING.SUBSCRIPTION.ACTIVATED
if($content->resource_type == 'subscription') {
// 新建订阅
if($content->event_type == 'BILLING.SUBSCRIPTION.ACTIVATED') {
// 验证订单信息
$agreement_id = $content->resource->id;
$goodId = $content->resource->plan_id;
// 支付价格
$price = $content->resource->billing_info->last_payment->amount->value;
// 创建时间
$start_time = strtotime(str_replace('T', ' ', trim($content->resource->create_time, 'Z')));
// 状态更新时间
$update_time = strtotime(str_replace('T', ' ', trim($content->resource->update_time, 'Z')));
// todo 验证并记录用户订阅时间...
}
}
4.2 支付成功或续订成功回调
事件:PAYMENT.SALE.COMPLETED
因订阅回调与支付成功回调不知道哪个先后,所以此处订单表内记录了两个订阅的字段,agreement_id:创建订阅时返回的id,out_trade_no:订单编号
如果此支付成功回调与订阅回调时间小于1小时,证明此支付成功回调与订阅回调为同一订单,我再处理默认首次订阅处理回调走BILLING.SUBSCRIPTION.ACTIVATED
if($content->resource_type == 'sale') {
if($content->event_type == 'PAYMENT.SALE.COMPLETED') {
$agreement_id = $content->resource->billing_agreement_id; // 若是订阅,此字段为同意协议id,
$payment_intent_new = $content->resource->id; // 订单id
$update_time = strtotime(str_replace('T', ' ', trim($content->resource->update_time, 'Z')));
// 支付价格
$price = $content->resource->amount->total;
if($agreement_id) {
// 因订阅回调与支付成功回调不知道哪个先后,根据agreement_id查询首订阅订单,如果存在后续验证,不存在说明此回调在前,订阅成功回调在后,首订阅默认执行订阅成功回调
$objTradeInfoOld = VipBuy::find()->andWhere(['agreement_id' => $agreement_id, 'status' => VipBuy::STATUS_SUCCESS])->one();
if(objTradeInfoOld) {
if($update_time - $objTradeInfoOld->notify_time <= 3600) {
// 如果此支付成功回调与订阅回调时间小于1小时,证明此支付成功回调与订阅回调为同一订单
return true;
}
$objTradeInfo = VipBuy::find()->andWhere(['out_trade_no' => $payment_intent_new, 'goods_id' => $objTradeInfoOld->goods_id, 'product' => $product])->one();
if($objTradeInfo->status == VipBuy::STATUS_SUCCESS) return true;
if(!$objTradeInfo) {
// 此为续订订单,生成新订单并执行其他逻辑
}
}
}
}
}
4.3 取消订阅回调
取消订阅流程:
事件:BILLING.SUBSCRIPTION.CANCELLED
if($content->resource_type == 'subscription') {
// 取消订阅
if($content->event_type == 'BILLING.SUBSCRIPTION.CANCELLED' && $content->resource->status == 'CANCELLED') {
$payment_intent = $content->resource->id;
$goodId = $content->resource->plan_id;
// 状态更新时间,可记录为用户取消订阅时间
$update_time = strtotime(str_replace('T', ' ', trim($content->resource->status_update_time, 'Z')));
// todo 取消订阅后续操作...
}
}
订阅完成