Quantcast
Channel: Building Web Apps
Viewing all articles
Browse latest Browse all 27

Lab 5. Subscriptions and Credit Card Processing

$
0
0

In this lab, we’re going to enable visitors to our web site to subscribe and send us real money! Well, not quite real money, since this is using a test account at the credit card gateway, but all it would take is a real merchant account to make the money real.

1. Installing the Active Merchant plug-in

Because we’re going to need some of its features, even before we get to the gateway interface itself, let’s start by installing the Active Merchant plug-in. This is a mature and widely-used plug-in that supports more then 30 credit card gateways.

The NetBeans plug-in interface seems to have trouble installing this plug-in, so let’s do it from the command line:

   ruby script/plugin install http://activemerchant.googlecode.com/svn/trunk/active_merchant

This is a big plugin with lots of pieces, so the installation may take a minute or two.

2. The subscription and customer models and forms

First, we need a form for the subscriber to complete. This form needs to include information about the subscription, which we’ll store in the subscription model, about the customer, which we’ll store in the customer model, and about the credit card, for which we’ll use a non-database model provided by the Active Merchant plug-in.

a. Creating the customer and subscription scaffolds

As in previous labs, we’ll use the scaffold generator to get a quick start on our models, views, and controllers.

We’ll create a customer model, which has information about the customer but not about their subscription, and a subscription model, which has only the information about the subscription and a foreign key to point to the customer.

Run both of these scaffold commands:

	ruby script/generate scaffold customer first_name:string last_name:string address:string city:string state:string zip:string phone:string
	ruby script/generate scaffold subscription customer_id:integer period:integer amount:integer reference:string message:string params:text test:boolean

The customer model should be self-explanatory. Here’s the description of the fields in the subscription model:

  • customer_id: foreign key for the associated customer
  • period: length of the subscription, in months
  • amount: cost of the subscription, in cents
  • reference: an identifier for the transaction, provided by the gateway
  • message: the message provided by the gateway, describing the success or failure of the transaction
  • params: a holding tank for various parameters that the gateway may return
  • test: a flag indicating that the gateway was in test mode when the transaction was completed

Now migrate the database to the current version. And delete the pesky layouts that we have no use for, views/layouts/customers.html.erb and views/layouts/subscriptions.html.erb.

b. Creating the subscription form

Now you can take a look at the automatically generated forms:

  • New customer form: //localhost:3000/customers/new
  • New subscription form: //localhost:3000/subscriptions/new

The new customer form has most of the information we want for entering a new subscription. The new subscription form is rather worthless, however, since this information mostly comes from the gateway, or from our application logic.

Nevertheless, the form we want is most naturally thought of as a new subscription form, so we’re going to use that one, eviscerate most of the scaffolded content, copy the fields from the customer form, and add a few more details.

Open the file view/subscriptions/new, and delete everything between the form_for statement and the <p> tag that precedes the submit button helper, so the entire file now looks like this:

New subscription

<%= error_messages_for :subscription %> <% form_for(@subscription) do |f| %>

<%= f.submit “Create” %>

<% end %>

While we have this nice simple view, change the header text (between the h1 tags) to “Subscribe to Our Site”, and change the submit button label from “Create” to “Subscribe”.

Now we’re going to insert the customer fields, much like they exist in the customer form, in the new subscription form. You can do this with some judicious copy-and-paste and then some editing, but it’s a lot of steps to write out (and to read), so we’ll just show the final result, which you can copy and paste from here:

<h1>Subscribe to Our Site</h1>

	<%= error_messages_for :subscription %>
	<%= error_messages_for :customer %>

	<% form_for(@subscription) do |f| %>

	<% fields_for(@customer) do |customer| %>
	  <p>
	    <b>First name</b><br />
	    <%= customer.text_field :first_name %>
	  </p>

	  <p>
	    <b>Last name</b><br />
	    <%= customer.text_field :last_name %>
	  </p>

	  <p>
	    <b>Address</b><br />
	    <%= customer.text_field :address %>
	  </p>

	  <p>
	    <b>City</b><br />
	    <%= customer.text_field :city %>
	  </p>

	  <p>
	    <b>State</b><br />
	    <%= customer.text_field :state %>
	  </p>

	  <p>
	    <b>Zip</b><br />
	    <%= customer.text_field :zip %>
	  </p>

	  <p>
	    <b>Phone</b><br />
	    <%= customer.text_field :phone %>
	  </p>

	<% end %>

	  <p>
	    <%= f.submit "Subscribe" %>
	  </p>
	<% end %>

