PHP Paypal Auth/Capture NVP Integration Troubles
Asked Answered
G

2

17

Background:

We implemented Paypal Authorization and Capture flow using NVP integration and php-curl.
The complete process is described on the PayPal developer website : https://developer.paypal.com/webapps/developer/docs/classic/express-checkout/ht_ec-singleAuthPayment-curl-etc/

On our website, the current payment scenario is :
- First, an user click on a button to initiate a payment authorization, redirecting him on the PayPal website (SetExpressCheckout with paymentaction=Authorization)
- If the user succesfully confirmed the payment on the PayPal website, he is redirected to our website on a specific success page
- This "success page" gets a token and a PayerID from the PayPal website, we then call GetExpressCheckoutDetails to check the status and the amount of this authorization
- If everything is ok, we tell PayPal to confirm this authorization (DoExpressCheckoutPayment with paymentaction=Authorization) and we get an authorization ID to store into our database
- Later, someone else can settle the transaction by clicking on a button, using the authorization ID we stored (DoCapture)

Additional informations:

According to the PayPal documentation :

PayPal honors 100% of authorized funds for three days
The accounts of buyers and merchants cannot be closed if there is a pending (unsettled) authorization
https://developer.paypal.com/docs/classic/paypal-payments-standard/integration-guide/authcapture/

On our website, authorizations are automatically voided if they are not settled within 24 hours. (using crontab)

The problem:

A problem occurs on the last part (when we call the "confirm" function) : When an user click on a "confirm" button, it seems that sometimes the curl request is taking time to get a transaction ID back from PayPal.
When this happens, the user usually close the webpage, PayPal confirm the authorization (and thus money transfert) but our website is not notified about this because the following code (from the "Source Code" section below) has not been executed or reached :

if ($transaction_id) {
    /*
     * [...]
     * Everything is ok, payment has been performed
     * so we do everything to give our user what he asked for
     */
} else {
    // Error : No transaction id
}

Because the script stopped before getting the curl response.
Moreover, if we try to click on the button again, PayPal tells us that the authorization ID doesn't exist (because already performed).

But sometimes everything works well without any problem or lag.

Source Code:

/*
 * This is our main function, called when
 * we have to settle our transaction 
 * when an user click on a "confirm" button
**/
public function confirm($cart_id) {
    /*
     * [...]
     * We check lot of stuff to be sure this user 
     * can perform this action
     */

    // We get theses values from the database
    authorization_id = "lorem ipsum";
    $amount = 10; 

    // We tell PayPal to settle the transaction
    $transaction_id = $this->settle_transaction($authorization_id, $amount);
    if ($transaction_id) {
        /*
         * [...]
         * Everything is ok, payment has been performed
         * so we do everything to give our user what he asked for
         */
    } else {
        // Error : No transaction id
    }
}

private function settle_transaction($authorization_id, $amount) {
    // Our credentials
    $params = array(
        "USER" => $this->paypal_user,
        "PWD" => $this->paypal_pwd,
        "SIGNATURE" => $this->paypal_signature,
        "VERSION" => 95
    );
    $params["METHOD"] = "DoCapture";
    $params["AUTHORIZATIONID"] = $authorization_id;
    $params["AMT"] = $amount;
    $params["CURRENCYCODE"] = "EUR";
    $params["COMPLETETYPE"] = "Complete";

    $result = $this->curl($params);
    if ($result) {
        // We check that this PayPal request has been successful
        if ($result["ACK"] == "Success") {
            $transaction_id = $result["TRANSACTIONID"];
            if ($result["PAYMENTSTATUS"] == "Completed") {
                return $transaction_id;
            }
        }
    }
    return NULL;
}


private function curl($params) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $this->paypal_endpoint);
    curl_setopt($ch, CURLOPT_POST, count($params));
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
    parse_str(curl_exec($ch), $result);
    curl_close($ch);
    return $result;
}

Do you have any idea to resolve this issue ?
I was thinking about settling transaction at the end of the script because PayPal honors 100% of authorized funds for three days, and I only need them to be hold for 1 day but I'm not sure of this anyway ...

Edit 1:

My apache2 error.log reported this when this problem happens :

