5
votes

I am using the adaptive payment system from Paypal. Using a sandbox account, I was able to make a PayRequest and get forwarded to Paypal to do the payment. It's then looking like:

Request=

Apr 24, 2012 10:35:46 PM com.paypal.adaptive.api.requests.PayRequest execute INFO: Sending PayRequest with: 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&

Response=

Apr 24, 2012 10:35:48 PM com.paypal.adaptive.api.requests.PayPalBaseRequest makeRequest INFO: Received Response: responseEnvelope.timestamp=2012-04-24T13%3A35%3A48.587-07%3A00&responseEnvelope.ack=Success&responseEnvelope.correlationId=c8dee8023cca6&responseEnvelope.build=2756816&payKey=AP-1UF57245CJ360523K&paymentExecStatus=CREATED

I'm now trying to figure out, how i can check, the payment was successfully completed. So I tried to implement the ipn system, which works using the sandbox tools. However, I don't know how to connect the 2 together. i.e. when a payment is made, I am assuming I need to create a record in a database that this user has made a payment, probably as pending/created? Then wait for the ipn to return to notify me that the payment is made, and update the database table to say complete? How can i correlate the PayRequest to the IPN-Notification, i'll get from paypal? Paypal is only sending a few information with the IPN-Notification like:

  • item_number=AK-1234
  • residence_country=US
  • verify_sign=ArcmaOINNZx08uC3iQY0zhEQN3IZAz70ynRk93Or8ixRi23bb4rGNIrd
  • address_country=United States
  • address_city=San Jose
  • address_status=unconfirmed
  • payment_status=Completed
  • [email protected]
  • payer_id=TESTBUYERID01
  • first_name=John
  • shipping=3.04
  • [email protected]
  • mc_fee=0.44
  • txn_id=484221854
  • quantity=1
  • [email protected]
  • notify_version=2.1
  • txn_type=web_accept
  • test_ipn=1
  • payer_status=verified
  • mc_currency=USD
  • mc_gross=12.34
  • custom=xyz123
  • mc_gross_1=9.34
  • payment_date=11:54:48 Apr 22, 2012 PDT
  • charset=windows-1252
  • address_country_code=US
  • address_zip=95131
  • address_state=CA
  • tax=2.02
  • item_name=something
  • address_name=John Smith
  • last_name=Smith
  • payment_type=instant
  • address_street=123, any street
  • receiver_id=TESTSELLERID1

I cant find something usable in that IPN-Notifcation. The best would be if i could get the same correlation-id with the IPN-Notification i already got with the pay-response. So i could save the response-correlation-id on my database and then check against it if i receive the IPN-Notification with the same correlation-id.

3
So to clarify you want to use Adaptive Payments (PAY) method to take the payment and then IPN message to verify success?swade1987
Yep and i want to correlate the payment to the user who paid.lazydaemon

3 Answers

14
votes

The test IPN they give you in the sandbox is terrible. Look at a real one triggered to your actual callback (even a test), and you'll see it has the payKey defined; this is what you use to look it up.

Note that they require port 80 for the IPN callback (though that's not documented anywhere).

Here's a real IPN notification (translated to JSON, info specific to my app redacted):

{"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"}

Note that you have to set reverse_all_parallel_payments_on_error in the PAY request manually. Even though they recommend doing so (and it'll probably save you angst) it's false by default.

Also, you can use PaymentDetails to get all the same info directly if you miss the IPN.

I don't know what @swade1987 was looking at, but my IPNs don't include any info about the fee amount. (That's actually how I found this post; trying to figure out why. The PP API documentation is horrid.)

Additional documentation can be found here https://developer.paypal.com/docs/classic/adaptive-payments/integration-guide/APIPN/

3
votes

A bit late but for whoever bumps into here from a search engine...

I just started dealing with Paypal APIs myself lately. The IPN message the OP is quoting is the one delivered at the IPN notification URL defined in the seller profile. In contrast, the IPN quoted by @sai, is the Adapative Payments IPN, delivered to the ipnNotificationUrl defined in the Pay, ExecutePaypement or Preapproval API requests.

They are two different types of IPN messages and are documented, look for Payment Information Variables and Pay/Preapproval Message Variables. You can get both types of IPNs if you opt for both of them.

Concerning the IPN message quoted by the OP, you can use the value of txn_id field to get PaymentDetails by transactionId. The transationId is as good as the payKey to reference a completed payment.

2
votes

This should help you massively.

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;
            }
        }
    }
}