Note that we’ve changed the form_for helper for the customer fields to fields_for. This is necessary because you can’t nest forms, and Rails provides a helper for just this situation. We also changed the block variable in the fields_for helper, f by default, to customer, so we can associate the customer fields with the customer model, and subscription fields with the subscription model.

c. Adding in the subscription period

So far, we have no fields for our subscription model in our new subscription form. The one thing we need the user to choose is the subscription period. We could put in a text field for this, as in the scaffolded form, but we probably don’t want arbitrary subscription lengths. So let’s just provide two options, selected via radio buttons. Insert the following text between the form_for and the fields_for statements:

<p>
      <b>Subscription Period</b><br />
      <%= f.radio_button :period, 1 %><label>One Month: $9.95</label><br />
      <%= f.radio_button :period, 12 %><label>One Year: $99.95</label>
   </p>

Both radio buttons are associated with the attribute :period. Since only one can be selected, that one will determine the value that is provided.

We’ll take care of figuring the price based on the selected period when we get to the controller.

d. Entering the credit card information

The last chunk of information we need from the user is their credit card information. We’ll use a fields_for helper, just as we did for the customer info. We don’t have a model yet for the credit card, and we’ll see when we get to the controller that this model comes from Active Merchant. For our purposes in the form, it acts just like a normal Active Record model.

Insert the following code just before the “Subscribe” submit button (and its surrounding p tag):

<% fields_for(:creditcard, @creditcard) do |cc| %>

      <p><label>Card Number</label><br />
         <%= cc.text_field :number %></p>

      <p><label>Card Type</label><br />
         <%= cc.select :type, "Visa", "visa"], ["MasterCard", "master"], ["Discover", "discover"], ["American Express", "american_express" (*ERROR - NO SUCH CATEGORY*) %></p>

      <p><label>Expiration</label><br />
         <%= cc.select :month, (1 .. 12) %>
         <%= cc.select :year, (Time.now.year .. 10.years.from_now.year) %></p>

      <p><label>Verification code</label><br />
         <%= cc.text_field :verification_value %></p>

   <% end %>

You may notice that this fields_for helper provides both the name of the object, as a symbol, and the instance variable used to hold it, whereas in our other examples we needed only the instance variable. We need this because this is not an Active Record model, even though it mostly behaves like one; Active Record provides some magic that eliminates the need for both versions, which Active Merchant doesn’t provide.

Here’s an explanation of the fields:

  • The :number field is straightforward — just a text field for the credit card number.
  • :card_type is a select field, and we’re providing an array of arrays for the list. The first element of each component array is the text to be displayed, and the second element is the data to be returned if that item is selected.
  • The :month field is a select, for which we use the Ruby “range” syntax to easily create the list of month numbers, from 1 through 12.
  • The :year field is similar, and we again use a range, with the start of the range being the current year, and the end of of the range being 10 years later.
  • The :verification_value field is for the 3- or 4-digit security code, and it is just a text field.

Finally, at the top of the form, we need to report any validation errors on the creditcard model. So add this line just below the other two error_messages_for helpers:

<%= error_messages_for :creditcard %>

e. Adding the subscription button

Let’s put a subscription button in the navigation bar so we can display our lovely new form easily. Add this code at whatever point you’d like in the navbar list in views/layouts/application.html.erb:

<li><%= link_to 'Subscribe!', new_subscription_path, :id => 'subscription_nav' %> </li>

To make the tab highlight work, we need to add the code for this new tab to the CSS. Add these lines to public/stylesheets/quickstart.css:

   #subscribe_tab a#subscribe_nav {
      background-color: #333;
   }

Refresh the page, and you’ll see the Subscribe! button. Click on the button, and you’ll see our nice new form. But it won’t yet do much if you submit it, and the tab highlight won’t work yet, because we have work to do in the controller.

f. Styling the validation errors

Rails includes a default style sheet that provides some styling for the validation error messages, as well as highlights for the fields that have errors. We haven’t included that stylesheet in the HTML until now, even though the stylesheet is in the public/stylesheets folder. So just add this line to the head section of views/layouts/application.html.erb:

<%= stylesheet_link_tag 'scaffold.css' %>

g. Adding links to the admin pages

The scaffolded customer and subscriber admin pages are useful for looking at the data that has been stored, so let’s add links to them to our admin page. Add these lines to the list of links in views/static/admin.hmtl.erb:

<li><%= link_to 'Customers Admin', customers_path %></li>
   <li><%= link_to 'Subscriptions Admin', subscriptions_path %></li>