[Mon Aug 08 20:42:55.959330 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:42:56.960453 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:42:57.961188 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:42:58.962230 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:42:59.963297 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:00.964384 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:01.965476 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:02.966478 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:03.967595 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:04.968713 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:05.969783 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:06.970877 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:07.972002 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:08.972749 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:09.973847 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:10.974926 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:11.976080 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:12.977168 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:13.978244 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:14.979320 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:15.980414 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:16.981493 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:17.982578 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:18.983673 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:19.984762 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:20.985841 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:21.986650 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:22.987725 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:23.988826 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:24.989939 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:25.991061 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:26.992181 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:27.993305 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:28.994422 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:29.995556 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:30.996661 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:31.997774 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:32.998905 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:34.000089 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:35.001202 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:36.002326 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:37.003424 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:38.004551 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:39.005677 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:40.006799 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:41.007902 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:42.009021 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:43.010132 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:44.011245 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:45.012361 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:46.013479 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:47.014577 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:48.015685 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:49.016801 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:50.017906 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:51.018980 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:52.020049 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:53.021158 2016] [mpm_event:error] [pid 141:tid 3779940374400] AH00485: scoreboard is full, not at MaxRequestWorkers
[Mon Aug 08 20:43:53.391316 2016] [:error] [pid 980:tid 3779386513152] (104)Connection reset by peer: [client MY-IP:55236] FastCGI: failed to read from backend server, referer: http://####
[Mon Aug 08 21:18:04.748237 2016] [:error] [pid 1287:tid 3779782977280] (104)Connection reset by peer: [client MY-IP:37196] FastCGI: failed to read from backend server

Edit 2:

I found this topic that seems to have a similar issue :

What is particularly strange is that the payment has been processed correctly.

And right now I can't seem to be able to reproduce this bug.
Do you think it could have been a PayPal issue or something like this ?
Even if it was, I wan't to make sure that this problem won't happen again, but how can I test if I can't reproduce this ?

Gusto answered 11/8, 2016 at 18:33 Comment(0)
E
2

Note: Not all payments will be instant. If the buyer only has a bank account associated with their PayPal account, the transfer wont be instant. Therefore it is best practice to use IPN if want automatic notifications of all payments and related activities.

According to PayPal Official Docs:

Instant Payment Notification (IPN) is a message service that notifies you of events related to PayPal transactions. You can use IPN messages to automate back-office and administrative functions, such as fulfilling orders, tracking customers, or providing status and other transaction-related information.

As best practice set transactional script in your IPN Listener. For the integration guide you can refer here : https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNImplementation/

I have extended a PHP class for PayPal IPN Listener few months backs. Hope it may help as starting point. Feel free to fork :https://github.com/datumradix/PayPal-IPN-PHP-Class-

Edit: (PayPal Documentation is not clear at many places and seems confusing to many first time readers)

IPN can come handy as secondary mechanism to confirm if the DoCapture was successful. IPN variables like txn_type, txn_id,auth_id, auth_amount and payer_id are all notified thro IPN. Please ref here for full list: https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNandPDTVariables/

Note: We can either specify the NOTIFYURL in each call or we can setup the same from paypal back-end. For steps to setup the same from PayPal profile settings, ref :https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNSetup/

Electrophysiology answered 20/8, 2016 at 16:39 Comment(8)
Thanks for your answer, I took a look at your code (and the ones in the official paypal examples). In your "payment.php" on line 99, Is there where I have to check the result of a DoCapture using an IPN callback ?Gusto
On this link they say that we can use NOTIFYURL from DoExpressCheckoutPayment to use IPN. But my DoExpressCheckoutPayment is setup with paymentaction=Authorization to complete the payment later, so the moment when I receive the notification is not the moment when I want to perform the payment.Gusto
And NOTIFYURL can't be used with DoCapture as there is no such field according to the documentation. Do you have any idea of correct working flow with DoCapture performed later ?Gusto
From your question, I assume ur issue is that your site is not getting notified at times when someone execute successful DoCapture. IPN can come handy as secondary mechanism to confirm if the DoCapture was successful. IPN variables like txn_type, txn_id,auth_id, auth_amount and payer_id are all notified thro IPNElectrophysiology
Refer here: developer.paypal.com/docs/classic/ipn/integration-guide/…Electrophysiology
And ref to the place where you use IPN callback to check result, yes we must use call backs to check the result when IPN is verified successfully. This is where we update our database to activate or process the order, or setup the database with the user's order details, email an administrator etc. We can access a slew of information via the ipn_data() array.Electrophysiology
Ok, I'm not sure to understand a specific part, first I have to set a NOTIFYURL field in my DoExpressCheckoutPayment call. Then, when one of my users triggers a DoCapture call, I'm not validating the order at this moment. But when my callback script is called by PayPal, I have to check for the txn_type to be equal to express_checkout ? Because according to your link express_checkout : Payment received for a single item; source is Express Checkout. Am I right ? or ... not ?Gusto
yes you are right. We can either specify the NOTIFYURL in each call or we can setup the same from paypal back-end.Electrophysiology
M
8

