如何通过 IPN 使用 Paypal 自适应支付?

Posted

技术标签:

【中文标题】如何通过 IPN 使用 Paypal 自适应支付?【英文标题】:How to use Paypal adaptive payments with IPN? 【发布时间】:2012-05-05 13:31:52 【问题描述】:

我正在使用 Paypal 的自适应支付系统。使用沙盒帐户,我能够发出 PayRequest 并被转发到 Paypal 进行付款。 然后看起来像:

请求=

2012 年 4 月 24 日晚上 10:35:46 com.paypal.adaptive.api.requests.PayRequest 执行 信息:发送 PayRequest 时使用:requestEnvelope.errorLanguage=en_US&actionType=PAY&receiverList.receiver(0).email=seller_1334320690_biz%40email.org&receiverList.receiver(0).amount=5.0&currencyCode=EUR&feesPayer=SENDER&cancelUrl=https%3A% 2F%2Flocalhost%3A8443&returnUrl=http%3A%2F%2Flocalhost%2F&ipnNotificationUrl=http%3A%2F%2Flocalhostu%2Ffinishdeposit&

响应=

2012 年 4 月 24 日晚上 10:35:48 com.paypal.adaptive.api.requests.PayPalBaseRequest makeRequest 信息:收到的响应:responseEnvelope.timestamp=2012-04-24T13%3A35%3A48.587-07%3A00&responseEnvelope.ack=Success&responseEnvelope.correlationId=c8dee8023cca6&responseEnvelope.build=2756816&payKey=AP-1UF57245CJ360523K&paymentExecStatus=CREATED

我现在想弄清楚,我该如何检查,付款是否成功完成。 所以我尝试实现 ipn 系统,它使用沙盒工具工作。 但是,我不知道如何将 2 连接在一起。即付款时,我假设我需要在数据库中创建该用户已付款的记录,可能为待处理/已创建? 然后等待ipn返回通知我支付成功,更新数据库表说完成? 我如何将 PayRequest 与 IPN 通知相关联,我将从贝宝获得? Paypal 仅使用 IPN 通知发送一些信息,例如:

item_number=AK-1234 residence_country=US verify_sign=ArcmaOINNZx08uC3iQY0zhEQN3IZAz70ynRk93Or8ixRi23bb4rGNIrd address_country=美国 address_city=圣何塞 address_status=未确认 payment_status=已完成 business=seller@paypalsandbox.com payer_id=TESTBUYERID01 first_name=约翰 运费=3.04 payer_email=buyer@paypalsandbox.com mc_fee=0.44 txn_id=484221854 数量=1 receiver_email=seller@paypalsandbox.com notify_version=2.1 txn_type=web_accept test_ipn=1 payer_status=已验证 mc_currency=USD mc_gross=12.34 custom=xyz123 mc_gross_1=9.34 payment_date=11:54:48 太平洋夏令时间 2012 年 4 月 22 日 charset=windows-1252 address_country_code=US address_zip=95131 address_state=CA 税=2.02 item_name=某事 address_name=约翰·史密斯 last_name=史密斯 payment_type=instant address_street=123,任何街道 receiver_id=TESTSELLERID1

我在该 IPN 通知中找不到可用的内容。最好的情况是,如果我可以通过付费响应获得的 IPN 通知获得相同的相关 ID。因此,我可以将 response-correlation-id 保存在我的数据库中,然后检查是否收到具有相同相关 ID 的 IPN 通知。

【问题讨论】:

所以要澄清一下您想使用自适应支付 (PAY) 方法进行支付,然后通过 IPN 消息验证成功? 是的,我想将付款与付款的用户关联起来。 【参考方案1】:

这应该对你有很大帮助。