3. The subscriptions controller

a. Setting up the instance variables

Let’s get to work on the file controllers/subscriptions_controller.rb.

In the new method, you’ll see that it sets only one instance variable, @subscription. Since we are using three instance variables in the form, we need to set those too. So after the line that sets the @subscription instance variable, add these two lines:

   @customer = Customer.new
   @creditcard = ActiveMerchant::Billing::CreditCard.new

The first line is just like all the others we’ve used in the past. But the second is a little different. We could have set up a creditcard model, and generated a scaffold, but that would be bad, bad, bad! We don’t want to store credit card information in our database. And we don’t want to have to write the routines to validate the credit card information.

Fortunately, Active Merchant provides us with a very nice solution. Active Merchant enables us to create a credit-card object, and to validate that object, without any possibility of it being stored in our database. Since the CreditCard class is part of the Billing module in the ActiveMerchant plugin, we need to create the creditcard object using the syntax shown above.

While we’re at it, add the two lines required for the tab highlighting and the page title:

   @current_tab = "subscribe_tab"
   @page_title = "Subscribe to Our Site"

Now we’re done with the new method.

b. Creating the new objects

The default create method in the subscriptions controller creates a subscription object in the usual manner, since we started from a subscription scaffold. But it doesn’t know anything about customers, or credit cards, no does it know how to figure out the subscription price from the subscription period, not to mention actually sending the credit card transaction to the gateway.

We need to teach it these things. This requires the biggest chunk of Ruby code we’ve written in the entire seminar, but there’s nothing very complicated here. Let’s take it step by step.

We’re not going to use the standard respond_to block that the scaffolding provided, so go ahead and delete all that code now. This should leave you with an empty method, except for the creation of the subscription object.

First, let’s create the customer object, which works in the usual way:

	@customer = Customer.new(params[:customer])

Remember the fields_for helper that we used to wrap the customer fields? That helper takes care of putting the customer fields into a hash within the params hash, named :customer, so we can easily pull those out and set up the customer object in the usual way.

Now we need to figure the subscription cost. The translation from subscription period to subscription cost is business logic, so it belongs in the model, not in the controller. So we’ll just assume that the model will provide us with a method to take care of this:

   @subscription.amount = Subscription.cost_for_period(@subscription.period)

We just need to remember to create this cost_for_period method in the subscription model.

Now let’s move on to setting up the credit card object. We do this in much the same way as the subscription and customer objects, except that this is not an Active Record model, but rather one provided by Active Merchant:

   @creditcard = ActiveMerchant::Billing::CreditCard.new(params[:creditcard])    

And then we need to set the first and last name of the cardholder, which we need to get from the customer object:

   @creditcard.first_name = @customer.first_name
   @creditcard.last_name = @customer.last_name

We now have our customer, subscriber, and creditcard objects set up.

c. Checking for a valid order

Now we need to make sure these objects are all valid. Normally, when saving a single object, we use something like this:

   if customer.save
      # proceed, no validation errors
   else
      # redisplay the form with validation error messages
   end

But in this case, we have three objects, and we don’t want to save any of them unless all three are valid. There’s a few ways to handle this, but here’s one simple approach:

   @customer.valid?
   @subscription.valid?    
   unless @creditcard.valid? and @customer.errors.empty? and @subscription.errors.empty?
      render :action => :new and return
   end

We take advantage of the valid? method that is available on all Active Record objects. This performs the validations but doesn’t save the object to the database. If there are any validation errors, they’re stored in the errors array for that object (which is where the error_messages_for helper in the form gets the messages if there are validation errors).

So we validate both the customer and subscription objects, and then we bail out and redisplay the form unless the credit card is valid and the errors attribute on both the customer object and the subscription object are empty.

If our code makes it past this unless clause, we know that we have a valid subscription to process.

d. Setting up the gateway

Now we need to set up all the information needed by the gateway and perform the actual transaction.

First, let’s create the gateway object:

	gateway = ActiveMerchant::Billing::BraintreeGateway.new(BRAINTREE_CREDENTIALS)

We’re just calling a method in ActiveMerchant that sets up the gateway. If we were using a different gateway, we’d of course use the method for the appropriate gateway, but virtually all of the other code would remain the same regardless of the gateway being used (with the one caveat that some gateways require different parameters).

Note that BRAINTREE_CREDENTIALS is set up as a constant. We set this constant in our development environment (config/environments/development.rb):

	BRAINTREE_CREDENTIALS = {:login => 'demo', :password => 'password'}
	ActiveMerchant::Billing::Base.mode = :test

