繁体   English   中英

如何通过 webhook 在 PayPal API 中确认每月订阅的计费?

[英]How to confirm billing of a monthly subscription in PayPal API via webhook?

我正在尝试实现PayPal 订阅流程,其中用户单击我通过仪表板创建的PayPal 订阅按钮

在后端,我监听订阅计费成功时触发的PAYMENT.SALE.COMPLETED webhook。 不幸的是,webhook 没有向我发送太多信息,因此我可以在我的数据库中检索与刚刚计费的订阅相关联的用户和项目。
这将允许我安全地向该用户显示私人内容

以下是 payPal 发送的webhook 内容(见长见谅):

const response = {
        id: 'WH-4W487015EX264720U-32N35125TV248784B',
        event_version: '1.0',
        create_time: '2021-04-26T08:24:41.436Z',
        resource_type: 'sale',
        event_type: 'PAYMENT.SALE.COMPLETED',
        summary: 'Payment completed for EUR 6.9 EUR',
        resource: {
            billing_agreement_id: 'I-T2HP99MJTS1T',
            amount: {
                total: '6.90',
                currency: 'EUR',
                details: {
                    subtotal: '6.90'
                }
            },
            payment_mode: 'INSTANT_TRANSFER',
            update_time: '2021-04-26T08:23:59Z',
            create_time: '2021-04-26T08:23:59Z',
            protection_eligibility_type: 'ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE',
            transaction_fee: {
                currency: 'EUR',
                value: '0.48'
            },
            protection_eligibility: 'ELIGIBLE',
            links: [
                {
                    method: 'GET',
                    rel: 'self',
                    href: 'https://api.sandbox.paypal.com/v1/payments/sale/6R7481343K8159132'
                },
                {
                    method: 'POST',
                    rel: 'refund',
                    href: 'https://api.sandbox.paypal.com/v1/payments/sale/6R7481343K8159132/refund'
                }
            ],
            id: '6R7481343K8159132',
            state: 'completed',
            invoice_number: ''
        },
        links: [
            {
                href: 'https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4W487015EX264720U-32N35125TV248784B',
                rel: 'self',
                method: 'GET'
            },
            {
                href: 'https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4W487015EX264720U-32N35125TV248784B/resend',
                rel: 'resend',
                method: 'POST'
            }
        ],
    }

我试图GET /v1/payments/sale/:id但它没有给我带来太多信息。

我还检查了有关该主题的其他堆栈溢出线程,但没有任何帮助。 我也不想使用前端 SDK 中提供的成功回调,因为它们不如 webhook 安全(连接可以在触发回调之前关闭, 请参阅此 gitlab 问题

我如何知道用户已为其订阅付费?

我们终于找到了一种解决方法,让我们的后端检索买家和商品。

前端

在订阅按钮代码上,经过多次尝试/错误后,我们注意到createSubscription方法接受承诺,并且我们可以使用它在付款继续之前将 subscriptionId 发送到后端:

paypal.Buttons({
    style: {...},
    createSubscription: function (data, actions) {
        return actions.subscription.create({
            /* Creates the subscription */
            plan_id: 'P-26J60279VA924454WMCBPBSA',
        }).then(subscriptionId => { // subscriptionId == I-9DH5L3A3JAEB
            return new Promise((res, rej) => {
                // here we send the subscriptionId to the back-end
                // and create a pending subscription
                const body = {subscriptionId, userId, itemId};
                apiCall('POST', '/subscription', body,() => {
                    // allow to return subscriptionId to paypal
                    resolve(subscriptionId); 
                })
            });
        });
    },
    onApprove: function (data, actions) {
       // this function was of NO USE
       // it is not safe to call your backend here
       // as connexion can close and paypal doesn't
       // wait after this function to capture payment
       // thus leading to orphaned subscriptions 
       // (paid but not linked to your backend)
    },
}).render('#paypal-button');

后端(webhook 处理程序)

后端等待确认 webhook,其中webhookResponse.resource.billing_agreement_id是订阅 ID ,并允许验证先前创建的订阅。 我不完全明白为什么billing_agreement_id没有命名为subscrition_id ...

如果不清楚,请告诉我。 我让它作为答案,直到有更好的方法来做到这一点:)

这是我创建和验证 Paypal 订阅付款的方法。

首先按照 Paypal 开发人员站点中的集成订阅步骤进行操作。

