Is there any sample for PayPal IPN

2020-05-27 03:17发布

问题:

I have an Asp.Net WEB API 2 project and I would like to implement an Instant Payment Notification (IPN) listener controller.

I can't find any example and nuget package. All I need is to acknowledge that the user paid with the standard html button on Paypal. It's quite simple.

All the nuget packages are to create invoice or custom button. It's not what I need

The samples on paypal are for classic asp.net and not for MVC or WEB API MVC

I'm sure somebody did that already and when I started coding I had a feeling that I was reinventing the wheel.

Is there any IPN listener controller example?

At least a PaypalIPNBindingModel to bind the Paypal query.

    [Route("IPN")]
    [HttpPost]
    public IHttpActionResult IPN(PaypalIPNBindingModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest();
        }

        return Ok();
    }

EDIT

So far I have the following code

        [Route("IPN")]
        [HttpPost]
        public void IPN(PaypalIPNBindingModel model)
        {
            if (!ModelState.IsValid)
            {
                // if you want to use the PayPal sandbox change this from false to true
                string response = GetPayPalResponse(model, true);

                if (response == "VERIFIED")
                {

                }
            }
        }

        string GetPayPalResponse(PaypalIPNBindingModel model, bool useSandbox)
        {
            string responseState = "INVALID";

            // Parse the variables
            // Choose whether to use sandbox or live environment
            string paypalUrl = useSandbox ? "https://www.sandbox.paypal.com/"
            : "https://www.paypal.com/";

            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri(paypalUrl);
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));

                //STEP 2 in the paypal protocol
                //Send HTTP CODE 200
                HttpResponseMessage response = client.PostAsJsonAsync("cgi-bin/webscr", "").Result;

                if (response.IsSuccessStatusCode)
                {
                    //STEP 3
                    //Send the paypal request back with _notify-validate
                    model.cmd = "_notify-validate";
                    response = client.PostAsync("cgi-bin/webscr", THE RAW PAYPAL REQUEST in THE SAME ORDER ).Result;

                    if(response.IsSuccessStatusCode)
                    {
                        responseState = response.Content.ReadAsStringAsync().Result;
                    }
                }
            }

            return responseState;
        }

but for the step 3 I tried to post my model as json but paypal returns a HTML page instead of VALIDATED or INVALID. I figured out that I have to use application/x-www-form-urlencoded and it the parameters as to be in the same order.

How can I get the request URL?

I would use the query Url and add &cmd=_notify-validate to it

回答1:

Based on accepted answer I came up with the following code implementing IPN listener for ASP.NET MVC. The solution has already been deployed and appears to work correctly.

[HttpPost]
public async Task<ActionResult> Ipn()
{
    var ipn = Request.Form.AllKeys.ToDictionary(k => k, k => Request[k]);
    ipn.Add("cmd", "_notify-validate");

    var isIpnValid = await ValidateIpnAsync(ipn);
    if (isIpnValid)
    {
        // process the IPN
    }

    return new EmptyResult();
}

private static async Task<bool> ValidateIpnAsync(IEnumerable<KeyValuePair<string, string>> ipn)
{
    using (var client = new HttpClient())
    {
        const string PayPalUrl = "https://www.paypal.com/cgi-bin/webscr";

        // This is necessary in order for PayPal to not resend the IPN.
        await client.PostAsync(PayPalUrl, new StringContent(string.Empty));

        var response = await client.PostAsync(PayPalUrl, new FormUrlEncodedContent(ipn));

        var responseString = await response.Content.ReadAsStringAsync();
        return (responseString == "VERIFIED");
    }
}

EDIT:

Let me share my experience - the above code was working just fine up until now, but suddenly it failed for one IPN it was processing, i.e. responseString == "INVALID".

The issue turned out to be that my account was set up to use charset == windows-1252 which is PayPal default. However, FormUrlEncodedContent uses UTF-8 for encoding and therefore the validation failed because of national characters like "ř". The solution was to set charset to UTF-8, which can be done in Profile > My selling tools > PayPal button language encoding > More Options, see this SO thread.



回答2:

This is my code

Feel free to review is something is wrong

        [Route("IPN")]
        [HttpPost]
        public IHttpActionResult IPN()
        {
            // if you want to use the PayPal sandbox change this from false to true
            string response = GetPayPalResponse(true);

            if (response == "VERIFIED")
            {
                //Database stuff
            }
            else
            {
                return BadRequest();
            }

            return Ok();
        }

        string GetPayPalResponse(bool useSandbox)
        {
            string responseState = "INVALID";
            // Parse the variables
            // Choose whether to use sandbox or live environment
            string paypalUrl = useSandbox ? "https://www.sandbox.paypal.com/"
            : "https://www.paypal.com/";

            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri(paypalUrl);
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));

                //STEP 2 in the paypal protocol
                //Send HTTP CODE 200
                HttpResponseMessage response = client.PostAsJsonAsync("cgi-bin/webscr", "").Result;

                if (response.IsSuccessStatusCode)
                {
                    //STEP 3
                    //Send the paypal request back with _notify-validate
                    string rawRequest = response.Content.ReadAsStringAsync().Result;
                    rawRequest += "&cmd=_notify-validate";

                    HttpContent content = new StringContent(rawRequest);

                    response = client.PostAsync("cgi-bin/webscr", content).Result;

                    if(response.IsSuccessStatusCode)
                    {
                        responseState = response.Content.ReadAsStringAsync().Result;
                    }
                }
            }

            return responseState;
        }


