Long story short, I believe I've implemented the flow correctly, but on the final DoExpressCheckoutPayment I am getting:
ACK => SuccessWithWarning
L_ERRORCODE0 => 11607
L_SHORTMESSAGE0 => Duplicate Request
L_LONGMESSAGE0 => A successful transaction has already been completed for this token
Is this simply because I'm doing a GetExpressCheckoutDetails request before this? (the GetExpressCheckoutDetails ACK is "Success")
Note that the other data returned from DoExpressCheckoutPayment looks good:
PAYMENTINFO_0_PAYMENTSTATUS => Completed
PAYMENTINFO_0_ACK => Success
Should I just look for PAYMENTINFO_0_ACK and ignore the rest?
Sidenote- In case it's of interest, I'm using the PHP lib at https://github.com/thenbrent/paypal-digital-goods though I changed the stuff in the examples return.php to GetExpressCheckoutDetails on a new class since, of course, it made no sense to use the same purchase data every time and it has to be dynamic
EDIT: Okay I'm baffled. If I only call the GetExpressCheckoutDetails, then the response is:
CHECKOUTSTATUS => PaymentActionNotInitiated
However, if I call GetExpressCheckoutDetails and then DoExpressCheckoutPayment, the response of the preceding GetExpressCheckoutDetails becomes:
CHECKOUTSTATUS => PaymentActionCompleted (and it follows that the result of the subsequent DoExpressCheckoutPayment has the error of Duplicate Request)
How does that even make sense?! Did vanilla PHP just become asynchronous? Has paypal allocated enough money to buy a time machine? I'm probably missing something very basic, but I really don't see it yet :\
EDIT 2 Some Sample Code (didn't strip it to make it 100% vanilla, but should be pretty straightforward):
public static function completePaypalPurchase() {
self::configurePaypal(''); // Not relevent, just some setting of API keys and stuff
$paypalAPI = new PayPal_Purchase(); // Just to get purchase info so we can form the real purchase request
$response = $paypalAPI->get_checkout_details(); // Uses token from GET automatically
echo("RESPONSE FROM GET CHECKOUT");
print_r($response);
$ack = strtoupper($response['ACK']);
$userID = (int)$response['CUSTOM']; // This was passed earlier and is retrieved correctly
$numCredits = (int)$response['L_QTY0'];
//NOTE: If I comment out the below, then the $response above has CHECKOUTSTATUS => PaymentActionNotInitiated
// BUT If I do not comment it out, leaving it as-is then the $response above has CHECKOUTSTATUS => PaymentActionCompleted
// That's the core of the problem and where I'm stuck
if($ack == "SUCCESS" && $numCredits && $userID && $userID == $loggedInUserID) {
$paypalAPI = self::getPaypalPurchaseCredits($userID, $numCredits); // This creates a new PayPal_Purchase() with this info. In fact, it's the same method and therefore should return the same sort of object as the one used at the beginning of the flow
$response = $paypalAPI->process_payment();
$ack = strtoupper($response['ACK']);
echo("RESPONSE FROM DO PAYMENT");
print_r($response);
if(isset($response['PAYMENTINFO_0_TRANSACTIONID']) && $ack == "SUCCESS") {
$transactionID = $response['PAYMENTINFO_0_TRANSACTIONID'];
return(new APIReturn(true, array('ack'=>$ack, 'userid'=>$userID, 'numcredits'=>$numCredits, 'transactionid'=>$transactionID)));
}
}
return(new APIReturn(false, self::ERROR_NORESULT));
}
The correct order the calls is SetExpressCheckout, GetExpressCheckoutDetails, and then DoExpressCheckoutPayment. If you're geting a duplicate order error then you must be calling DECP twice somehow. You need to step through your code and see exactly how that's happening. It may be something in the class that you're using.
On that note, you may be interested in taking a look at my class instead. It makes everything very simple as it turns it all into PHP arrays and handles the gritty work for you.
If you don't want to start over with new class, though, then again, you need to step through what's happening with your code and through the class methods to see where it's getting posted twice.
Another thing I notice is that you're only checking for ACK = Success. That means when ACK = SuccessWithWarning it'll be treated as a failure. You need to handle both Success and SuccessWithWarning (which a decent class library would handle for you.)
Sorry I don't have a more definitive answer, but again, somewhere either in your code or in the library it must be getting posted twice. Are you logging the raw API requests and responses along the way? If so you'd be able to confirm it's getting hit twice because you'd have 2 sets of DECP requests and responses logged.
Related
I am using the PayPal PHP SDK found here: https://github.com/paypal/Checkout-PHP-SDK
And I am somewhat puzzled in terms of how to complete the process.
On the outset this seems quite simple:
Setup your credentials
Create the Order
Check the result, and re-direct to approval link
User makes a payment and is sent to the SUCCESS link that you would have set.
i.e. http://example.com/pay/complete/paypal?token=8UK32254ES097084V&PayerID=SEQNPLB2JR9LY
And this is where things get a bit shakey.
Conveniently, a token and a PayerID is returned.
And according to the documentation, you now need to "Capturing the Order" and the following code is provided:
use PayPalCheckoutSdk\Orders\OrdersCaptureRequest;
// Here, OrdersCaptureRequest() creates a POST request to /v2/checkout/orders
// $response->result->id gives the orderId of the order created above
$request = new OrdersCaptureRequest("APPROVED-ORDER-ID");
$request->prefer('return=representation');
try {
// Call API with your client and get a response for your call
$response = $client->execute($request);
// If call returns body in response, you can get the deserialized version from the result attribute of the response
print_r($response);
}catch (HttpException $ex) {
echo $ex->statusCode;
print_r($ex->getMessage());
}
What is confusing is that the OrdersCaptureRequest requires an "APPROVED-ORDER-ID"
But all that has been returned is a "token" and a "PayerID".
So my question is, what is this APPROVED-ORDER-ID, and where do I get it?
Thank you!
what is this APPROVED-ORDER-ID, and where do I get it
At that moment, sourced from token= . It should correspond to an Order Id you received in the response to your step 2 ("Create the Order")
For step 3, it is better to use no redirects whatsoever. Instead, implement this front-end UI, which offers a far superior in-context experience that keeps your site loaded in the background: https://developer.paypal.com/demo/checkout/#/pattern/server
There is no reason for a modern website to be redirecting unnecessarily
(I'm not able to post code/errors etc because: 'You need at least 10 reputation to post more than 2 links'; and everything contains a lot of links; just spent a couple of hours on a fully detailed post).
I've read similar posts here (and other places) but they have not helped.
I'm basically following:
https://developer.paypal.com/docs/integration/direct/express-checkout/integration-jsv4/server-side-REST-integration/#set-up-your-client
but PayPal's documentation is somewhat chaotic, lacking in some crucial details and mysterious in places. Obviously written by a committee.
In a nutshell I have a PayPal payment id: "id": "PAY-0J356327TH335450NK56Y2PQ",
PayPal say return this from my create payment script. I'm not entirely sure how it should be returned: simply echo "PAY-0J356327TH335450NK56Y2PQ"; or return a json string. Tried both, did not solve anything.
Then there is (from the client side button):
onAuthorize: function(data) {
return paypal.request.post(EXECUTE_PAYMENT_URL, {
paymentID: data.paymentID,
payerID: data.payerID
}).then(function() {
// The payment is complete!
// You can now show a confirmation message to the customer
});
Which I read as it calling my execute payment script with 2 variables (paymentID and payerID), which I need to use in my script.
I've tried the obvious:
$paymentID = $_POST["paymentID"];
$payerID = $_POST["payerID"];
The not so obvious (because I'm assuming it's POSTing data although some docs seem somewhat confused on this point):
$paymentID = $_GET["paymentID"];
$payerID = $_GET["payerID"];
My final attempt of:
$ret = file_get_contents("php://input");
$ar = json_decode($ret, TRUE);
$paymentID = $ar["paymentID"];
$payerID = $ar["payerID"];
None of which made any difference.
The errors I'm getting from PayPal are:
ppxo_no_token_passed_to_payment Object ...
ppxo_unhandled_error Object ...
Error: No value passed to payment
decorate ...
I obviously have a valid access token otherwise I'd not get as far as getting a payment id.
Currently stumped; any ideas/pointers appreciated.
This really should be simple.
I had the same error message.
For me it occured when the javascript code receieved the response from my CREATE_PAYMENT_URL ajax request.
The response I was sending back from the server was not valid JSON so when it came time to execute onAuthorize data.paymentID and data.payerID were not available.
I suggests using console.log(data) just above the return statement in onAuthorize to make sure the data json object has a paymentId and payerId.
If not then it may be that your previoius CREATE_PAYMENT_URL is not returning the payment id.
The responses should be json. For example a valid create payment response:
{"id":"PAY-5T1130394T551090NLGRLACY"}
Hope that helps.
I'm using the following class to process the IPN data:
https://github.com/WadeShuler/PHP-PayPal-IPN (and it has been working fine for ages)
The last successfully processed payment was on September 28, the next IPN payment that came though on October 5 returned a response of INVALID, as has every payment since.
If I test in the IPN simulator, the message processes successfully.
Sandbox or Live, the response is INVALID. I would have thought the Sandbox would have a facility where I can review why a message was rejected as INVALID, but that doesn't seem to be the case? Would certainly have been the easy way to see the rejection reason, am I missing something?
The link that brings you to Paypal from my site is:
https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_ext-enter&redirect_cmd=_xclick&business=sales-facilitator#fydentry.com&item_name=QLD++%28Pacific+pines+secondary+Sat+25th+July+%29&item_number=FYD15000003&custom=0001000003¤cy_code=AUD&amount=0.01&no_shipping=1&image_url=http://www.fydentry.com/img/follow-your-dreams-100.jpg&return=http://www.fydentry.com/entry-test.php&cancel_return=http://www.fydentry.com/entry-test.php¬ify_url=http://www.fydentry.com/payback-test.php
The data posted back from Paypal is:
transaction_subject=0001000003&txn_type=web_accept&payment_date=07%3A05%3A07+Oct+15%2C+2015+PDT&last_name=buyer&residence_country=AU&pending_reason=multi_currency&item_name=QLD++%28Pacific+pines+secondary+Sat+25th+July+%29&payment_gross=&mc_currency=AUD&business=sales-facilitator%40fydentry.com&payment_type=instant&protection_eligibility=Ineligible&verify_sign=Acssfl2b2v1gxOK33TD2StcDhf-OAZxwix74kxFlSgpWMku6myuy.WFL&payer_status=verified&test_ipn=1&tax=0.00&payer_email=sales-buyer%40fydentry.com&txn_id=9BV63134E20871700&quantity=1&receiver_email=sales-facilitator%40fydentry.com&first_name=test&payer_id=FY3RYW98GNMXG&receiver_id=95FC3QCK53MHC&item_number=FYD15000003&handling_amount=0.00&payment_status=Pending&shipping=0.00&mc_gross=0.01&custom=0001000003&charset=windows-1252¬ify_version=3.8&ipn_track_id=cfbd422d97d69
The response I send back to Paypal is:
cmd=_notify-validate&transaction_subject=0001000003&txn_type=web_accept&payment_date=07%3A05%3A07+Oct+15%2C+2015+PDT&last_name=buyer&residence_country=AU&pending_reason=multi_currency&item_name=QLD++%28Pacific+pines+secondary+Sat+25th+July+%29&payment_gross=&mc_currency=AUD&business=sales-facilitator%40fydentry.com&payment_type=instant&protection_eligibility=Ineligible&verify_sign=Acssfl2b2v1gxOK33TD2StcDhf-OAZxwix74kxFlSgpWMku6myuy.WFL&payer_status=verified&test_ipn=1&tax=0.00&payer_email=sales-buyer%40fydentry.com&txn_id=9BV63134E20871700&quantity=1&receiver_email=sales-facilitator%40fydentry.com&first_name=test&payer_id=FY3RYW98GNMXG&receiver_id=95FC3QCK53MHC&item_number=FYD15000003&handling_amount=0.00&payment_status=Pending&shipping=0.00&mc_gross=0.01&custom=0001000003&charset=windows-1252¬ify_version=3.8&ipn_track_id=cfbd422d97d69
And the code in payback-test.php is:
<?php
include_once("f_common.php");
include_once("IpnListener.php");
use wadeshuler\paypalipn\IpnListener;
$listener = new IpnListener();
$listener->use_sandbox = true;
$dbh = open_db();
$res = 'UNKNOWN';
try {
$listener->requirePostMethod();
$verified = $listener->processIpn();
} catch (Exception $e) {
error_log($e->getMessage());
exit(0);
}
if ($verified) {
$res = "VERIFIED";
} else {
$res = "INVALID";
}
$stmt_debug = $dbh->prepare('INSERT INTO fyd_paypal_ipn (post_data, response_data, status, message_time) VALUES (:post_data, :response_data, :status, UTC_TIMESTAMP())');
$stmt_debug->execute(array(':post_data' => $listener->rawPostData, ':response_data' => $listener->debug_response, ':status' => $res));
?>
Note that for the purpose of this exercise, I also modified the IpnListener class so that I can get a copy of the response for debugging purposes.
At the top of the file in the variable declarations, I made $rawPostData public, and added this variable:
public $debug_response;
And store the value right before we use it:
$this->debug_response = $req;
if ($this->use_curl) {
$res = $this->curlPost($req);
} else {
$res = $this->fsockPost($req);
}
I'm assuming something has changed on the Paypal side to stop this working in the first place, but I can't see what it is, particularly as my response appears to be correct (identical), and I can't find anything in the Sandbox side that helps me identify why it is being regarded as invalid? Unfortunately the debug code at the bottom where I track the IPN post data and response is new, so I don't have an older (working!) set of messages to compare the contents to.
hummmm, since 1 october paypal change sha-1 to sha-2
https://www.paypal-knowledge.com/infocenter/index?page=content&id=FAQ1766&expand=true&locale=en_US
I've gotten to the bottom of it, it appears to be a certificate issue, combined with a script error.
My certificate file was out of date to the one currently in Wade's code. I spotted that and updated the certificate file yesterday. I've now verified that this was the root cause of my original failure.
However, while updating that, I also updated to the latest class - which was really just removing some deprecated code that I wasn't using and is no longer supported, but I missed one small change which broke things.
There's a line in the IpnListener class which sets the certificate location:
curl_setopt($ch, CURLOPT_CAINFO, dirname(dirname(__FILE__)) . '/cert/api_cert_chain.crt');
But the directory name ends up wrong on my server, and it can't find the certificate. The previous version of the class read:
curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__) . '/cert/api_cert_chain.crt');
which worked. Now that I've changed it back, everything works again. Note that the domain I'm working from is set up in a sub-directory to my primary domain, which is possibly why the new code ends up pointing to the wrong place (one level above where I need it to be).
And I'm still confused as to why the IPN simulator worked, while the Sandbox and Live systems didn't.
I'm at a complete loss. I think I might be getting "mis-informed", but I'll try explain my situation as best I can.
The Idea
We have a form for users to purchase credits. Type in credit number,
click pp button.
Upon click of button, a post is made to set the
transaction log information and set it as pending (works fine).
Upon valid post return it continues to submit the paypal form (works also).
The user is redirected to paypal page and makes payment (so far so good).
after payment made, they click the return and are directed toward the "success" page (still working).
upon reaching this page I take in post data from pp (uh oh, here's where it gets sticky)
verify the data is "true" pp data and update the transaction log (HOW!?)
What I'm being told & what i've tried
I was initially going to use IPN to do a post back to paypal to verify the recieved data (ensure it wasn't spoofed), however, I'm being told that for cost purposes and having to setup an "ipn server" we can't use IPN ....
Ok, so I was gonna use PDT, except either I missed a major step in my attempt or it ISNT working right at all because I'm not doing somthing right. Here is where I'm lost, i've tried a dozen different things, including a direct link post, using sparks (for CI) to set the data and call to paypal link, and etc ...
I've looked over every paypal question on here and a half dozen other forums and can't seem to get anything going.
Can anyone "clearly" tell me how I can verify the POST data of a successful paypal transaction and maybe even tell me if i'm being misinformed about the IPN, cause I looked over the docs and I can't find what i've been told, nor can I really find my solution.
I feel stupid, please help.
When your user clicks a PayPal button and goes to PayPal, when they complete the transaction, an IPN POST is made to a URL of your choosing. So you don't have to have another web server.
When the IPN request comes in, PayPal wants you to re-send the entire POST they made to you back to them, including all of the fields, in the exact order, at which point they will return the word 'VERIFIED' or 'INVALID.' If verified, then do whatever it is that you need to do to toggle your txn log from pending to verified. Also, any information you include in your button (your button is actually a form so you can include your own fields) is included in the POST. Useful for keeping a 'transaction id' or some other identifier for mapping back to your transaction.
If the IPN fails it will resend in n+4 minute increments (where n is how long it waited the last time - 4 minutes, next after 8 minutes, next after 12 minutes, etc) for a few days.
Finally made it work correctly thanks to the update in info on IPN.
My solution added the following line to my form:
<input type="hidden" name="notify_url" value="<?= site_url('payment/notifyTest'); ?>">
Then in the notifyTest function i ran this:
$pDat = $this->input->post(NULL, TRUE);
$isSandBox = array_key_exists('test_ipn', $pDat) && 1 === (int)$pDat['test_ipn'] ? TRUE : FALSE;
$verifyURL = $isSandBox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr';
$token = random_string('unique');
$request = curl_init();
curl_setopt_array($request, array
(
CURLOPT_URL => $verifyURL,
CURLOPT_POST => 0,
CURLOPT_POSTFIELDS => http_build_query(array('cmd' => '_notify-validate') + $pDat),
CURLOPT_RETURNTRANSFER => 0,
CURLOPT_HEADER => 0,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => 0,
CURLOPT_CAINFO => 'cacert.pem',
));
$response = curl_exec($request);
$status = curl_getinfo($request, CURLINFO_HTTP_CODE);
curl_close($request);
if($status == 200 && $response == 'VERIFIED') {
// SUCCESS
$data = array (
... => ...
);
$this->db->insert('transactions', $data);
}
else {
// FAILED
$data = array (
... => ...
);
$this->db->insert('transactions', $data);
};
THE IMPORTANT DIFFERENCE AS WE FOUND -> DO NOT SET YOUR CURL VARS TO TRUE OR FALSE
USE 0 FOR TRUE AND 1 FOR FALSE, IT MIGHT SOUND STUPID, BUT IT WOIKED!!!
I've successfully made my way through the LinkedIn OAuth process (using the REST API - OAuth 1.0a). However I'm having trouble with my first API call after the callback. I set the UserToken, UserTokenSecret and UserVerfier in the library I am writing, and this call this function to get my profile information:
public function getUserProfile()
{
$consumer = new OAuthConsumer($this->consumer_key, $this->consumer_secret, NULL);
$auth_token = new OAuthConsumer($this->getUserToken(), $this->getUserTokenSecret());
$access_token_req = new OAuthRequest("GET", $this->access_token_endpoint);
$params['oauth_verifier'] = $this->getUserVerifier();
$access_token_req = $access_token_req->from_consumer_and_token($this->consumer,
$auth_token, "GET", $this->access_token_endpoint, $params);
$access_token_req->sign_request(new OAuthSignatureMethod_HMAC_SHA1(),$consumer,
$auth_token);
$after_access_request = $this->doHttpRequest($access_token_req->to_url());
$access_tokens = array();
parse_str($after_access_request,$access_tokens);
# line 234 below
$access_token = new OAuthConsumer($access_tokens['oauth_token'], $access_tokens['oauth_token_secret']);
// prepare for get profile call
$profile_req = $access_token_req->from_consumer_and_token($consumer,
$access_token, "GET", $this->api_url.'/v1/people/~');
$profile_req->sign_request(new OAuthSignatureMethod_HMAC_SHA1(),$consumer,$access_token);
$after_request = $this->doHttpRequest($profile_req->to_url());
var_dump($after_request);
}
The function var_dumps a string, which is the basic synopsis of my profile:
string(402) " User Name etc. etc. http://www.linkedin.com/profile?viewProfile=&key=28141694&authToken=HWBC&authType=name&trk=api*a137731*s146100* "
That's good. However, the minute I refresh the page, the same function call fails with:
Undefined index: oauth_token, line number: 234
(this line marked with comment in above code block).
Then, of course, the var_dump reports this error from LinkedIn:
string(290) " 401 1310652477038 R8MHA2787T 0 [unauthorized]. The token used in the OAuth request is not valid. "
something to note:
the user token, secret, and verifier are persisted during the initial authorization callback (right before this function is called). So, they are the same during the first call (when it works, right after coming back from linkedin) and during a page reload (when it fails on line 234).
Also, I must admit I'm not 100% sure I understand everything that's going on in this function. I actually took examples from this tutorial (about a different service, not linkedin) http://apiwiki.justin.tv/mediawiki/index.php/OAuth_PHP_Tutorial and combined it with the information I gathered from the LinkedIn API documentation, spread throughout their developer site. Most notable was the addition of the 'verifier' which the tutorial did not use.
Any insight into this problem would be greatly appreciated. Thanks in advance.
-Nick
UPDATE
The only way I've been able to get this going is to do a new OAuth handshake every single time. Is this the way it's supposed to happen? I was under the impression that once I got my user token/secret and verifier, that I could then use these for continuous API calls until the token expired or was revoked.
As it is now, every time the page reloads I'm requesting a new user token, secret and verifier, then immediately calling to get the user profile (which succeeds). Next reload, I get a whole new key/secret and verifier. Seems like quite a lot of work for each call, and as I understood it, you should be able to perform offline operations with this method - and if I need new authorization each time, then I guess I can't do that?
Well. I've finally figured out what was going on so thought I'd post the answer here, just in case someone else runs into this.
The example that I was using as a guide was flawed. After the access token is retrieved, you should then create a new OAuthRequest object, instead of using the existing $access_token_req instance.
So this:
// prepare for get profile call
$profile_req = $access_token_req->from_consumer_and_token($consumer,
$access_token, "GET", $this->api_url.'/v1/people/~');
$profile_req->sign_request(new OAuthSignatureMethod_HMAC_SHA1(),$consumer,$access_token);
$after_request = $this->doHttpRequest($profile_req->to_url());
Should be changed to this:
$api_req = new OAuthRequest("GET", $this->api_url.$api_call);
// prepare for get profile call
$api_req = $api_req->from_consumer_and_token($consumer,
$access_token, "GET", $this->api_url.'/v1/people/~');
$api_req->sign_request(new OAuthSignatureMethod_HMAC_SHA1(),$consumer,$access_token);
$after_request = $this->doHttpRequest($api_req->to_url());