如何使用 SDK 对 PayPal 定期付款配置文件进行计费

How to Bill a PayPal Recurring Payment Profile with the SDK

我在 .net 中使用 PayPal SDK(我认为它是 'old' 版本,classic?) I have a bunch of recurring payment agreements under my merchant profile (the ones that can be invoiced manually from https://www.paypal.com/ca/cgi-bin/webscr?cmd=_merchant-hub,并列在 Activity -> 所有报告 -> 客户协议 ->在 PayPal 网站上定期付款)。手动为它们开具发票工作正常,但我想自动化。我能够获得要开具发票的定期付款配置文件列表,所以我只是错过了最后一步 - 实际为经常性付款配置文件开具发票。

我试过了

PayPalAPIInterfaceServiceService.BillUser() 

PayPalAPIInterfaceServiceService.BillOutstandingAmount()

而且它们都不起作用。 PayPalAPIInterfaceServiceService.BillUser() returns一个

Agreement Id is not valid

错误(我猜他们正在寻找一种不同类型的计费协议)。 PayPalAPIInterfaceServiceService.BillOutstandingAmount() returns一个

Outstanding balance must be > 0

错误。我想我也许可以使用

设置定期付款的未结余额
PayPalAPIInterfaceServiceService.UpdateRecurringPaymentsProfile()

但是,当在构造函数中将经常性付款配置文件 ID 传递给 UpdateRecurringPaymentsProfileRequestDetailsType 或通过 UpdateRecurringPaymentsProfileRequestDetailsType.ProfileID 设置时,会导致

Profile ID is not valid for this account. Please resubmit request with the correct profile ID.

将UpdateRecurringPaymentsProfileRequestDetailsType.ProfileReference设置为定期付款配置文件 ID 时,错误消息为

The profile ID is invalid

最后,我也试过使用参考交易来计费:

PaymentDetailsType payment = new PaymentDetailsType() { OrderTotal = new BasicAmountType(CurrencyCodeType.USD, amount.ToString()) };
DoReferenceTransactionRequestDetailsType request = new DoReferenceTransactionRequestDetailsType(recurringPaymentId, PaymentActionCodeType.SALE, payment);
var response = service.DoReferenceTransaction(new DoReferenceTransactionReq() { DoReferenceTransactionRequest = new DoReferenceTransactionRequestType(request) });

这导致

Billing Agreement Id or transaction Id is not valid

我运行没主意了!

根据配置文件 ID,invoicing/billing 定期付款配置文件的正确 PayPal SDK 调用是什么?

根据 PayPal 商家技术支持,使用 link 类似

创建的重复性 PayPal 支付协议
https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=###

ID 以“I-”开头不能使用 PayPal API。

相反,必须使用参考交易,它只能使用 PayPal API 创建(而不是如上所示 link 到 'hosted button')。

现在,PayPal 商家帐户默认不启用参考交易;必须打电话或发电子邮件给 PayPal 才能启用它 (some guidance available)。

一旦启用了参考交易(为了启用我的 PayPal 花了大约两周的时间来回,但也许我没有大多数人那么幸运 :^),可以使用 Express Checkout API (等等)来创建参考交易。

参考交易列在可以注册的自动 SFTP RPP 报告中。他们确实出现在商家中心(他们的 ID 以 B- 而不是 I- 开头),并且可以从将鼠标悬停在将鼠标悬停在列表上(查看参考交易详细信息时不会显示付款人电子邮件地址)。还可以在由 returnUrl 调用的代码中使用 GetExpressCheckoutDetails 获取付款人信息,将作为参数传递给 returnUrl 的令牌传递给它(与用于调用 CreateBillingAgreement 的令牌相同)。

这是处理创建 PayPal 参考交易的 php 代码。一个 php 文件即可处理所有问题。我在其中留下了一些(注释掉的)调试代码,以帮助您在需要时逐步完成。

<?php
    require_once( dirname(__FILE__) . '/ppconfig.php' ); 

    /* ppconfig.php should contain something like:
    <?php
    global $ppApiUser, $ppApiPwd, $ppApiSig;
    $ppApiUser = '...';
    $ppApiPwd = '...';
    $ppApiSig = '...';
    ?>    
    */

    //print_r($_GET);
   
    if (!array_key_exists("a", $_GET))
        Intro();
    else switch ($_GET["a"])
    { 
        case "go":
            Setup();   
            break;

        case "cf":
            Confirmed();   
            break;

        case "cx":
            Cancelled();   
            break;

        default:
            Intro();
            break;
    }

    function Intro()
    {
        echo("<p>Please <a href='" . BaseUrl() . "?a=go'>click here</a> to create a PayPal billing agreement.</p>");
    }    

    function Setup()
    {
        $post = [
            'PAYMENTREQUEST_0_PAYMENTACTION' => 'AUTHORIZATION',
            'PAYMENTREQUEST_0_AMT' => '0',
            'PAYMENTREQUEST_0_CURRENCYCODE' => 'USD',
            'L_BILLINGTYPE0' => 'MerchantInitiatedBilling',
            'L_BILLINGAGREEMENTDESCRIPTION0' => 'Monthly Fee',
            'cancelUrl' => BaseUrl() . '?a=cx',
            'returnUrl' => BaseUrl() . '?a=cf'
        ];        

        $post = SetupPostArray($post, 'SetExpressCheckout');
        //echo "<p>query: " . http_build_query($post) . "</p>";

        $parsedResponse = DoCurl($post);
        // example response: Array ( [TOKEN] => EC-9WG24287H6582094R [TIMESTAMP] => 2021-05-17T18:14:09Z [CORRELATIONID] => 37010d4454fac [ACK] => Success [VERSION] => 86 [BUILD] => 55627781 )
           
        if ($parsedResponse['ACK'] === "Success")        
            Redirect("https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=" . $parsedResponse['TOKEN']);
        else
            echo("<p>There was an issue creating your PayPal billing agreement; please forward this to us: SetExpressCheckout response = " . print_r($parsedResponse, true) . "</p>");             
    }    

    function Confirmed()
    {
        //$callerIp = $_SERVER['REMOTE_ADDR'];
        //$callerName = gethostbyaddr ( $callerIp );
        //echo("<p>Caller ip: $callerIp  Caller name: $callerName</p>"); returns my own address

        // sample request url: https://xxx.php?a=cf&token=EC-6F958498XP432134J
        // (paypal parameter same as cancel)

        $details = GetExpressCheckoutDetails($_GET["token"]);
        //echo("<p>Payer email: " . $details['EMAIL'] . "  Payer ID: " . $details['PAYERID'] . "</p>");
        
        $post = SetupPostArray([ 'TOKEN' => $_GET["token"] ], 'CreateBillingAgreement');  
        $parsedResponse = DoCurl($post);
        // sample response to create billing agreement: Array ( [BILLINGAGREEMENTID] => B-4EM76674LS64xxxxx [TIMESTAMP] => 2021-05-17T17:51:59Z [CORRELATIONID] => 3be427e93xxxx [ACK] => Success [VERSION] => 86 [BUILD] => 55627781 )

        if ($parsedResponse['ACK'] === "Success")
        {
            echo("<p>You successfully setup a pre-authorized payment (" . $parsedResponse['BILLINGAGREEMENTID'] . "). Thank you.</p>");                               
        }
        else
            echo("<p>There was an issue creating your PayPal billing agreement; please forward this to us: CreateBillingAgreement = " . print_r($parsedResponse, true) . "</p>");                
    }    

    function Cancelled()
    {
        // sample request url: https://xxx.php?a=cx&token=EC-6F958498XP432134J
        // (paypal parameter same as success)

        echo("<p>It looks like you cancelled the creation of your PayPal pre-authorized payment (how could you!) Please <a href='" . BaseUrl() . "?a=go'>click here</a> to try again.</p>");
    }    

    function GetExpressCheckoutDetails($token)
    {
        $post = SetupPostArray([ 'TOKEN' => $token ], 'GetExpressCheckoutDetails');  
        //echo "<p>GetExpressCheckoutDetails query: " . http_build_query($post) . "</p>";
        $parsedResponse = DoCurl($post);
        // sample response to GetExpressCheckoutDetails:  Array ( [TOKEN] => EC-87S04858V0280572C [BILLINGAGREEMENTACCEPTEDSTATUS] => 1 [CHECKOUTSTATUS] => PaymentActionNotInitiated [TIMESTAMP] => 2021-05-19T15:16:06Z [CORRELATIONID] => f5d17498exxxx [ACK] => Success [VERSION] => 86 [BUILD] => 55627781 [EMAIL] => xxxx@gmail.com [PAYERID] => xxxx [PAYERSTATUS] => verified [FIRSTNAME] => xxx [LASTNAME] => xxx [COUNTRYCODE] => xx [SHIPTONAME] => xxx xxx [SHIPTOSTREET] => xxx Dr [SHIPTOCITY] => xxx [SHIPTOSTATE] => xx [SHIPTOZIP] => xxxx [SHIPTOCOUNTRYCODE] => xx [SHIPTOCOUNTRYNAME] => xxx [ADDRESSSTATUS] => Confirmed [CURRENCYCODE] => USD [AMT] => 0.00 [ITEMAMT] => 0.00 [SHIPPINGAMT] => 0.00 [HANDLINGAMT] => 0.00 [TAXAMT] => 0.00 [INSURANCEAMT] => 0.00 [SHIPDISCAMT] => 0.00 [INSURANCEOPTIONOFFERED] => false [PAYMENTREQUEST_0_CURRENCYCODE] => USD [PAYMENTREQUEST_0_AMT] => 0.00 [PAYMENTREQUEST_0_ITEMAMT] => 0.00 [PAYMENTREQUEST_0_SHIPPINGAMT] => 0.00 [PAYMENTREQUEST_0_HANDLINGAMT] => 0.00 [PAYMENTREQUEST_0_TAXAMT] => 0.00 [PAYMENTREQUEST_0_INSURANCEAMT] => 0.00 [PAYMENTREQUEST_0_SHIPDISCAMT] => 0.00 [PAYMENTREQUEST_0_SELLERPAYPALACCOUNTID] => xxx@xxx.com [PAYMENTREQUEST_0_INSURANCEOPTIONOFFERED] => false [PAYMENTREQUEST_0_SHIPTONAME] => xxx xxx [PAYMENTREQUEST_0_SHIPTOSTREET] => xxx Dr [PAYMENTREQUEST_0_SHIPTOCITY] => xxx [PAYMENTREQUEST_0_SHIPTOSTATE] => xx [PAYMENTREQUEST_0_SHIPTOZIP] => xxxxx [PAYMENTREQUEST_0_SHIPTOCOUNTRYCODE] => xx [PAYMENTREQUEST_0_SHIPTOCOUNTRYNAME] => xxx [PAYMENTREQUEST_0_ADDRESSSTATUS] => Confirmed [PAYMENTREQUESTINFO_0_ERRORCODE] => 0 )

        //echo("<p>GetExpressCheckoutDetails response: " . print_r($parsedResponse, true) . "</p>");                

        return $parsedResponse;        
    }
    
    function SetupPostArray($post, $method)
    {
        global $ppApiUser, $ppApiPwd, $ppApiSig; 
        $post['USER'] = $ppApiUser;
        $post['PWD'] = $ppApiPwd;
        $post['SIGNATURE'] = $ppApiSig;
        $post['METHOD'] = $method; 
        $post['VERSION'] = '86';
        return $post;
    }

    function DoCurl($post)
    {
        $ch = curl_init("https://api-3t.paypal.com/nvp");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));    
        $response = curl_exec($ch);
        curl_close($ch);    

        //echo("<p>Raw response: '$response'</p>");
        // example response: Array ( [TOKEN] => EC-9WG24287H6582094R [TIMESTAMP] => 2021-05-17T18:14:09Z [CORRELATIONID] => 37010d445xxxx [ACK] => Success [VERSION] => 86 [BUILD] => 55627781 )

        parse_str($response, $parsedResponse);

        return $parsedResponse;
    }

    function Redirect($url)
    {
        echo "<script type='text/javascript'> 
        window.location = '$url'; 
        </script>";        
    }

    function BaseUrl()
    {
        //return strtok($_SERVER["REQUEST_URI"], '?'); // see 
        return strtok("https://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]", '?'); // see whosebug.com/a/6975045, whosebug.com/a/6768831
    }
?>