MorayJ has asked for the wisdom of the Perl Monks concerning the following question:

Is anyone using Dancer2 with paypal's IPN?

Paypal have an outdated perl script in their examples which makes it a bit difficult https://github.com/paypal/ipn-code-samples

Also, it's not designed for Dancer2 and I'm hoping I'm missing some obvious way of doing it with Dancer2 without having to call in other modules

Paypal's description of IPN is here https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNImplementation/

The idea, as I understand it, is that PayPal sends me a post request with all the details of the sale as key/value pairs

I then need to acknowledge receipt by sending a blank response

Then I need to post back the key/value pairs they sent me with cmd=_notify-validate& prefixed to the front

They then send me a response telling me whether it is VERIFIED or INVALID (words which are the sole content of their next post).

Using their simulator https://developer.paypal.com/developer/ipnSimulator/ (you must have a developer account to use it), I am told "IPN was sent and the handshake was verified."

The requirement for this seems to be sending back the empty response - a script with just that in gets the same result.

However, even though I think I'm sending back their values identically (with cmd=_notify-validate& prefixed), I seem not to be as I am getting the result as INVALID

I'm not sure if I'm even doing the right thing by invoking LWP here

If anyone can give me a nudge to get me off the bank here, that would be really appreciated

Thanks

MorayJ

