So I'm using the Paypal PHP SDK on Github, http://paypal.github.io/PayPal-PHP-SDK/ . Some strange behavior I've noticed which I'm not sure what's going on.
So let's say I create a billing plan, but don't touch it after creation, so that the state is simple CREATED. Everything is good, I can retrieve it from the list of plans. However, the moment I change the state to ACTIVE via a patch, I can see that it is in fact active, but only just once. Any subsequent attempts to see the list of plans no longer shows that plan. What's going on? I'm literally copy pasting the example source they give.
Edit - just to expand, I know the plan still exists, because I can subscribe users to it. Weirdly the paypal page where you click ok to subscribe is extremely non verbose... doesn't even say what the price is, just to approve paying my store. And yet the Agreement object that is returned by PayPal, which includes the approval url, has all this info. Weird.
If you are using the PayPal-PHP-SDK, you could assign more parameters to Plan::all() method.
As shown in the List Plan sample code, you could pass parameter 'status' as :
try {
// Get the list of all plans
// You can modify different params to change the return list.
// The explanation about each pagination information could be found here
// at https://developer.paypal.com/webapps/developer/docs/api/#list-plans
$params = array('page_size' => '20', 'page' => '98', 'status' => 'ACTIVE');
$planList = Plan::all($params, $apiContext);
} catch (Exception $ex) {
ResultPrinter::printError("List of Plans", "Plan", null, $params, $ex);
exit(1);
}
As in the case, you could change the status, and page along with page_size. This will help you get the active list of plans.
Actually, by default the list plan status is defaulted to CREATED.
Related
Of the three files here- https://github.com/paypal/ipn-code-samples/tree/master/php
I have my Webhook URL set to the stock github version of- PaypalIPN.php (this validates successfully 100% of the time, if I use example_usage.php... Doesn't work. If I use both as Webhooks... Doesn't work).
From the Paypal button side of things I'm able to post my website's active user (call him $MrUser) with this:
createOrder: function(data, actions) {
return actions.order.create({
purchase_units: [{
"custom_id":"<?php echo $MrUser; ?>",
"description":"One hundred Webdollars",
"amount":
{
"currency_code":"USD",
"value":1.99
}
}]
});
},
Here's the SQL I need to run upon successful validation (I change $MrUser to $kitty for clarity's sake):
require 'sqlconfig.php';
$dsn = "mysql:host=$host;dbname=$db;charset=UTF8";
try {
$pdo = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
echo $e->getMessage();
}
$hashedIP = $_SERVER['REMOTE_ADDR'];
$kitty = $_POST['custom']; // Not sure this is working yet, but this should give me $mrUser;
$sql = "INSERT INTO `Insert_upon_Paypal_success` (`date`,`hashedIP`,`username`,`webdollarAMT`) VALUES (now(),:hashedIP,:kitty,'100')";
$statement = $pdo->prepare($sql);
$statement->bindValue(':hashedIP', $hashedIP);
$statement->bindValue(':kitty', $kitty);
$inserted = $statement->execute();
I'm popping this into the PaypalIPN.php file upon validation, but, it doesn't work. Here's how I have it in there:
// Check if PayPal verifies the IPN data, and if so, return true.
if ($res == self::VALID) {
return true;
// i.e. putting all of the SQL above right here.
} else {
return false;
}
I'm guessing I need to put the SQL in a specific place that I'm missing, as per the layout of the PaypalIPN.php file... Please help!!
There is no reason to use IPN with current PayPal Checkout integrations. It is very old technology (20+ years) and should be deprecated soon.
Webhooks are a successor to IPN. However, even they are unnecessary for normal payment processing -- better used only if you need automated notifications of post-checkout exceptions such as refunds or disputes.
For normal PayPal payments, do not use either.
Instead, use the v2/checkout/orders API and make two routes (url paths) on your server, one for 'Create Order' and one for 'Capture Order'. You could use the (recently deprecated) Checkout-PHP-SDK for the routes' API calls to PayPal, or your own HTTPS implementation of first getting an access token and then doing the call with PHP's curl or similar. Both of these routes should return/output only JSON data (no HTML or text). Inside the 2nd route, when the capture API is successful you should verify the amount was correct and store its resulting payment details in your database (particularly purchase_units[0].payments.captures[0].id, which is the PayPal transaction ID) and perform any necessary business logic (such as reserving product or sending an email) immediately before forwarding return JSON to the frontend caller. In the event of an error forward the JSON details of it as well, since the frontend must handle such cases.
Pair those 2 routes with this frontend approval flow: https://developer.paypal.com/demo/checkout/#/pattern/server . (If you need to send any additional data from the client to the server, such as an items array or selected options, add a body parameter to the fetch with a value that is a JSON string or object)
This seems a very naive set of questions, but I really couldn't find it.
Question 1) I am trying to implement IPN for PayPal. I want to enter The listener URL, but I couldn't find where. I've searched the internet and all the responses are old. Where could I put it?
Question 2) I am using the following code and I want to pass custom data so that when IPN sends me back the transaction message, It will have that custom data in it. I need to pass 2 custom data. How is this possible for the following code? I really couldn't find this one in the docs too.
<template>
<div id="paypal-button-container"></div>
</template>
<script>
export default {
props: ["amount"],
mounted() {
let self = this;
paypal
.Buttons({
createOrder: function(data, actions) {
// Set up the transaction
return actions.order.create({
purchase_units: [
{
amount: {
value: self.amount,
}
}
],
});
},
onApprove: function(data, actions) {
// Capture the funds from the transaction
return actions.order.capture().then(function(details) {
// Show a success message to your buyer
console.log(details);
self.$emit("paypalPaySuccess", {
order_id: details.id,
})
alert("Transaction completed by " + details.payer.name.given_name);
}).catch((err)=>{
})
}
})
.render("#paypal-button-container");
}
};
</script>
Addition of some questions
I am using all the events in dashboard.
I need to add some more questions, because I can't find the information I need.
Question 3) In client-side, I make action.orders.capture().then . which means that I capture funds immediatelly. When webhooks come to my back-end, there're 2 webhooks that come for single payment . (PAYMENT.CAPTURE.PENDING and CHECKOUT.ORDER.APPROVED) . a) why doesn't PAYMENT.CAPTURE.COMPLETED arrive as webhook event? b) what if I want to get DENIED events? If PAYMENT.CAPTURE.COMPLETED doesn't arrive, PAYMENT.CAPTURE.DENIED won't arrive too.
Question 4) As I said, there're 2 webhook events appearing for single payment on my back-end. The first one has id in it. Let's say I store this in my database as id and status(PENDING). Then another event came which is CHECKOUT.ORDER.APPROVED. Now, this one has different id then the previous one. So I can't really go back to database and update the status as this has different id. What should I do? One thing I noted is that the second came event has also another id (the previous came event) somewhere in captured object. is this what i should use it? which one is the final transaction_id?
Question 5) Turns out invoice_id should be unique each time. So I wanted to pass user_id but i guess i also should generate random string and append it to user_id something like this: invoice_id: '7,randomstring'. right?
Question 6) Same event sometimes comes twice. Why is that? Should I return status 200 in order for that same event not to come again? When will it be good to return that status? I guess after I update the database, right?
Q1: IPN is deprecated. You should be using webhooks, such as CHECKOUT.ORDER.APPROVED
Q2: There does not appear to be any specific way to pass custom data.
Option 1:
What I ended up doing was using the invoice_id field in purchase_units. I tested and found that I could pass a reasonably long string of characters and they did not have to be unique. It seems a bit of a hack but it works.
When you implement the CHECKOUT.ORDER.APPROVED webhook, you can then parse the data in purchase_unit invoice_id.
Option 2:
Another idea I considered which doesn't need to hack the invoice_id, is to send data to the server using the details from the capture event in the javascript, store that custom information, along with the order id in the database, and then when the webhook occurs, cross-match the order id to the saved one to retrieve the information.
I prefer option 1, but your needs may vary.
It's pretty indicative though of how poorly designed the whole PayPal API is. There are huge gaps in the implementation, very little documentation, and zero support.
I had to implement Stripe as well. The entire implementation took me 3 days. The same PayPal implementation took me over 3 weeks! The lack of documentation, having to constantly just try things out, and the inconsistency of the sandbox implementation were real time killers.
First - fair warning for any new user, stay away from appcelerator, that team should be ashamed of the work they have performed. Read on for an example. If I am wrong I will happily admit it. The customer's data already exists and mobile application is in production using existing field names.
I am performing batch_create with (paid) account on arrowdb against their arrow cloud data source. Single queries all work well so I know there are no key or general connectivity issues. Login works, I can do manual or automatic session mgmt and all works well for queries, creates, deletes, etc.
I am using the node arrowdb 1.06 library.
Now we get to batch_create which my code looks like this after a login:
arrowDBApp.post("/v1/objects/EM/batch_create.json",
{"json_array": [
{
old_id: '537280ca1316e90db2335bf7',
old_acl_id: '5372800e1316e90da8331dac',
idp_id: '17',
reference: '17',
serves: [ '52a7c0a0534b2b0b5000002b' ],
meter: 'NO METER',
amps: '110',
property_id: '52a7c02612f73e0b1e0032f7'
}
]},
function (err, result) {
if (err) {
console.error(err.message);
}
else {
console.log(result.body.meta);
}
}
);
With this code I get this error:
Request returned with HTTP status code 400 Invalid object at index 0: Invalid BSON id 17
And if I change the field name "idp_id" to "idpid" the problem goes away and the batch is created. Or, if I change the content from "17" to any GUID "xxxxxxxxxxxxxxxxxxxxxxxx" format.
So the question is, does anyone know a way to have "_id" in a field name without the API validating the content as having to be in the GUID format? Like I said the data exists already and we do not want to change field names and have to update the client apps (web app, mobile app, etc.).
Why would they simply validate any field with '_id'in it as a guid? They don't do it on single creates? I have hundreds of thousands of objects that have to be inserted and would like to batch create them.
thanks for any assistance. Never had anything but poor experience with ACS, always go AWS folks....
I want to test hiding and unhiding of an entry. I conduct the following tests in Mojolicious t/basic.t:
my $t = Test::Mojo->new('AdminApi');
$t->get_ok('/publications/hide/1');
$t->get_ok('/read/publications/meta')->content_unlike(qr/Paper with id 1:/i);
$t->get_ok('/read/publications/meta/1')->content_like(qr/Cannot find entry id: 1/i);
$t->get_ok('/publications/unhide/1');
$t->get_ok('/read/publications/meta')->content_like(qr/Paper with id 1: <a href/i);
$t->get_ok('/read/publications/meta/1')->content_unlike(qr/Cannot find entry id: 1/i);
My problem is that the two lines '/publications/hide/1' and '/publications/unhide/1' do not hide and unhide the entry. The state of the entry remains untouched.
If I repeat the steps manually in the browser everything works well. For the obvious reasons I want to have it automated with the tests. How to do this?
EDIT: The calls '/publications/hide/1' and '/publications/unhide/1' change the state of the database - just a simple UPDATE query. The change applies to the whole application - for all users. But one needs to be logged in as a user to hide/unhide. Question: how do I emulate a logged user during the test?
Contents generated by '/read/publications/meta' and '/read/publications/meta/1' can be read without login.
Bitbucket Repo
File with test code: basic.t
As you have already said, you need to be logged in to perform the hide and unhide action.
my $t = Test::Mojo->new('AdminApi');
You are creating a new UserAgent here. The Test::Mojo class inherits from Mojo::UserAgent. It has a cookie_jar and thus keeps a session alive. You need that to perform this action:
$t->get_ok('/publications/hide/1');
But right now you are not logged in. What you need to do is log in the user. Looking at the code in your repository, you actually assert that you are not logged in.
$t->get_ok('/')->status_is(200)->content_like(qr/Please login or register/i);
Before you perform the hide, you need to log in the user. After digging a bit in your code I found the action and the template to do that, so I know what the request needs to look like.
$t->post_ok(
'/do_login' => { Accept => '*/*' },
form => { user => 'admin', pass => 'hunter2' }
);
Now your $t UserAgent should be logged in and you can do the hide. Note that get_ok only checks if there was no transport error. So in fact it would make sense to now check if in fact you are now logged in.
You could do that by introspecting the session in the application, by checking the logfile (you are writing "Login success" there) or by checking the page for some string that says that the user is logged in. In templates/display/start.html.ep there is a text that welcomes the logged-in user, so you can use that.
$t->post_ok(
'/do_login' => { Accept => '*/*' },
form => { user => 'admin', pass => 'hunter2' }
)->text_like(qr/Nice to see you here admin/i);
Because text_like uses the text-nodes, the <em> around the username is not relevant in the test.
Right, now we know you are logged in. Time to switch the thing on and off.
$t->get_ok('/publications/hide/1');
Because there is no obvious error thrown for that as far as I can tell, I don't see how to test the success of that. Status code is one way, but there might be something in the content as well that you could test.
To verify the state of the application, you would now call the publication.
$t->get_ok('/read/publications/meta')->content_unlike(qr/Paper with id 1:/i);
$t->get_ok('/read/publications/meta/1')->content_like(qr/Cannot find entry id: 1/i);
Right. But remember, our $t is still logged in. Maybe the logged-in user is allowed to see hidden stuff as well as unhidden stuff. Maybe they are not.
It's probably safer to make a second UserAgent that's not logged in, and check with that one as well.
# check with an unauthorized user
my $t_not_logged_in = Test::Mojo->new('AdminApi');
$t_not_logged_in
->get_ok('/read/publications/meta')
->content_unlike(qr/Paper with id 1:/i);
$t_not_logged_in
->get_ok('/read/publications/meta/1')
->content_like(qr/Cannot find entry id: 1/i);
Now basically you repeat the same thing by unhiding your content and testing again. Rinse and repeat.
Keep in mind that unless you are using an explicit testing database (which you seem not to do), you cannot be sure that there even is an entry 1. Or what the name of that is. You should use fixtures for the tests. You could, for example, create a fresh instance of the DB using sqlite and work with that.
I am using a paypal ipn script i found here
http://coderzone.org/library/PHP-PayPal-Instant-Payment-Notification-IPN_1099.htm
I am aware that I can send information to paypal and get a response. It states I can get the information back using $_POST . My query is how do I specify the UK currency?
Also wanted to clarify a minor point. Am I correct that this is how i can confirm it was a success.
if ($_POST['payment_status'] == 'completed')
// Received Payment!
// $_POST['custom'] is order id and has been paid for.
}
This might be a little late for you sorry, but just in case - I currently use "currencyCode" = > "AUD" and it is working in the sandbox.
There's a full list of the currency codes available at PayPal
For yours, I'm guessing it would be:
$p->add_field('currencyCode', 'GBP');
As for your question about the IPN itself, it looks like you're on the right track. It will depend on the data you're getting back and whether you're interested in the individual transactions (if using adaptive payments) or if you're reversing them all on error etc. The easiest way to determine what you'll need to do is to simply display or log all the post data so you can see how it's constructed.
You'll also need to set it up so that the script is accessible by PayPal. You'll then pass the full URL of this script to the "notify_url" parameter and send it off to PayPal. Once the payment has completed PayPal will send a bunch of information to your script so that you can process it.
Unfortunately I'm not from a PHP background so I can't give you the exact code you'll need. Also note that there are a lot of security issues that you'll want to look into before going to a production environment. Not sure if you already intend to do this with that validateIPN function, but you need to ensure that you can tell whether it comes from PayPal and not a malicious user. One way would be to pass a value using the custom attribute and have PayPal pass this back to you, however you'd be much better off using the API certificates etc.
If you haven't already, it may be worth checking out a few of the sample applications PayPal has done up, there seem to be quite a few PHP ones.
Let me know if you need anything else,
Use this, it works for me
$p->add_field('currency_code', 'GBP');
You need to use PayPal Adaptive Payments, IPN wouldn't help.
PayPal Adaptive Payments
Using PayPal PHP library then it could look like this:
// Create an instance, you'll make all the necessary requests through this
// object, if you digged through the code, you'll notice an AdaptivePaymentsProxy class
// wich has in it all of the classes corresponding to every object mentioned on the
// documentation of the API
$ap = new AdaptivePayments();
// Our request envelope
$requestEnvelope = new RequestEnvelope();
$requestEnvelope->detailLevel = 0;
$requestEnvelope->errorLanguage = 'en_GB';
// Our base amount, in other words the currency we want to convert to
// other currency type. It's very straighforward, just have a public
// prop. to hold de amount and the current code.
$baseAmountList = new CurrencyList();
$baseAmountList->currency = array( 'amount' => $this->amount, 'code' => 'GBP' );
// Our target currency type. Given that I'm from Mexico I would like to
// see it in mexican pesos. Again, just need to provide the code of the
// currency. On the docs you'll have access to the complete list of codes
$convertToCurrencyListUSD = new CurrencyCodeList();
$convertToCurrencyListUSD->currencyCode = 'USD';
// Now create a instance of the ConvertCurrencyRequest object, which is
// the one necessary to handle this request.
// This object takes as parameters the ones we previously created, which
// are our base currency, our target currency, and the req. envelop
$ccReq = new ConvertCurrencyRequest();
$ccReq->baseAmountList = $baseAmountList;
$ccReq->convertToCurrencyList = $convertToCurrencyListUSD;
$ccReq->requestEnvelope = $requestEnvelope;
// And finally we call the ConvertCurrency method on our AdaptivePayment object,
// and assign whatever result we get to our variable
$resultUSD = $ap->ConvertCurrency($ccReq);
$convertToCurrencyListUSD->currencyCode = 'EUR';
$resultEUR = $ap->ConvertCurrency($ccReq);
// Given that our result should be a ConvertCurrencyResponse object, we can
// look into its properties for further display/processing purposes
$resultingCurrencyListUSD = $resultUSD->estimatedAmountTable->currencyConversionList;
$resultingCurrencyListEUR = $resultEUR->estimatedAmountTable->currencyConversionList;