Processing Your Own Online Payments - A Brief Tutorial [Part 3]
January 17th, 2008This is the third part of an ongoing series on setting up your own online store.
- Part 1: Client-Side Storefront
- Part 2: Server-Side Validation and Routing
- Part 3: Server-Side Payment Processing
- Part 4: Adding PayPal Support
Once again, I’m using a PHP/JavaScript system because that’s what I’m familiar with. If your webhost supports Ruby on Rails, then you’ll save a lot of time by downloading the Potion Store from the folks over at Potion Factory.
And once again a discalaimer:
Disclaimer: I am not an accountant or lawyer, before doing anything involving other people’s money you should check with a lawyer and an accountant.
I’d like to thank everyone that gave me feedback from the previous article, please continue to do so.
Site Map
At the end of this series, I will have described a store that looks more or less like this:

-
store.php
This is the page someone comes to when they have chosen to pay with their own credit card.
As I mentioned last time, the store.php page now contains everything that we moved out of our original storefront that we need to process the credit card transaction.
Most of this is just plain HTML forms stuff which we went over in part 1, so I won’t go over it again here.
One thing that I want to highlight again however is the use of the PHP session_start command at the start of this (and every php page):
<?php session_start(); ?>
Once this command has been called, I can now retrieve any value that was saved as a session variable. For example, here is how I retrieve the total price of the order and format it to two decimals places:
<?=number_format($_SESSION['amountTotal'],2) ?>
Once the customer information is entered into this page, client side validation is done (which we covered in part 1), the form is submitted in the standard way to confirm.php
-
confirm.php
The confirm.php page is where the customer has the chance to validate all of their payment information. It also allows us to get things setup for the final processing of payment information. Here is the relevant code from confirm.php:
- <?php
- session_start();
- require_once ‘./include/storeSettings.php;
- if($_SESSION['paymentMethod'] == “cc”)
- {
- // Store the customer information in session variables except for the sensitive data
- $_SESSION['firstName'] = $_POST["FirstName"];
- $_SESSION['lastName'] = $_POST["LastName"];
- $_SESSION['company'] = $_POST["Company"];
- $_SESSION['address1'] = $_POST["Street1"];
- $_SESSION['address2'] = $_POST["Street2"];
- $_SESSION['city'] = $_POST["City"];
- $_SESSION['state'] = $_POST["State"];
- $_SESSION['country'] = $_POST["Country"];
- $_SESSION['zip'] = $_POST["ZipCode"];
- $_SESSION['paymentSource'] = “************” . substr($_POST["CreditCardNumber"], (strlen($_POST["CreditCardNumber"]) - 4), 4);
- $_SESSION['email'] = $_POST["Email"];
- // Encrypt sensitive data before putting it into session variables
- $_SESSION['S1'] = encryptString($_POST["CreditCardNumber"]);
- $_SESSION['S2'] = encryptString($_POST["CVV2"]);
- $_SESSION['S3'] = encryptString($_POST["CCMonth"]);
- $_SESSION['S4'] = encryptString($_POST["CCYear"]);
- $payButtonText = “Complete Purchase”;
- }
- else
- {
- .
- // Pay Pal stuff coming in Part 4
- .
- }
- ?>
- <html>
- <head>
- .
- .
- .
- <form name=’form1′ method=’post’ action=’process.php’>
- .
- .
- .
- <input class=’storePayButton’ type=’submit’ value=’<?=$payButtonText ?>’>
- </form>
On line 4 we check to see if we are processing the credit card ourselves (as opposed to PayPal, which we’ll discuss in Part 4). Then on lines 6 - 22 we proceed to gather up the customer data from the form posting and store them into session variables.
There are two important security related things to notice here. First of all on line 16 we are storing the credit card information that we display back to the user for confirmation. Notice that we don’t use the whole credit card number, just the last 4 digits. Credit card information should never be shown in full on the confirmation screen.
Lines 18 - 22 deal with another security issue. Since we obviously still need to have the customer’s credit card number to process the payment, and since that processing doesn’t happen until after the confirmation page, we need a way to pass the credit card information to our processing page. Now, since this is very sensitive information we have to be very careful with how we deal with it. Here is the line of thinking that led to the above solution:
- Store it in a hidden field on a form - This is only one degree of separation from displaying it in the browser to the user, and is therefore too insecure.
- Store it in our database and delete it later - Bad idea. Not anywhere in this process will I store anyone’s payment information in a database and neither should you. Even if you plan on removing it at the end of the transaction. As a rule of thumb, credit card information and databases should never go together.
- Store it in a session variable - This is pretty secure if we’re using an ssl certificate and then it won’t be shown to the user right? Well almost. If you are the only one using the server, and it is your server, then this could be a moderately good idea. However if you are on any type of shared hosting plan, there is a chance (albeit slim), that some other user could access your session variables.
- Use encrypted session variables - Use session variables, but encrypt the values before storing them. This is what we are going to do.
Back in part 2 we talked about our storeSettings.php file where we stored our database connection information. Recall that this is in a password protected directory somewhere outside the web root. In addition to the database connection info, we also have the following functions defined:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
<?php . . . // Secret encryption stuff define('ENCRYPT_KEY', 'some_really_good_password'); define('ENCRYPT_ALGORITHM', MCRYPT_BLOWFISH); define('ENCRYPT_MODE', MCRYPT_MODE_CFB); define('ENCRYPT_RANDOM_SOURCE', MCRYPT_DEV_URANDOM); function encryptString($stringToEncrypt) { $iv = mcrypt_create_iv(mcrypt_get_iv_size(ENCRYPT_ALGORITHM, ENCRYPT_MODE), ENCRYPT_RANDOM_SOURCE); return base64_encode($iv . mcrypt_encrypt(ENCRYPT_ALGORITHM, ENCRYPT_KEY, $stringToEncrypt, ENCRYPT_MODE, $iv)); } function decryptString($stringToDecrypt) { $toDecrypt = base64_decode($stringToDecrypt); $ivSize = mcrypt_get_iv_size(ENCRYPT_ALGORITHM, ENCRYPT_MODE); $iv = substr($toDecrypt, 0, $ivSize); $data = substr($toDecrypt, $ivSize); return mcrypt_decrypt(ENCRYPT_ALGORITHM, ENCRYPT_KEY, $data, ENCRYPT_MODE, $iv); } . . . ?>
On lines 6 - 9 we define some information that we always want our encryption routines to use, like the encryption algorithm and the key used to encrypt and decrypt the string.
Lines 10 - 14 define the encryptString function, which takes a string and returns the encrypted version.
Lines 15 - 22 define the decryptString function, which takes an encrypted string and returns the decrypted version.
-
process.php
Finally we come to process.php, the page that will actually handle the credit card validation and complete the transaction. There are lots of things going on here, so I’m going to break the code up into chunks.
- <?php
- session_start();
- require_once “./include/StoreSettings.php”;
- require_once “./include/lphp.php”;
- // Connect to DB
- $conn = mysql_connect(DB_HOST, DB_USER, DB_PASS) or header(”Location: ” . ERROR_PAGE . “301″); // Error connectiong to DB
- mysql_select_db(DB_NAME);
- if($_SESSION['isUpgrading'])
- {
- $UpgradeCode = mysql_escape_string($_SESSION['upgradeCode']);
- $query = “UPDATE UpgradeCodes set Used=’1′ WHERE Code = ‘$UpgradeCode’”;
- mysql_query($query) or header(”Location: ” . ERROR_PAGE . “302″); // Error updating upgrade code table
- }
The first interesting thing that we do here in lines 8 - 13 is mark the upgrade code as having been used. This prevents multiple people from using the same upgrade key. Notice that if this is some type of reusable coupon code (such as for MacSanta), you wouldn’t want to invalidate the code in this step. How you handle that depends on what type of coupon code you’re dealing with.
- // Sanitize Values
- $FirstName = mysql_escape_string($_SESSION['firstName']);
- $LastName = mysql_escape_string($_SESSION['lastName']);
- //Run Query to add the user
- $query = “INSERT INTO Customers(FirstName, LastName, Company, Address1, Address2, City, State, Country, Zip, PaymentSource, Email, ChargeTotal, OrderTime) “;
- $query .= “VALUES (’$FirstName’, ‘$LastName’, ‘$Company’, ‘$Address1′, ‘$Address2′, ‘$City’, ‘$State’, ‘$Country’, ‘$Zip’, ‘$PaymentSource’, ‘$Email’, ‘$AmountTotal’, NOW())”;
- mysql_query($query) or header(”Location: ” . ERROR_PAGE . “303″); // Error creating user record
- $customerID = mysql_insert_id();
.
.
.
Lines 14 - 16 begin sanitizing the values that we are going to insert into our database. This prevents nasty SQL injection attacks from happening. As I mentioned before, if you’re not sure whether magic quotes is enabled on your sever, you need to check by calling the get_magic_quotes function before doing this.
In lines 18 and 19 we build up our insertion query statement that we use to insert the customer data into our database. Notice again that the credit card information is NOT stored in the database. If you have to store something for payment identification later, only store the last 4 digits of the card number that was used on the verification screen earlier.
Finally on line 20 we run the query, and make sure that everything worked out ok, then on line 21 we retrieve the customer id (aka the primary key) for the record we just inserted. This is a unique key that refers to that row that was auto-generated by the database. We will use this key to link the records between this table and our products table, which I’ll discuss soon. If you don’t understand why a unique key is important, it may be a good idea to review database normalization guidelines before building your customer database.
- $ProductQuantity = mysql_escape_string($_SESSION['productQuantity']);
- $ProductPrice = mysql_escape_string($_SESSION['productPrice']);
- $UpgradeCode = mysql_escape_string($_SESSION['upgradeCode']);
- $query = “INSERT INTO Products(CustomerID, ProductCode, Quantity, Price, UpgradeCode) “;
- $query .= “VALUES (’$customerID’, ‘123′, ‘$ProductQuantity’, ‘$ProductPrice’, ‘$UpgradeCode’)”;
- mysql_query($query) or header(”Location: ” . ERROR_PAGE . “304″); // Error creating product record
Here is where the Products table comes in. In lines 22 - 27 we insert a record into the products table containing the customer ID that we retrieved earlier. Why have two separate tables? Well if you have multiple products and one customer buys two different things, this allows you to avoid duplication of information. Notice that our example doesn’t handle multiple products at all except conceptually in the database design. However modifying the example to do that would be relatively trivial for someone with a moderate amount of programming skills.
- // Process payment
- $paymentApproved = false;
- $paymentResponse = “”;
- if($_SESSION['paymentMethod'] == “cc”)
- {
- $mylphp=new lphp;
- $myorder["host"] = “super.secure.ccprocessor.com”;
- $myorder["port"] = “90210″;
- $myorder["keyfile"] = “./include/myCert.pem”;
- // Credit Card Data
- $myorder["cardnumber"] = decryptString($_SESSION['S1']);
- $myorder["cvmvalue"] = decryptString($_SESSION['S2']);
- $myorder["cardexpmonth"] = decryptString($_SESSION['S3']);
- $myorder["cardexpyear"] = decryptString($_SESSION['S4']);
- $myorder["chargetotal"] = $_SESSION['amountTotal'];
- // Billing Data
- $myorder["name"] = $_SESSION["firstName"] . ” ” . $_SESSION["lastName"];
- $myorder["address1"] = $_SESSION["address1"];
- // Send transaciton
- $ccResult = $mylphp->curl_process($myorder);
- $paymentResponse = $ccResult["r_ordernum"] . “|” . $ccResult["r_ref"] . “|” . $ccResult["r_approved"] . “|” . $ccResult["r_code"] . “|” . $ccResult["r_message"] . “|” . $ccResult["r_avs"];
- $paymentApproved = ($ccResult["r_approved"] == “APPROVED”);
- }
- else
- {
- // Pay Pal stuff coming in Part 4
- }
.
.
.
Now begins the actual credit card processing. This section may vary somewhat depending on whom you use for your credit card processing, but if you use LinkPoint like I do, then it could look very similar.
Lines 33 - 36 begin by instantiating the lphp object provided by the LinkPoint library we included on line 4. This is the object that we will send over a secure connection to LinkPoint. On lines 34 - 36 we provide some basic configuration information, and on lines 44 - 45 (and beyond through the ellipsis) we provide the customer information.
In lines 38 - 42 we retrieve our encrypted session variables which contain the credit card processing data, decrypt them and assign them to the appropriate keys in our LinkPoint object. (We used the decryptString function which we defined above to do this)
Finally on line 47 we send the transaction to LinkPoint using the curl_process function, which is defined in the LinkPoint library. This function basically opens a secure connection to the LinkPoint servers, sends the data in the $myorder variable and stores the response in $ccResult.
Lines 48 and 49 parse the response information. There is a lot of information stored in various keys that may be handy later, but the only thing I really need separate at this point is the approval code, which is retrieved on line 49. Since it may turn out that I do need some other part of the response for something later, I do store the rest of it in the database at a later point in the process.
- if($paymentApproved)
- {
- // Update Database with CC Approval Results
- $escapedResponse = mysql_real_escape_string($paymentResponse);
- $query = “UPDATE Customers set Approved=’1′,CCResult=’$escapedResponse’ WHERE CustomerID = ‘$customerID’”;
- mysql_query($query);
- // Check CCV Code Results for CC orders
- if($_SESSION['paymentMethod'] == “cc”)
- {
- $CCVResult = substr($result["r_avs"], 3, 1);
- if($CCVResult == “N”)
- {
- // Void bad ccv codes
- $myorder["ordertype"] = “VOID”;
- $myorder["oid"] = $result["r_ordernum"];
- $result = $mylphp->curl_process($myorder);
- $voidResponse = $result["r_ordernum"] . “|” . $result["r_ref"] . “|” . $result["r_approved"] . “|” . $result["r_code"] . “|” . $result["r_message"];
- $query = “UPDATE Users set Voided=’1′,VoidResult=’$voidResponse’, VoidTime=NOW() WHERE OrderID = ‘$orderID’”;
- mysql_query($query);
- // Send to error page with error for bad CVV code
- header(”Location: ” . ERROR_PAGE . “306″);
- }
- }
Now, here is a subtlety that may not bother you if you aren’t using LinkPoint. Remember that little CCV code (the security code on the back of the card)? It turns out that if everything else about the order checks out, the order will come back approved even if the number the customer enters for their security code is completely bogus. Why is that? Well it is mainly because in some uses of the LinkPoint API the CCV isn’t collected. (Like when the customer swipes their card in a terminal). But since this is a “Card not present” situation, it is very important that you verify the CCV code.
The first thing we do on lines 57 - 60 is store the entire credit card processing response in the database. Note again, that this does not include any of the credit card information, just the response from the processor.
On line 62 we make sure that this is a credit card transaction (as opposed to PayPal) before we try and check the CCV code. Next on line 64 we retrieve the character from the avs response code that indicates whether the security code was good. If it wasn’t good, lines 67 - 75 void the transaction both with the credit card processor and in our local database, and then forwards the customer to an appropriate error page.
If everything checks out ok, we proceed to lines 79 - 81 when generates a unique license key for our customer:
- // Create License Key
- $stringToEncode = generateUniqueAndSecureString();
- $licenseKey = md5($stringToEncode);
- $formattedLicenseKey = “SM21-” . rtrim(chunk_split(strtoupper($licenseKey), 4, “-”), “-”);
- // Update product record with license key
- $query = “UPDATE Products set LicenseKey=’$formattedLicenseKey’ WHERE CustomerID = ‘$customerID’”;
- mysql_query($query);
- // Email results
- $receiptText = “Thanks for your purchase! blah blah blah….”;
- $receiptText .= “More important stuff.”;
- mail($_SESSION["email"], “Your Receipt”, $receiptText, “From:sales@me.com\r\nReply-to:productsupport@me.com\r\nBcc: receipts@me.com”);
- }
I’m using a pretty simplistic license key generation scheme here. It involves three steps:
- Come up with a secret unique string
- Hash it using MD5
- Format it to make it look like a license key
The security of your license scheme will all come down to how you perform step 1. I’m sure that there are well researched and tested methods for this, but don’t waste too much time on it. As Brent Simmons once said:
There are two kinds of people in this world, those that pay for software and those that don’t, and never the twain shall meet.
On lines 83 - 84 we update the products table with the license key we generated for that customer. Notice that despite all of my harping on about database normalization that this code won’t work if you have multiple products. (Unless they all accept the same license key). If you did have multiple products, you would need to loop through lines 78 - 84 for each product you sold in that order.
Lines 86 - 87 build the string we use for our confirmation receipt. Some people use fancy rich text or html, I just use plain text. However you setup your email receipt, you should include the license code and registration instructions here, this will save you quite a bit of support time later.
Line 88 actually mails the receipt, notice that the “From” field is a dummy sales email address, while the Reply-To value is set to a product support email address. Also notice that another email address is BCC’d, this allows me to be immediately notified of all of my sales which gives me a warm fuzzy feeling.
- else
- {
- // Update Database with Failure code
- $query = “UPDATE Customers set Approved=’0′,CCResult=’$ccResponse’ WHERE CustomerID = ‘$customerID’”;
- mysql_query($query);
- header(”Location: ” . ERROR_PAGE . “307″); // Error validating payment
- }
- ?>
Finally on lines 90 - 96 we deal with what happens if the payment wasn’t approved by the credit card processor. We simply update the database with the failure code sent back from the processor and redirect the customer to an error page. This failure code is very useful to have when your customer emails you and want to know why you aren’t accepting their card. It is not however an all knowing response that will help you know the exact problem.
Basically there are three types of responses that you’ll be able to discern. Bad Address, bad CCV code, and rejected. (There is also a fraud response that link point sends but that usually just means the customer tried to process the card again to quickly). Bad address and bad ccv code are pretty self explanatory. The other code is not.
If you customer contacts you and wants to know why the order was rejected, then all you can do is have them call their issuing bank to find out. It will be a waste of time for you to try and ask your processor, as they won’t tell you anything other than it was rejected. (Nor should they, since the “why” is really none of your business).
Finally lines 98 - 108 show some sample HTML code that appears to the user upon successful completion of this process:
- <!DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN” “http://www.w3.org/TR/html4/loose.dtd”>
- <html>
- <body>
- <div class=’storeResponseText’>Your order has been approved! Here is your registration information (This information will also be sent to you via the email address you
- provided) :</p>
- <table border=’0′ class=’licenseTable’>
- <tr><td class=’storeResponseText’>User Name:</td><td class=’licensekey’><?=$_SESSION["firstName"] . ” ” . $_SESSION["lastName"] ?></td></tr>
- <tr><td class=’storeResponseText’>Email Address:</td><td class=’licensekey’><?=strtoupper($_SESSION["email"])?></td></tr>
- <tr><td class=’storeResponseText’>Product Key:</td><td class=’licensekey’><?=$formattedLicenseKey?></td></tr>
- </table>
- </div>
.
.
.
I include the same registration information as the email receipt, but you can of course include whatever you want.
Conclusion and Words of Warning
If you think that the above code has a lot of places where things could go wrong, you are right. You need to test every possible failure path that you can think of. This is where having a test plan really comes in handy. Test, test, and retest. Your credit card processor should have a test mode that you can use to process test payments. Use this mode to make sure that things happen the way they should with a bad card number, bad security code, bad upgrade key, etc…
Make sure that if the customer’s name is something like “John O’; DROP TABLE Customers; –’”, that you handle that case correctly. Finally when you switch to live mode, test some more. Test every case again with your own credit card. Don’t worry, you should be able to void your transactions after each test. (If you can’t you should really switch payment processors). Test to make sure that a cleared payment does in fact appear in your bank account. Test everything. Test Test Test. The next day, test some more. Every time you make a change test EVERYTHING again.
If you see an unusually long stretch of time without sales, test again. Just last week we had three days of seemingly no sales, but it turned out that there was just a problem with PHP’s mail setup that our host made which prevented the email from being sent. Fortunately the sales were still stored in the database so we didn’t lose any information.</soapbox>
Finally another word of warning: I am not an accountant or a lawyer, before doing anything involving other people’s money you should check with a lawyer and an accountant.
Lee,
Another excellent article! Thank you for taking to time to write all of this up, it has been a tremendous help in planning my own store.
@Chris
I’m glad you like it, hopefully I can get part 4 posted sooner than it took me to finish part 3.
Looks very promising with the part 4 coming up. I am looking into solutions at this very point and, well the fourth part is kinda vital for a solution. I love that you take the time and really explain what you are doing. Especially since most people looking for a web store solution is probably not making there living by creating web stores.