客户端

html

<script src="https://www.paypal.com/sdk/js?client-id=<YOUR CLIENT ID>&vault=true&intent=subscription"></script>
<div id="paypal-button-container"></div>

您可以使用以下代码段从 PayPal 获取数据:

Javascript

           paypal.Buttons({
                createSubscription: function( data, actions ) {
                    return actions.subscription.create({
                        'plan_id': '<YOUR SUBSCRIPTION PLAN>' // Creates the subscription
                    });
                },
                onApprove: function( data, actions ) {
                    finalize( data, actions );
                }
            }).render( '#paypal-button-container' ); // Renders the PayPal button

            const finalize = async ( data, actions ) => {
                const rawResponse = await fetch( '/api/paypal-subscription.php', {
                    method: 'POST',
                    headers: {
                        'Accept': 'application/json',
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ data: data, actions: actions })
                });
                const results = await rawResponse.json();
                if ( results.error == 0 ){
                    console.log( "payment created" );
                } else {
                    console.log( 'Errore creazione agenzia' );
                };
            };

确认订阅后,会触发 onApprove 事件。 在 function 内部,您可以调用另一个 function 来完成订阅过程。 function 有两个 object:数据和动作。

在数据 object 中,您有一个订阅 ID,它引用订阅的唯一 ID。 您必须将此 ID 与链接到它的订阅买家一起保存(例如:通过使用 ajax 在服务器上调用 php 文件保存到数据库)。

服务器端 Webhook

在服务器端,您可以从 PayPal 获取数据。 您必须在开发人员仪表板中设置 webhook 调用以执行以下操作(如果需要,您可以 select 更多或所有事件)。 BILLING.SUBSCRIPTION.CREATEDBILLING.SUBSCRIPTION.ACTIVATED和定期付款PAYMENT.SALE.COMPLETED

<?php
$data = json_decode( file_get_contents( "php://input" ), true );
$data = $data['resource'];

if ( !array_key_exists( 'billing_agreement_id', $data ) ) {
    // Not a payment for a billing agreement
    // handle single payments or:
    die();
};
?>

请记住:webhooks 模拟器不会填充billing_agreement_id ,即携带 subscriptioID 的键,在其他 Webhooks 调用中称为 id。 我建议在沙盒中创建一个订阅,该订阅具有每日 FREQUENCY 和一 (1) 天的间隔。 通过此订阅, PAYMENT.SALE.COMPLETED将立即被触发。 PAYMENT.SALE.COMPLETED调用中找到的关键是billing_agreement_id

验证 Paypal webhook 通知

您还必须验证通知的真实性:

<php
// get request headers
$headers = apache_request_headers();

// get http payload
$body = file_get_contents( 'php://input' );

// compose signature string: The third part is the ID of the webhook ITSELF(!),
// NOT the ID of the webhook event sent. You find the ID of the webhook
// in Paypal's developer backend where you have created the webhook
$data =
    $headers['Paypal-Transmission-Id'] . '|' .
    $headers['Paypal-Transmission-Time'] . '|' .
    '<WEBHOOK ID FROM THE DEVELOPER DASHBOARD>' . '|' . crc32( $body );


// load certificate and extract public key
$pubKey = openssl_pkey_get_public( file_get_contents( $headers['Paypal-Cert-Url'] ) );
$key = openssl_pkey_get_details( $pubKey )['key'];

// verify data against provided signature 
$result = openssl_verify(
    $data,
    base64_decode( $headers['Paypal-Transmission-Sig'] ),
    $key, 'sha256WithRSAEncryption'
);


if ( $result == 1 ) {
    // webhook notification is verified
} elseif ( $result == 0 ) {
    // webhook notification is NOT verified
} else {
    // there was an error verifying this
};
?>

传输 id、传输日期、webhook id 和 HTTP 主体上的 CRC。 前两个可以在请求的 header 中找到,开发者后端的 webhook id(当然,那个 id 永远不会改变),CRC 计算如下所示。

证书的位置也在 header 中,所以我们加载它并提取私钥。

最后需要注意的是:Paypal(同样在 header 字段中)提供的算法名称与 PHP 所理解的不完全相同。 Paypal 将其称为“sha256WithRSA”,但 openssl_verify 将期望“sha256WithRSAEncryption”。 您可以在此处阅读有关验证过程的更多信息

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM