paypal订阅与google订阅的不同之处:
1. google设置首周优惠时,用户第一次订阅会有优惠,用户退订之后再次订阅就不会执行首周订阅优惠; 而paypal设置首周优惠时,用户的每次取消后订阅都视为新订阅,都满足首周优惠条件
2. paypal订阅成功回调时不会返回 “订阅到期的时间”
订阅流程及创建计划查看:https://blog.csdn.net/weixin_39461487/article/details/125900163
正式环境创建订阅计划地址:
PayPal Subscription | Automate repeat payments | PayPal US
沙盒环境创建订阅计划计划:
2、curl请求并订阅计划
- 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';
- }
- }
- }
官方文档地址: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;
- }
- }
官方文档地址: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];
- }
官方文档地址: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);
- }
- 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);
- }
- }
- $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"
- }
- }
- }
回调需要配置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"}]}
事件: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 验证并记录用户订阅时间...
- }
- }
事件: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) {
- // 此为续订订单,生成新订单并执行其他逻辑
- }
- }
- }
- }
- }
取消订阅流程:
事件: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 取消订阅后续操作...
- }
- }
订阅完成