If you were to use this in production, you’d need to get a real gateway account from Braintree (or another gateway provides), and you’d put the production credentials in config/environments/production.rb.

We also set the gateway mode to :test for good measure, though it doesn’t actually matter because the gateway we’re accessing is only a test gateway.

e. Setting the optional information

Different gateways require different amounts of information, and even within one gateway often some information is optional but may reduce your transaction fee if it is provided and is correct. All this optional information is assembled into on hash. So in preparation for running the purchase transaction, we’ll set up this options hash:

   options = {
      :ip => request.remote_ip,
      :billing_address => { 
         :name     => @customer.full_name,
         :address1 => @customer.address,
         :address2 => '',
         :city     => @customer.city,
         :state    => @customer.state,
         :country  => 'US',
         :zip      => @customer.zip,
         :phone    => @customer.phone
      }
   }

The names of the hash keys are taken from the documentation for the Braintree gateway. First we set the customer’s IP address, which we can get from the request object that is always available in any controller. Then we set the billing_address hash, taking the information from the customer object.

Notice, however, that there is no full_name attribute on the customer object — we have first_name and last_name. We could just concatenate those here, but there’s likely to be other places where we need the full name, so it’s better to invent a method to provide us what we need, and then we’ll add this method to the customer model.

f. Performing the transaction

With all this setup in place, actually performing the transaction is easy:

   response = gateway.purchase(@subscription.amount, @creditcard, options)

We simply call the purchase method on the gateway object that we previously created, passing in as parameters the amount of the subscription, the credit card object, and the options hash. We store the response in a new variable we’ve called simply response.

It is in this single line of code that all the web services interaction occurs. Remember that gateway is an Active Merchant object, and when we invoke the purchase method on that object, the Active Merchant code connects to the Braintree payment gateway web service, sends all the information about the transaction, waits for the response, and then returns the response as the result of executing the method.

g. Processing the response

Now that we’ve attempted the transaction, we need to see if it was successful. If not, we want to redisplay the subscription form with appropriate error messages. If so, then we want to save the information from the gateway for later reference and save the subscription and customer information.

Here’s the code:

   unless response.success?
      flash[:notice] = "Transaction failed: #{response.message}"
      render :action => :new
   else
      # save the responses from the transaction in the subscription record
      @subscription.message = response.message
      @subscription.reference = response.authorization
      @subscription.test = response.test?
      @subscription.params = response.params.to_yaml
      @subscription.customer = @customer
      @subscription.save!
      flash[:notice] = "Thanks for subscribing to our site!"
      redirect_to :controller => 'static', :action => 'home'
   end

First we use the success? method on the response object, which is provided by Active Merchant, to see if all went well. If not, we set an appropriate message and redisplay the subscription form.

If all did go well, we save in the subscription object various pieces of information provided by the gateway response. One thing the response object returns is a params object, which is a hash of various parameters that may change from gateway to gateway. So rather than creating a model field for each of these parameters, we convert the params object to text form, using .to_yaml (this is called serializing the data). This allows us to store any arbitrary set of parameters without requiring any code changes.

Finally, we need to associate the customer with the subscription, and then save the subscription. Saving the subscription will also save the associated customer object.

Note that we use the save! method here, because we’ve already validated all the objects, and the new information we’ve added from the transaction doesn’t need to be validated. So if this save fails, that means that there was a programming error, or something really unexpected (like a database failure) happened. If this save fails, an exception will be raised. Somewhere in our code, we should capture that exception and display a “Sorry, something bad happened” message, but there’s not much else we can do.

Finally, we set the flash message and redirect to the home page.

h. Handling the validation error cases

There’s one problem with our code. Suppose that a validation or transaction error occurs, and we redisplay the form. If this happens, we need to have all the instance variable set up that the new subscription form expects. We’ve already created the instance variables for the subscription, customer, and credit card, but we still need to set the page title and the current tab. Since there’s two paths where this would be needed (validation failure, or transaction failure), it’s easiest to just set these values at the top of the method, even though they won’t be used if the transaction succeeds. So add these two lines at the top of the create method:

    @current_tab = "subscribe_tab"
    @page_title = "Please correct the errors below"

Note that we’ve set the page title to an error message, as one more hint to the customer that there’s something they need to fix.

i. Avoiding credit card leaks

We need to be really careful to avoid credit card numbers leaking out. We don’t save them, but we do process them in memory. And we have, inadvertently, stored credit card numbers: they’re in our application log file for each transaction. We can stop this by adding the following line at the top of the subscriptions controller:

	filter_parameter_logging :creditcard

