Introduction
If you’ve once tried PayPal Payment service, you should know that PayPal provides several ways for users to pay and receive payment
Those are included in this article:
- How to pay via PayPal Payment Standard
- How to validate the payment result via PayPal IPN Message
- With PayPal Payment Standard, how to submit several items with their names, unit prices, and quantities.
In this article, the language I use is Laravel, the framework of PHP.
Since this article basically is a record of a project I was working, there may be some parts that don’t have so much to do with payment service.
You could skip those parts and start from ‘Make a payment order’
Validation
It’s part of the whole process, and doesn’t have anything to do with payment service. You could just skip it.
$toBeValidatedCondition = [ |
Collect required information.
When I was working, it seemed that I had more time than front end, lol, so I decided to take care as mush information as possible, managing to let front end do more things with the least information.
$toBeSavedInfo = [ |
Splitting point
Since I use two payment services in this project, so I need a place to determine where the request is from and should go
switch ($thirdPartyPaymentService->id) |
Make a payment order
Whenever the user call PAY API
, a temporary order will be built, which is only between the seller and PayPal.
Because I’m going to insert data into two tables at a time, we I am going to use Laravel’s Transaction to implement the data inserting work. If you are interested in Transaction of Laravel, you could refer to a short section in my another article, which I mentioned a bit.
public function make(Array $toBeSavedInfo, Request $request, Recipient $recipient) |
Make a URL for submitting the payment request to PayPal
Here we will use a lot of variables of PayPal Payment Standard. You could refer to the usage of every of them from this article
By the way, because when Ray doing this project, the front end’s schedule was a bit tighter than Ray, so Ray decided to handle as much information as possible. The backend will collect all those information from orders that have been built, and feed PayPal with those information. So basically the front end only has to let me know what orders the user want to pay.
public function send(Array $toBeSavedInfo, Request $request, Recipient $recipient) |
Pay the payment
- The user reach the PayPal payment page via the URL we produced above. Please register test account via link here
Reach payment page
Here we could see the items in detail, including price and quantity. Click continue to go on
Here we could see the shipping address we designated
Transaction completed, and here we could see all the details
Verify payment state
After the payment is completed, PayPal will send an IPN message to us. You could check the IPN in detail through the official document
Firstly, let’s install PayPal’s official IPN CODE SAMPLES
git clone https://github.com/paypal/ipn-code-samples
In
php
directorycd ipn-code-samples/php
And then, let’s copy
PaypalIPN.php into our project
, Ray personally put it underapp
Now, put the folder
cert
that is underipn-code-samples
also intoapp
as image below:In
composer.json file
, searchfiles
underautoload-dev
, and addPaypalIPN.php
. If there is nofiles
in yourcomposer.json
, you might need to create one yourself"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
},
"files": [
"app/Helpers.php",
"app/AllPay.Payment.Integration.php",
"app/PaypalIPN.php"
]
},On
terminal
window, in our project folder, executecomposer
commandcomposer install
Now basically we have everything except for the one last step. Copy the content of
example_usage_advanced.php
into wherever you want. It could be one of your controllers, or a function under certain class as follows:
The code below is a bit long. It should be almost the same as the original sample code except for some I put comment to, so you don’t have to go through every of them.public function listen(Request $request)
{
// Since the offical sample is to catch $_POST, so I convert the request of Laravel into POST. You could modify it on your own.
$_POST = $request->post();
// It shows if the payment is cleared.
$payment_status = $_POST['payment_status'];
// Remember the payment service order we passed to PayPal that I mentioned earlier
$merchant_trade_no = $_POST['custom'];
// Very important. We will use it later
$txn_id = $_POST['txn_id'];
$txn_type = $_POST['txn_type'];
// When the payment is paid
$payment_date = Carbon::parse($_POST['payment_date'])->setTimezone('UTC');
// The total amount
$mc_gross = $_POST['mc_gross'];
$mc_currency = $_POST['mc_currency'];
$enable_sandbox = env('PAYPAL_SANDBOX_ENABLESANDBOX');
// Use this to specify all of the email addresses that you have attached to paypal:
$my_email_addresses = array(env('PAYPAL_SANDBOX_MAIL'));
// Set this to true to send a confirmation email:
$send_confirmation_email = env('PAYPAL_SANDBOX_SEND_CONFIRMATION_EMAIL');
$confirmation_email_address = "buybuybuygogo@gmail.com";
$from_email_address = "test@gmail.com";
// Set this to true to save a log file:
$save_log_file = env('PAYPAL_SANDBOX_SAVE_LOG_FILE');
$log_file_dir = storage_path() . "/app/payment_logs";
// Here is some information on how to configure sendmail:
// http://php.net/manual/en/function.mail.php#118210
// Here is the function verifying the IPN message.
$ipn = new PaypalIPN();
if ($enable_sandbox)
{
$ipn->useSandbox();
}
$verified = $ipn->verifyIPN();
$data_text = "";
foreach ($_POST as $key => $value)
{
$data_text .= $key . " = " . $value . "\r\n";
}
$test_text = "";
if ($_POST["test_ipn"] == 1)
{
$test_text = "Test ";
}
// Check the receiver email to see if it matches your list of paypal email addresses
$receiver_email_found = false;
foreach ($my_email_addresses as $a)
{
if (strtolower($_POST["receiver_email"]) == strtolower($a))
{
$receiver_email_found = true;
break;
}
}
date_default_timezone_set("America/Los_Angeles");
list($year, $month, $day, $hour, $minute, $second, $timezone) = explode(":", date("Y:m:d:H:i:s:T"));
$date = $year . "-" . $month . "-" . $day;
$timestamp = $date . " " . $hour . ":" . $minute . ":" . $second . " " . $timezone;
$dated_log_file_dir = $log_file_dir . "/" . $year . "/" . $month;
$paypal_ipn_status = "VERIFICATION FAILED";
if ($verified)
{
// When it passes the if below, it means that the IPN is verified, so we could do something after that.
$paypal_ipn_status = "RECEIVER EMAIL MISMATCH";
if ($receiver_email_found)
{
$paypal_ipn_status = "Completed Successfully";
$PayPal = (new PayPal())->where('merchant_trade_no', $merchant_trade_no)->first();
// Here we check a few things as follows:
// 1. Check txd_id to prevent double processing some transaction that was previously processed. If this txd_id already exists in our database, we should ignore it.
// 2. Check mc_gross, which should be exactly the same as the total amount in our payment service order.
// 3. Check mc_currency to make sure it's the same as the one in our payment service order.
// 4. Check payment_status to make sure that the transaction is completed.
if ((!PayPal::checkIfTxnIdExists($txn_id)) && ($mc_gross == $PayPal->total_amount) && ($mc_currency == $PayPal->mc_currency) && ($payment_status == 'Completed'))
{
// Insert txd_id into the payment service order and update some values, which shows if this order is cleared or not.
$PayPal->update(['txn_id' => $txn_id, 'txn_type' => $txn_type, 'payment_date' => $payment_date, 'status' => 1, 'expiry_time' => null]);
$recipient = $PayPal->recipient;
$orderRelations = $PayPal->orderRelations->where('payment_service_id', 2);
// After updating the payment service order, we update the user order accordingly.
Order::updateStatus($orderRelations, $recipient);
// After everything is perfectly done, we send a notification mail to the buyer and seller
Helpers::mailWhenPaid($PayPal, $orderRelations);
}
}
} elseif ($enable_sandbox)
{
if ($_POST["test_ipn"] != 1)
{
$paypal_ipn_status = "RECEIVED FROM LIVE WHILE SANDBOXED";
}
} elseif ($_POST["test_ipn"] == 1)
{
$paypal_ipn_status = "RECEIVED FROM SANDBOX WHILE LIVE";
}
if ($save_log_file)
{
// Create log file directory
if (!is_dir($dated_log_file_dir))
{
if (!file_exists($dated_log_file_dir))
{
mkdir($dated_log_file_dir, 0777, true);
if (!is_dir($dated_log_file_dir))
{
$save_log_file = false;
}
} else
{
$save_log_file = false;
}
}
// Restrict web access to files in the log file directory
$htaccess_body = "RewriteEngine On" . "\r\n" . "RewriteRule .* - [L,R=404]";
if ($save_log_file && (!is_file($log_file_dir . "/.htaccess") || file_get_contents($log_file_dir . "/.htaccess") !== $htaccess_body))
{
if (!is_dir($log_file_dir . "/.htaccess"))
{
file_put_contents($log_file_dir . "/.htaccess", $htaccess_body);
if (!is_file($log_file_dir . "/.htaccess") || file_get_contents($log_file_dir . "/.htaccess") !== $htaccess_body)
{
$save_log_file = false;
}
} else
{
$save_log_file = false;
}
}
if ($save_log_file)
{
// Save data to text file
file_put_contents($dated_log_file_dir . "/" . $test_text . "paypal_ipn_" . $date . ".txt", "paypal_ipn_status = " . $paypal_ipn_status . "\r\n" . "paypal_ipn_date = " . $timestamp . "\r\n" . $data_text . "\r\n", FILE_APPEND);
}
}
if ($send_confirmation_email)
{
// Send confirmation email
mail($confirmation_email_address, $test_text . "PayPal IPN : " . $paypal_ipn_status, "paypal_ipn_status = " . $paypal_ipn_status . "\r\n" . "paypal_ipn_date = " . $timestamp . "\r\n" . $data_text, "From: " . $from_email_address);
}
// Reply with an empty 200 response to indicate to paypal the IPN was received correctly
header("HTTP/1.1 200 OK");
}
Conclusion
Above mentioned is the whole process of using PayPal Payment Standard and PayPal IPN Message to complete a transaction.
Here is breakdown of every individual step:
- A user clicks a PayPal button to kick off a checkout flow; your web application makes an API call; your back-office system makes an API call; or PayPal observes an event.
- PayPal HTTPS POSTs your listener an IPN message that notifies you of this event.
- Your listener returns an empty HTTP 200 response.
- Your listener HTTPS POSTs the complete, unaltered message back to PayPal.
- PayPal sends a single word back - either VERIFIED (if the message matches the original) or INVALID (if the message does not match the original).
Comments