namespace Gateway

    public class MerchantSellerIPNService : IMerchantSellerIPNService
    
        /// <summary>
        /// This is the method which is hit when using the URL in the PAY request to PayPal.
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public string ProcessIPN(Stream stream)
        
            // Declare locally used variables.
            byte[] requestArray = null;
            string requestString = null;
            string responseString = null;
            StreamReader IPNReturnReader;
            StreamWriter streamWriter;
            MemoryStream responseStream = new MemoryStream();
            HttpWebRequest payPalRequest;
            System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();

            // Get the URL to send the IPN received back to PayPal (use either of the two below depending on the environment.)
            <add key="PAYPAL_IPN_URL" value="https://www.sandbox.paypal.com/cgi-bin/webscr" />
            <add key="PAYPAL_IPN_URL" value="https://www.paypal.com/cgi-bin/webscr"/>

            string IPNReturnURL = ConfigurationManager.AppSettings["PAYPAL_IPN_URL"];

            // Read in the data provided from PayPal
            StreamReader streamReader = new StreamReader(stream);

            // Obtain the email address and pre-approval key passed to use via PayPal for later use.
            string strPayPalMessage = streamReader.ReadToEnd();

            // Initalize the POST web request we are going to send to PayPal to valid the IPN we received from them.
            payPalRequest = (HttpWebRequest)WebRequest.Create(IPNReturnURL);
            payPalRequest.Method = "POST";
            payPalRequest.ContentType = "application/x-www-form-urlencoded";

            // Create an array containing the IPN message PayPal sent to us.
            requestArray = encoding.GetBytes(strPayPalMessage);

            // Then add the necessary string to the back to use for verfication.
            requestString = Encoding.ASCII.GetString(requestArray);
            requestString += "&cmd=_notify-validate";
            payPalRequest.ContentLength = requestString.Length;

            // Now write the updated IPN message back to PayPal for verification.
            streamWriter = new StreamWriter(payPalRequest.GetRequestStream(), System.Text.Encoding.ASCII);
            streamWriter.Write(requestString);
            streamWriter.Close();

            // Read the response from PayPal and process it.
            IPNReturnReader = new StreamReader(payPalRequest.GetResponse().GetResponseStream());
            responseString = IPNReturnReader.ReadToEnd();
            IPNReturnReader.Close();

            if (responseString == "VERIFIED")
            
                try
                
                    if (strPayPalMessage.Contains("payment_status=Completed"))
                    
                        if (ProcessPaymentIPNMessage(strPayPalMessage))
                            PayPalInStore.Utils.ErrorHandler.LogError(new Exception("ProcessPaymentIPNMessage - Able to create new payment Transaction Detail Record"), "DEBUG");
                        else
                            PayPalInStore.Utils.ErrorHandler.LogError(new Exception("ProcessPaymentIPNMessage - Unable to create new payment Transaction Detail Record"), "DEBUG");
                    
                    else if (strPayPalMessage.Contains("payment_status=Refunded"))
                    
                        if (ProcessRefundIPNMessage(strPayPalMessage))
                            PayPalInStore.Utils.ErrorHandler.LogError(new Exception("ProcessRefundIPNMessage - Able to create new refund Transaction Detail Record"), "DEBUG");
                        else
                            PayPalInStore.Utils.ErrorHandler.LogError(new Exception("ProcessRefundIPNMessage - Unable to create new refund Transaction Detail Record"), "DEBUG");
                    
                    else
                    
                        PayPalInStore.Utils.ErrorHandler.LogError(new Exception("MerchantSellerIPNService - ProcessIPN - Unknown message type"), "DEBUG");
                    
                
                catch (Exception ex)
                
                    PayPalInStore.Utils.ErrorHandler.LogError(new Exception("MerchantSellerIPNService - ProcessIPN failed"), "DEBUG");
                
            
            else if (responseString == "INVALID")
            
                PayPalInStore.Utils.ErrorHandler.LogError(new Exception("MerchantSellerIPNService - Invalid IPN Message Received: "), "DEBUG");
            
            else
            
                PayPalInStore.Utils.ErrorHandler.LogError(new Exception("MerchantSellerIPNService - Fatal IPN Message Received: "), "DEBUG");
            

            return "MerchantSellerIPNService Completed";
        

        /// <summary>
        /// Method used to process the Payment IPN notification message and update the database as required.
        /// </summary>
        /// <returns></returns>
        private bool ProcessPaymentIPNMessage(string PayPalIPNMessage)
        
            // Firstly, we need to split the IPN message into sections based on the & sign.
            string[] PayPalMessageElemetsArray = PayPalIPNMessage.Split('&');

            // Now obtain the list of information (from the message) we require to make the TransactionDetail record.
            string merchantTransactionId = Array.Find(PayPalMessageElemetsArray, element => element.StartsWith("txn_id=", StringComparison.Ordinal));
            string feeAmount = Array.Find(PayPalMessageElemetsArray, element => element.StartsWith("mc_fee=", StringComparison.Ordinal));
            string grossAmount = Array.Find(PayPalMessageElemetsArray, element => element.StartsWith("mc_gross=", StringComparison.Ordinal));

            // TODO: REMOVE THIS ITS FOR DEBUGGING PURPOSES
            string errorMessage2 = String.Format("ProcessPaymentIPNMessage - merchantTransactionId: 0, feeAmount: 1, grossAmount: 2", merchantTransactionId, feeAmount, grossAmount);
            PayPalInStore.Utils.ErrorHandler.LogError(new Exception(errorMessage2), "DEBUG");

            try
            
                // We now need to remove the variable name and '=' from the elements so we only have the necessary information.
                merchantTransactionId = merchantTransactionId.Replace("txn_id=", "");
                feeAmount = feeAmount.Replace("mc_fee=", "");
                grossAmount = grossAmount.Replace("mc_gross=", "");

                // Now convert the values obtained from the IPN message and calculate the net amount.
                decimal dFeeAmount = Convert.ToDecimal(feeAmount);
                decimal dGrossAmount = Convert.ToDecimal(grossAmount);
                decimal dNetAmount = Math.Round((dGrossAmount - dFeeAmount), 2);

                try
                
                    // Finally create the new transaction fee record.
                    TransactionDetail transactionDetail = new TransactionDetail();
                    transactionDetail.MerchantTransactionId = merchantTransactionId;
                    transactionDetail.Gross = dGrossAmount;
                    transactionDetail.Fee = Decimal.Negate(dFeeAmount);
                    transactionDetail.Net = dNetAmount;
                    transactionDetail.TransactionType = (int)TransactionDetailTransactionType.InStorePayment;
                    transactionDetail.Save();
                
                catch (Exception ex)
                
                    string errorMessage = String.Format("ProcessPaymentIPNMessage - Unable to create new TransactionDetail record for Merchant Transaction ID: 0", merchantTransactionId);
                    PayPalInStore.Utils.ErrorHandler.LogError(new Exception(errorMessage), "DEBUG");
                    return false;
                

                return true;
            
            catch (Exception ex)
            
                string errorMessage = String.Format("ProcessPaymentIPNMessage - Unable to create new TransactionDetail record for Merchant Transaction ID: 0", merchantTransactionId);
                PayPalInStore.Utils.ErrorHandler.LogError(new Exception(errorMessage), "DEBUG");
                return false;
            
        

        /// <summary>
        /// Method used to process the Refund IPN notification message and update the database as required.
        /// </summary>
        /// <returns></returns>
        private bool ProcessRefundIPNMessage(string PayPalIPNMessage)
        
            // Firstly, we need to split the IPN message into sections based on the & sign.
            string[] PayPalMessageElemetsArray = PayPalIPNMessage.Split('&');

            // Now obtain the list of information (from the message) we require to make the TransactionDetail record.
            string merchantTransactionId = Array.Find(PayPalMessageElemetsArray, element => element.StartsWith("txn_id=", StringComparison.Ordinal));
            string parentTransactionId = Array.Find(PayPalMessageElemetsArray, element => element.StartsWith("parent_txn_id=", StringComparison.Ordinal));
            string feeAmount = Array.Find(PayPalMessageElemetsArray, element => element.StartsWith("mc_fee=", StringComparison.Ordinal));
            string grossAmount = Array.Find(PayPalMessageElemetsArray, element => element.StartsWith("mc_gross=", StringComparison.Ordinal));

            try
            
                // We now need to remove the variable name and '=' from the elements so we only have the necessary information.
                merchantTransactionId = merchantTransactionId.Replace("txn_id=", "");
                parentTransactionId = parentTransactionId.Replace("parent_txn_id=", "");
                feeAmount = feeAmount.Replace("mc_fee=", "").Replace("-", "");
                grossAmount = grossAmount.Replace("mc_gross=", "").Replace("-", "");

                // Now convert the values obtained from the IPN message and calculate the net amount.
                decimal dFeeAmount = Convert.ToDecimal(feeAmount);
                decimal dGrossAmount = Convert.ToDecimal(grossAmount);
                decimal dNetAmount = Math.Round((dGrossAmount - dFeeAmount), 2);

                // Now create the new transaction fee record.
                try
                
                    // Finally create the new transaction fee record.
                    TransactionDetail transactionDetail = new TransactionDetail();
                    transactionDetail.MerchantTransactionId = merchantTransactionId;
                    transactionDetail.Gross = dGrossAmount;
                    transactionDetail.Fee = Decimal.Negate(dFeeAmount);
                    transactionDetail.Net = dNetAmount;
                    transactionDetail.TransactionType = (int)TransactionDetailTransactionType.InStoreRefund;
                    transactionDetail.Save();
                
                catch (Exception ex)
                
                    string errorMessage = String.Format("ProcessPaymentIPNMessage - Unable to create new TransactionDetail record for Merchant Transaction ID: 0", merchantTransactionId);
                    PayPalInStore.Utils.ErrorHandler.LogError(new Exception(errorMessage), "DEBUG");
                    return false;
                

                // Finally update the PurchaseRefund record with the Parent Transaction Id (used as a backup incase the API IPN message for the payment wasn't received).
                try
                
                    PurchaseRefund refund = PurchaseRefund.SingleOrDefault(x => x.RefundTransactionId == merchantTransactionId);
                    if (refund != null)
                    
                        refund.ParentTransactionId = parentTransactionId;
                        refund.Save();
                    
                
                catch (Exception ex)
                
                    string errorMessage = String.Format("ProcessPaymentIPNMessage - Unable to update PurchaseRefund record (Transaction ID: 0) with Parent Transaction Id: 1", merchantTransactionId, parentTransactionId);
                    PayPalInStore.Utils.ErrorHandler.LogError(new Exception(errorMessage), "DEBUG");
                    return false;
                

                // If all is succesful we can return true.
                return true;
            
            catch (Exception ex)
            
                string errorMessage = String.Format("ProcessPaymentIPNMessage - Unable to create new TransactionDetail record for Merchant Transaction ID: 0", merchantTransactionId);
                PayPalInStore.Utils.ErrorHandler.LogError(new Exception(errorMessage), "DEBUG");
                return false;
            
        
    