use Dancer2; use Dancer2::Plugin::Database; use Template; use LWP::UserAgent 6; use Data::Dumper; use Net::SSLeay post '/cgi-bin/paypal.cgi' => sub { my $return_query = request->body; debug request; $return_query = "cmd=_notify-validate&" . $return_query; # post back to PayPal system to validate my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 1 } ); +#ssl_opts => { verify_hostname => 1 } my $req = HTTP::Request->new('POST', 'https://www.sandbox.paypal.com/c +gi-bin/webscr'); $req->content_type('application/x-www-form-urlencoded; charset=UTF- +8'); $req->header(Host => 'www.paypal.com'); $req->content("\n\n"); my $res = $ua->request($req); debug "First response: " $res; $req->content($return_query); $res = $ua->request($req); debug "Second response: " . Dumper $res; my %variable; foreach my $key (keys params) { my $value = params->{$key}; $value =~ tr/+/ /; $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; $variable{$key} = $value; } # assign posted variables to local variables my $item_name = $variable{'item_name'}; my $item_number = $variable{'item_number'}; my $payment_status = $variable{'payment_status'}; my $payment_amount = $variable{'mc_gross'}; my $payment_currency = $variable{'mc_currency'}; my $txn_id = $variable{'txn_id'}; my $receiver_email = $variable{'receiver_email'}; my $payer_email = $variable{'payer_email'}; if ($res->is_error) { debug "It's an error"; # HTTP error } elsif ($res->content eq 'VERIFIED') { debug "$payment_status"; # check the $payment_status=Completed # check that $txn_id has not been previously processed # check that $receiver_email is your Primary PayPal email # check that $payment_amount/$payment_currency are correct # process payment } elsif ($res->content eq 'INVALID') { debug "It's invalid"; # log for manual investigation } else { debug "It's an error"; # error }

EDIT: I left this for some time to try not to get into knots. And now it seems to work. As I don't think I've changed the code, I *believe* that I must have not put https in the notify_url setting on paypal. I'm guessing I had assumed that it would be fine as my server resolves to https.

So, this is probably nothing to do with Dancer which seems to work as expected (or perl) and is me doing a settings error with paypal

But there is some very useful info about working with paypal and perl below, so not totally wasted. Thank you

Replies are listed 'Best First'.
Re: How do you use Paypal IPN with Dancer2?
by $h4X4_|=73}{ (Monk) on Jul 12, 2016 at 10:03 UTC

    Have you tried getting the script to work the way they originally had it?
    This may not be an issues but in your code you have.

    $return_query = "cmd=_notify-validate&" . $return_query;
    But the way they had it.
    $query .= '&cmd=_notify-validate';
    would be like this, if you wanted to wright it that way. Thats also guessing that string $return_query is formatted to support &Some=Thing;Like=That.
    $return_query = $return_query."&cmd=_notify-validate";

    Update: $req->content_type('application/x-www-form-urlencoded; charset=UTF-8'); charset=UTF-8 may not work with PayPal.

      Hi, thanks for looking

      You're right to notice that I've got that the other way round - I have tried it as the original.

      This is one of the reasons the script seems to be out of date as paypal docs now talk about prefixing rather than appending (though I have found one comment suggesting that either way now works). They even have an example showing the return content with it prefixed.

      As far as I can tell it is prefixing fine. I'm wondering about encoding and stuff. It's almost certainly something ridiculous that I'm overlooking.

      I'm going to investigate Dancer2's delayed, as might help to keep everything Dancer.

      Thanks again

        In the trouble shooting tips they say to keep the same encoding.
        Receiving an INVALID message from PayPal in response to your listener's post back for validation "Ensure that you use the same character encoding for your response string as the encoding specified in the charset field of the original IPN message. When testing using the IPN Simulator, the character encoding will always be UTF-8."

        Update: But I use PDT because "So, use PDT if your site includes a feature that requires immediate payment notification." IPN vs. PDT

        Update 2: From what I can remember of IPN. It does not matter that much that it comes back "INVALID" because all that is saying is that you did not return the data back the way PayPal wanted. But the transaction still will go through and you should still receive the transaction info from the first IPN response.

        Update 3: You did mention encoding and that made me think that maybe the content your getting from request->body; could be encoded or maybe decoded. Or LWP could encode the data before sending it making it a double encoded string. Just a few more things to check for.

Re: How do you use Paypal IPN with Dancer2?
by perlfan (Parson) on Jul 13, 2016 at 21:14 UTC
    I've been there. DON'T use that script. Use Business::PayPal::IPN instead.

    Inside of your handler, do something like:
    my $PP_WEBSCR = q{https://www.sandbox.paypal.com/YOURSANDBOX/webscr}; +# for testing #my $PP_WEBSCR = q{https://www.paypal.com/cgi-bin/webscr} # for produc +tion $Business::PayPal::IPN::GTW = $PP_WEBSCR; # set PP verification url yo +u use my $ipn = eval { Business::PayPal::IPN->new( query => query_string ) } +; # where "query_string" is a Dancer2 method #... do something with $ipn or handle error
    Also note, that unless you want PP to keep attempting the IPN, always return 200.

      I was going to mention Business::PayPal::IPN but i seen in Business::CPI::Gateway::PayPal::IPN the person that wrote it stated this about Business::PayPal::IPN = This is a rewrite of Business::PayPal::IPN. It works somewhat similar to it, and shares almost none of the same code.

      Looking into what Business::PayPal::IPN does. It uses CGI to get the param's, so if the module works. that means it decodes the content before sending it back to PayPal.
      You can try to use this code on $return_query to decode it and see if it works now.

      sub decode_it { my $value = shift || ''; $value =~ tr/+/ /; $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; return $value; }


      Update: What perlfan means by "always return 200" is at the end of the code you need to print out a blank header like in the example. They use text/plain for the 200 response at the end. I'm not sure what that would equal in Dancer2 to get a header like that.
      print "content-type: text/plain\n\n";

        Also, it has to be an HTTP status of 200. For example, if you have an uncaught die (but it's your fault not PPs) or your processing script crashes, then PP has no way of knowing not to repeat the IPN. Also, I'll have to take a look at Business::CPI::Gateway::PayPal::IPN. Thanks!

        Thanks, the decoding doesn't seem to be working if I've handled it correctly.

        As far as I can tell, I'm ending the script gracefully as the web page for sending the test reports everything ok (so I assume that means that it's got all the responses it expected)

        The 'invalid' it sends is in content not a header, incidentally.

        I do wonder if this issue may to be do with my apache set up and the forwarding to Dancer and if there might not be some kind of header changing going on there.

        I've also seen someone claim their script doesn't work in the simulator but does when they use the sandbox environment

        I think, for now, I'm going to use a different validation method - or just assume that the IPN is good enough as it is.

        If I solve this, I'll come back and report on it.

        Thanks again

        MorayJ

Re: How do you use Paypal IPN with Dancer2?
by lancer (Scribe) on Jul 17, 2016 at 16:59 UTC
    I have implemented it recently using HTTP::Server::Simple. I think the process should be similar on Dancer2.

    (Also your server's IPN URL must use HTTPS and must not contain an IP address or a port number.)

    The code is something like this:
    my $USE_SIMULATOR = 0; # 0 = live, 1 = sandbox / simulator my $LIVE_URL = 'https://www.paypal.com/cgi-bin/webscr'; # live my $SANDBOX_URL = 'https://www.sandbox.paypal.com/cgi-bin/webscr'; # s +andbox my $PAYPAL_HOST_HEADER = ($USE_SIMULATOR ? 'www.sandbox.paypal.com' : +'www.paypal.com'); my $RESPONSE_URL = ($USE_SIMULATOR ? $SANDBOX_URL : $LIVE_URL); # read POST params my $postdata = read_all ($content_length, $server->stdin_handle); my $post = (length ($postdata) > 0) ? CGI->new ($postdata)->Vars : {}; # extract some transaction details my $txn_type = $post->{txn_type}; my $txn_id = $post->{txn_id}; my $payment_status = lc ($post->{payment_status}); # respond to Paypal's initial HTTP request print "HTTP/1.1 200 OK\r\n\r\n"; # send back the original POST data with our extra field "cmd=_notify-v +alidate" to get an INVALID or VERIFIED response # this makes a new HTTP request my $ua = LWP::UserAgent->new (ssl_opts => { verify_hostname => 1 }); my $req = HTTP::Request->new ('POST', $RESPONSE_URL); $req->content_type('application/x-www-form-urlencoded'); $req->header(Host => $PAYPAL_HOST_HEADER); $postdata = 'cmd=_notify-validate&' . $postdata; $req->content($postdata); my $res = $ua->request($req); # process the response if ($res->is_error) { # connection error ... } elsif ($res->content eq 'VERIFIED') { # ok ... ( further processing ) } elsif ($res->content eq 'INVALID') { # error ... } else { # unexpected error ... }

      (Also your server's IPN URL must use HTTPS
      This is not true. Some people have it working on HTTP.

      What you are doing with this part of the code can cause servers to return HTTP 500 Internal Server Error because of malformed header format. The server always sets that header not your program. PayPal requires the HTTP 200 response to be a text/plain header.

      # respond to Paypal's initial HTTP request print "HTTP/1.1 200 OK\r\n\r\n";

        Thank you for the feedback!

        I have edited the part about using HTTPS-only URL-s.

        The code where the HTTP 200 header is returned seemed to work for me in that exact form.*
        (Note, I was using HTTP::Server::Simple, where your program has to print the HTTP status line and all headers.)

        * Actually I'm not sure about that. I will check it sometime soon and update this post if I can verify that.
Re: How do you use Paypal IPN with Dancer2?
by $h4X4_|=73}{ (Monk) on Jul 19, 2016 at 00:33 UTC

    I have just verified some PayPal Updates that some people may like to know.

    IPN Verification Postback to HTTPS If you are using PayPal’s Instant Payment Notification (IPN) service, you will need to ensure that HTTPS is used when posting the message back to PayPal for verification. After June of 2017 HTTP postbacks will no longer be supported.

    "At this time, there is no requirement for HTTPS on the outbound IPN call from PayPal to the merchant’s IPN listener. ... To avoid any disruption of service, you must verify that your systems are ready for this change by June 30, 2017"