This tells the logger not to show the value of this parameter. The logs will instead show “[FILTERED]” in place of the actual value.

4. Completing the models

a. The subscription model

We’re done with the controller, but we need to complete the models before you can test the code.

First we need to set the associations. A subscription belongs to a customer; that is, we included the customer ID in the subscription table. So add this line to the file models/subscription.rb:

	belongs_to :customer

Now we have to implement the method that we made up when we were writing the controller, that determines the subscription price based on the period. Add the following code:

   def self.cost_for_period period
      if period == 1
         995     # $9.95 in cents
      elsif period == 12
         9995    # $99.95 in cents
      else
         "invalid"
      end
   end

This method is a class method: it acts not upon an instance of the class, but on the class in general. (In other words, the result is the same for all subscriptions.) We signal this fact by prefixing the class name with self..

The logic is quite simple. If the period is 1, the price is 995 (Active Merchant expects prices in cents, so this is $9.95). If the period is 12, the price is 9995 ($99.95).

What should we do if the period is something else? It shouldn’t be possible, since the value is set by a choice from one of two radio buttons. But suppose some trickster spoofed the POST data from the form? They could put in any value they wanted. So we provide a final else clause that returns the string “invalid”.

Then we need to add a line of validation code to the model:

   validates_numericality_of :amount, :message => "Choose a subscription period"

This catches the situation in which neither radio button is selected, so the period is nil, and it also takes care of the situation in which the period is invalid, since the string “invalid” fails the “numericality” test.

b. The customer model

Now we just need to polish off the customer model. First, let’s make all the fields except the phone number required:

	validates_presence_of :first_name, :last_name, :address, :city, :state, :zip

And we need to define the association with subscriptions:

	has_many :subscriptions

(We could use a has_one relationship here if we wanted to restrict a customer to having a single subscription.)

And finally, we need to provide the full_name method that we invented for use in the controller:

   def full_name
      self.first_name + ' ' + self.last_name
   end

Note that this method acts on an instance of the class, not on the class itself, so we do not use self. in the method name, but we do use self. when accessing the instance’s attributes.

5. Ready to test!

This has been our most complex example yet. Let’s make sure it all works.

You’ll need to restart the server, since we’ve added a constant in the configuration file, which is only read on startup.

Click the Subscribe! button, choose a subscription period, and enter a name and address. For the credit card information, the Braintree gateway documentation provides us with the following test card information:

  • Card type: VISA
  • Card number: 4111 1111 1111 1111
  • Expiration: 10/2010

You can use any first and last name and address.

Enter this information, click subscribe, and if all has gone well you’ll see the home page, with “Thanks for subscribing to our site!” at the top. If you get a Rails error, check that you’ve put in all the proper code in the subscriptions controller, the subscription and customer models, and the development environment file.

Now click on the Admin nav button, and then on the Subscriptions Admin link. This will display the scaffolded subscription admin page, so we can see the information that has been written to the subscription database by our code. You can see the series of parameters that have been stuffed into the params field. You can also look at the Customers Admin page to see the customer information.

To see how the parameters change with different levels of verification, enter a subscription with 77777 in the zip code field. The parameters should then show avsresponse=Z (zip code match) instead of N (no match). To simulate a verification code match, enter 999 in that field, and the parameters will show cvvresponse=M (match) instead of N (none). If you get a real gateway account, you’ll find that you pay different fees depending on whether these items match or not, and you can configure your account to decline transactions that don’t match.

You can show what happens with a bad credit card number by entering any other number. The error message displayed doesn’t come from the gateway; our validation of the creditcard object catches this before we get that far, and Active Merchant provides the error messages.

6. Next Steps

This was by far our most complicated lab, but even so, it falls short of a fully robust solution. A complete ecommerce solution has a lot of components and could easily be a two-day class of its own.

Some things you’d want to consider adding for a production environment:

  • Validate addresses more thoroughly than simply requiring that they not be blank
  • Set a default value for the subscription period
  • Process the exception that could occur if for some reason the final save! failed
  • Create a user account for the customer, so they can log in and see their subscription status
  • Improve the layout of the subscription form
  • Process the error messages, rather than using the standard error_messages_for helper, to display them more cleanly
  • Improve the customer admin page by showing the subscription date and period
  • Refactor the subscription/create controller code to push more of the logic into the model
  • And, or course, we haven’t provided any logic to deal with expiration of subscriptions or renewals

Viewing all articles
Browse latest Browse all 27

Trending Articles