you need to learn about ignore_user_abort(true); (and possibly set_time_limit(0);), use that to avoid the problem of scripts exiting half-way through the code. second, may i suggest a database of recently confirmed tokens that is updated BEFORE the curl call, so that if a user quits, then try to press "confirm" again, you'll know it's already a confirmed token, and don't re-run the curl code, and can immediately inform the user? -- http://php.net/manual/en/function.ignore-user-abort.php

  • AND WARNING, SOME SHARED HOSTING PROVIDES DOES NOT ALLOW ignore_user_abort / set_time_limit TO BE MODIFIED AT RUNTIME
Misname answered 15/8, 2016 at 19:55 Comment(1)
Thanks for your help, I can't try what you suggest because it seems that I can't reproduce this issue anymore. I added some informations to my question.Gusto
E
2

Note: Not all payments will be instant. If the buyer only has a bank account associated with their PayPal account, the transfer wont be instant. Therefore it is best practice to use IPN if want automatic notifications of all payments and related activities.

According to PayPal Official Docs:

Instant Payment Notification (IPN) is a message service that notifies you of events related to PayPal transactions. You can use IPN messages to automate back-office and administrative functions, such as fulfilling orders, tracking customers, or providing status and other transaction-related information.

As best practice set transactional script in your IPN Listener. For the integration guide you can refer here : https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNImplementation/

I have extended a PHP class for PayPal IPN Listener few months backs. Hope it may help as starting point. Feel free to fork :https://github.com/datumradix/PayPal-IPN-PHP-Class-

Edit: (PayPal Documentation is not clear at many places and seems confusing to many first time readers)

IPN can come handy as secondary mechanism to confirm if the DoCapture was successful. IPN variables like txn_type, txn_id,auth_id, auth_amount and payer_id are all notified thro IPN. Please ref here for full list: https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNandPDTVariables/

Note: We can either specify the NOTIFYURL in each call or we can setup the same from paypal back-end. For steps to setup the same from PayPal profile settings, ref :https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNSetup/

Electrophysiology answered 20/8, 2016 at 16:39 Comment(8)
Thanks for your answer, I took a look at your code (and the ones in the official paypal examples). In your "payment.php" on line 99, Is there where I have to check the result of a DoCapture using an IPN callback ?Gusto
On this link they say that we can use NOTIFYURL from DoExpressCheckoutPayment to use IPN. But my DoExpressCheckoutPayment is setup with paymentaction=Authorization to complete the payment later, so the moment when I receive the notification is not the moment when I want to perform the payment.Gusto
And NOTIFYURL can't be used with DoCapture as there is no such field according to the documentation. Do you have any idea of correct working flow with DoCapture performed later ?Gusto
From your question, I assume ur issue is that your site is not getting notified at times when someone execute successful DoCapture. IPN can come handy as secondary mechanism to confirm if the DoCapture was successful. IPN variables like txn_type, txn_id,auth_id, auth_amount and payer_id are all notified thro IPNElectrophysiology
Refer here: developer.paypal.com/docs/classic/ipn/integration-guide/…Electrophysiology
And ref to the place where you use IPN callback to check result, yes we must use call backs to check the result when IPN is verified successfully. This is where we update our database to activate or process the order, or setup the database with the user's order details, email an administrator etc. We can access a slew of information via the ipn_data() array.Electrophysiology
Ok, I'm not sure to understand a specific part, first I have to set a NOTIFYURL field in my DoExpressCheckoutPayment call. Then, when one of my users triggers a DoCapture call, I'm not validating the order at this moment. But when my callback script is called by PayPal, I have to check for the txn_type to be equal to express_checkout ? Because according to your link express_checkout : Payment received for a single item; source is Express Checkout. Am I right ? or ... not ?Gusto
yes you are right. We can either specify the NOTIFYURL in each call or we can setup the same from paypal back-end.Electrophysiology

© 2022 - 2024 — McMap. All rights reserved.