【讨论】:

【参考方案2】:

他们在沙盒中给你的测试 IPN 很糟糕。查看触发到您的实际回调(甚至是测试)的真实事件,您会看到它已定义 payKey;这是你用来查找的。​​p>

请注意,IPN 回调需要端口 80(尽管在任何地方都没有记录)。

这是一个真实的 IPN 通知(转换为 JSON,我的应用程序的特定信息已编辑):

"payment_request_date":"Sun Jun 24 06:12:20 PDT 2012",
"return_url":"http://redacted/paypal/transactions/3?status=completed",
"fees_payer":"EACHRECEIVER",
"ipn_notification_url":"http://redacted/paypal/notifications",
"sender_email":"redacted",
"verify_sign":"AFcWxVredacted",
"test_ipn":"1",
"transaction[0].id_for_sender_txn":"redacted",
"transaction[0].receiver":"redacted",
"cancel_url":"http://redacted/paypal/transactions/3?status=canceled",
"transaction[0].is_primary_receiver":"false",
"pay_key":"AP-redacted",
"action_type":"PAY",
"transaction[0].id":"redacted",
"transaction[0].status":"Completed",
"transaction[0].paymentType":"SERVICE",
"transaction[0].status_for_sender_txn":"Completed",
"transaction[0].pending_reason":"NONE",
"transaction_type":"Adaptive Payment PAY",
"transaction[0].amount":"USD 1.00",
"status":"COMPLETED",
"log_default_shipping_address_in_transaction":"false",
"charset":"windows-1252",
"notify_version":"UNVERSIONED",
"reverse_all_parallel_payments_on_error":"true"