回答3:

I was also looking for a solution similar to the OP's original question Is there any IPN listener controller example? At least a PaypalIPNBindingModel to bind the Paypal query. and I got to this page. I tried the other solutions mentioned in this thread, they all worked but I really need the PayPal query-to-model solution so I googling until I stumbled on Carlos Rodriguez's Creating a PayPal IPN Web API Endpoint blogpost.

Here's an overview on what Carlos did:

  1. Create a model. Base the properties you'll define in the model from the ipn response you'll get from PayPal.

    public class IPNBindingModel
    {
        public string PaymentStatus { get; set; }
        public string RawRequest { get; set; }
        public string CustomField { get; set; }    
    }
    
  2. Create a PayPal Validator class.

    public class PayPalValidator
    {
        public bool ValidateIPN(string body)
        {
            var paypalResponse = GetPayPalResponse(true, body);
            return paypalResponse.Equals("VERIFIED");
        }
    
        private string GetPayPalResponse(bool useSandbox, string rawRequest)
        {
            string responseState = "INVALID";
            string paypalUrl = useSandbox ? "https://www.sandbox.paypal.com/"
            : "https://www.paypal.com/";
    
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri(paypalUrl);
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
                HttpResponseMessage response = client.PostAsJsonAsync("cgi-bin/webscr", "").Result;
                if (response.IsSuccessStatusCode)
                {
                    rawRequest += "&cmd=_notify-validate";
                    HttpContent content = new StringContent(rawRequest);
                    response = client.PostAsync("cgi-bin/webscr", content).Result;
                    if (response.IsSuccessStatusCode)
                    {
                        responseState = response.Content.ReadAsStringAsync().Result;
                    }
                }
            }
            return responseState;
        }
    }
    
  3. Create your controller.

    [RoutePrefix("paypal")]
    public class PayPalController : ApiController
    {
        private PayPalValidator _validator;
    
        public PayPalController()
        {
           this._validator = new PayPalValidator();
        }
    
        [HttpPost]
        [Route("ipn")]
        public void ReceiveIPN(IPNBindingModel model)
        {
            if (!_validator.ValidateIPN(model.RawRequest)) 
                throw new Exception("Error validating payment");
    
            switch (model.PaymentStatus)
            {
    
                case "Completed":
                    //Business Logic
                    break;
            }
       }
    }
    
  4. Create a model binder that will define how Web Api will automatically create the model for you.

    public class IPNModelBinder : IModelBinder
    {
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType != typeof(IPNBindingModel))
            {
               return false;
            }
        var postedRaw = actionContext.Request.Content.ReadAsStringAsync().Result;
    
        Dictionary postedData = ParsePaypalIPN(postedRaw);
        IPNBindingModel ipn = new IPNBindingModel
        {
            PaymentStatus = postedData["payment_status"],
            RawRequest = postedRaw,
            CustomField = postedData["custom"]
        };
    
        bindingContext.Model = ipn;
        return true;
    }
    
    private Dictionary ParsePaypalIPN(string postedRaw)
    {
        var result = new Dictionary();
        var keyValuePairs = postedRaw.Split('&');
        foreach (var kvp in keyValuePairs)
        {
            var keyvalue = kvp.Split('=');
            var key = keyvalue[0];
            var value = keyvalue[1];
            result.Add(key, value);
        }
    
        return result;
    }
    }
     }
    
  5. Register your model binder to WebApiConfig.cs. config.BindParameter(typeof(IPNBindingModel), new IPNModelBinder());

Hope this helps somebody else. Thank you Carlos Rodriguez for your amazing code.



回答4:

Extending Michal Hosala's answer, there are two things needed to get a successful handshake with PayPal

First, setting the security protocol before making request to PayPal

ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

Second, avoiding the dictionary because for verification, PayPal requires the data to be posted back in the same order and preceded by the cmd variable. I ended up doing this

Request.InputStream.Seek(0, SeekOrigin.Begin);
string rawRequestBody = new StreamReader(Request.InputStream).ReadToEnd();
var ipnVarsWithCmd = rawRequestBody.Split('&').Select(x => new KeyValuePair<string, string>(x.Split('=')[0], x.Split('=')[1])).ToList();
ipnVarsWithCmd.Insert(0, new KeyValuePair<string, string>("cmd", "_notify-validate"));


回答5:

There is an official c# example here: https://github.com/paypal/ipn-code-samples in path \c#\paypal_ipn_mvc.cs

The C# example shows an ASP.NET MVC controller with an action that responds to the IPN.