• Home

  • Custom Ecommerce
  • Application Development
  • Database Consulting
  • Cloud Hosting
  • Systems Integration
  • Legacy Business Systems
  • Security & Compliance
  • GIS

  • Expertise

  • About Us
  • Our Team
  • Clients
  • Careers

  • Blog

  • EpiTrax

  • VisionPort

  • Contact
  • Our Blog

    Ongoing observations by End Point Dev people

    Ecommerce on Sinatra: In a Jiffy

    Steph Skardal

    By Steph Skardal
    January 17, 2011

    Several of us at End Point have been involved in a non-ecommerce project for one of our clients running on Ruby, Sinatra, Unicorn, using DataMapper, PostgreSQL, PostGIS, with heavy use of JavaScript (specifically YUI). Sinatra is a lightweight Ruby web framework – it’s not in direct competition with Rails but it might be a better “tool” for lightweight applications. It’s been a fun project to work with Sinatra, DataMapper, and YUI as I’ve been working traditionally focused on their respective related technologies (Rails, ActiveRecord, jQuery).

    Out of curiosity, I wanted to see what it might take to implement a bare-bones ecommerce store using Sinatra. Here is a mini-tutorial to develop an ecommerce store using Sinatra.

    A snapshot of our final working app.

    Getting Started

    I create a new directory for the project with the following directories:

    sinatrashop/
      db/
        migrate/
      models/
      public/
        images/
         stylesheets/
      views/
    

    Data Model

    Now, let’s look at the data model. Since this is a bare-bones store, I have one order model which contains all the order information including contact information and addresses. We’re not storing the credit card in the database. Also, since this is a bare-bones app, we’re going to go with one product with a set price and force the limitation that users only buy one at a time. I’ve also chosen to use ActiveRecord here since I’m still not sold on DataMapper, but another ORM can be used as well. Here is our model:

    # sinatrashop/models/order.rb
    class Order < ActiveRecord::Base
      validates_presence_of :email
      validates_presence_of :bill_firstname
      validates_presence_of :bill_lastname
      validates_presence_of :bill_address1
      validates_presence_of :bill_city
      validates_presence_of :bill_state
      validates_presence_of :bill_zipcode
      validates_presence_of :ship_firstname
      validates_presence_of :ship_lastname
      validates_presence_of :ship_address1
      validates_presence_of :ship_city
      validates_presence_of :ship_state
      validates_presence_of :ship_zipcode
      validates_presence_of :phone
      validates_format_of :email,
        :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
        :on => :create
    end
    

    And here is our migration:

    # sinatrashop/db/migrate/001_create_orders.rb
    class CreateOrders < ActiveRecord::Migration
      def self.up
        create_table :orders do |t|
          t.string   :email, :null => false
          t.string   :bill_firstname, :null => false
          t.string   :bill_lastname, :null => false
          t.string   :bill_address1, :null => false
          t.string   :bill_address2
          t.string   :bill_city, :null => false
          t.integer  :bill_state, :null => false
          t.string   :bill_zipcode, :null => false
          t.string   :ship_firstname, :null => false
          t.string   :ship_lastname, :null => false
          t.string   :ship_address1, :null => false
          t.string   :ship_address2
          t.string   :ship_city, :null => false
          t.integer  :ship_state, :null => false
          t.string   :ship_zipcode, :null => false
          t.string   :phone, :null => false
          t.timestamps
        end
      end
    
      def self.down
        drop_table :orders
      end
    end
    

    I did some research here and created the Rakefile shown below to run the migrations. The Rakefile establishes a connection to a sqlite3 database and runs migrations in the db/migrate directory.

    # sinatrashop/Rakefile
    namespace :db do
      task :environment do
        require 'rubygems'
        require 'logger'
        require 'active_record'
        ActiveRecord::Base.establish_connection :adapter => 'sqlite3',
          :database => 'db/development.sqlite3.db'
      end
    
      desc "Migrate the database"
      task(:migrate => :environment) do
        ActiveRecord::Base.logger = Logger.new(STDOUT)
        ActiveRecord::Migration.verbose = true
        ActiveRecord::Migrator.migrate("db/migrate")
      end
    end
    

    Views

    Now, let’s think about the views we’ll present to users. There are many template rendering options in Sinatra, but we’ll go with erb and create an index.erb file. By default, Sinatra looks for views in the ROOT/views directory. This will be our only view and layout and below is a breakdown of what it will include:

    # header information
    <body>
    # product information
    # form for submission
    # errors or success message
    </body>
    

    Obviously, there will be a lot more code here, but the view needs to show the basic product information, the form fields to collection information, and errors or a success message to handle the different use cases. See the code here to examine the contents.

    Application Code

    Next, let’s take a look at the application code. This will be in sinatrashop/store.rb:

    require 'sinatra'
    require 'erb'
    require 'active_record'
    require 'configuration'
    require 'models/order'
    
    get '/' do
      erb :index
    end
    
    post '/' do
      erb :index
    end
    

    The application code handles two requests, a get and post to ‘/’. The get is a standard home page request. The post to ‘/’ is the order submission. The post ‘/’ action needs to save the order, establish a connection to the payment gateway, and authorize and capture the payment. If any of these actions fail, the order must not be saved to the database and errors must be presented to the user. Consider the following code, which uses ActiveRecord::Base.transaction method and will rollback the saved order if any part of the authorization fails. We also use ActiveMerchant here, which is an extraction from Shopify for payment gateway integration that can be used as a gem.

    # sinatrashop/store.rb
    post '/' do
      begin
        order = Order.new(params[:order])
        ActiveRecord::Base.transaction do
          if order.save
            params[:credit_card][:first_name] = params[:order][:bill_firstname]
            params[:credit_card][:last_name] = params[:order][:bill_lastname]
            credit_card = ActiveMerchant::Billing::CreditCard.new(params[:credit_card])
            if credit_card.valid?
               gateway = ActiveMerchant::Billing::AuthorizeNetGateway.new(settings.authorize_credentials)
    
               # Authorize for $10 dollars (1000 cents) 
               response = gateway.authorize(1000, credit_card)
               if response.success?
                 order.update_attribute(:status, "complete")
                 gateway.capture(1000, response.authorization)
                 @success = true
               else
                 raise Exception, response.message
               end
             else
               raise Exception, "Your credit card was not valid."
             end
           else
             raise Exception, '<b>Errors:</b> ' + order.errors.full_messages.join(', ')
           end
         end
      rescue Exception => e
        @message = e.message 
      end
    end
    

    Configuration

    You might notice above that there is a “settings” hash used in the payment gateway connection request. I create a configuration file which sets up some configuration variables in Sinatra’s configure do block:

    # sinatrashop/configuration.rb
    require 'active_merchant'
    
    configure do
      set :authorize_credentials => {
        :login => "LOGIN"
        :password => "PASSWORD"
      }
      ActiveRecord::Base.establish_connection(
        :adapter => 'sqlite3',
        :database =>  'db/development.sqlite3.db'
      )
      ActiveMerchant::Billing::Base.mode = :test
    end
    

    Testing

    I wrote several tests to handle a few use cases. These can be examined here. The tests use Rack::Test and can be run with the command ruby -rubygems test_store.rb.

    Infrastructure Concerns

    Additional changes are required for running the application on the HTTP server of your choice. Additionally, the entire site should probably run in SSL, which would need configuration. Finally, sqlite may be replaced with a different database here with updates to the configuration.rb and Rakefile files. During development, I ran my app with the command ruby -rubygems store.rb.

    Conclusion

    Our bare-bones ecommerce app contains a single simple order model that contains all of our order information. There are only two actions defined our application code. There is one index view. Public assets are served from ROOT/public/. The Rakefile contains functionality for running migrations. There is no admin interface here. A site administrator needs to retrieve orders from the database for sending email notifications and fulfillment. In incremental development, this is the simplest setup to allow someone to collect money, but it requires quite a bit of manual management (emails, fulfillment, etc.). The code can be found here. Here are more screenshots of the working application:

    Order errors!

    Payment gateway errors.

    A successful transaction.

    ecommerce ruby sinatra


    Comments