请注意,您必须在 PAY 请求中手动设置 reverse_all_parallel_payments_on_error。尽管他们建议这样做(它可能会为您省去焦虑),但默认情况下它是错误的。

此外,如果您错过了 IPN,您可以使用 PaymentDetails 直接获取所有相同的信息。

我不知道 @swade1987 在看什么,但我的 IPN 不包含任何有关费用金额的信息。 (实际上我就是这样找到这篇文章的;试图找出原因。PP API 文档很糟糕。)

其他文档可以在这里找到https://developer.paypal.com/docs/classic/adaptive-payments/integration-guide/APIPN/

【讨论】:

很高兴我找到了这篇文章。我很困惑为什么沙盒 ipn 通知与真正的支付如此不同。很高兴我花了很长时间构建表和类文件来处理测试 ipn,而真正的 ipn 与它完全不同。谢谢你统治的贝宝! 就像 OP 一样,我对 paykey 没有被传回我的脚本的想法感到困惑。感谢分享!【参考方案3】:

有点晚了,但是对于从搜索引擎撞到这里的人来说......

我最近才开始自己处理 Paypal API。 OP 引用的 IPN 消息是通过卖家资料中定义的 IPN 通知 URL 传递的消息。相比之下,@sai 引用的 IPN 是自适应支付 IPN,传递到 Pay、ExecutePaypement 或 Preapproval API 请求中定义的 ipnNotificationUrl。

它们是两种不同类型的 IPN 消息,分别是 documented,请查找支付信息变量和支付/预批准消息变量。如果您同时选择这两种类型的 IPN,您可以获得这两种类型的 IPN。

关于OP引用的IPN消息,可以使用txn_id字段的值通过transactionId获取PaymentDetails。 transationId 与 payKey 一样可以引用已完成的付款。

【讨论】:

以上是关于如何通过 IPN 使用 Paypal 自适应支付?的主要内容,如果未能解决你的问题,请参考以下文章

Paypal 自适应支付 IPN 回调:如何返回完整未更改的 ipn 消息

Paypal 自适应支付 IPN 费用信息

Paypal 自适应支付和 IPN

Paypal 自适应支付 - IPN 和成功的返回响应

自适应支付 IPN 交易 ID 未出现

自适应支付 paypal - 没有支付 api 操作的 pdt 数据