• Home

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

  • Expertise

  • About Us
  • Our Team
  • Clients
  • Blog
  • Careers

  • VisionPort

  • Contact
  • Our Blog

    Ongoing observations by End Point Dev people

    ActiveRecord Callbacks for Order Processing in Ecommerce Applications

    Steph Skardal

    By Steph Skardal
    January 13, 2012

    As I recently blogged about, I introduced a new Ruby on Rails Ecommerce Engine. The gem relies on RailsAdmin, a Ruby on Rails engine that provides a nice interface for managing data. Because the RailsAdmin gem drives order creation on the backend in the context of a standard but configurable CRUD interface, and because I didn’t want to hack at the RailsAdmin controllers, much of the order processing logic leverages ActiveRecord callbacks for processing. In this blog article, I’ll cover the process that happens when an order is saved.

    Order Data Model

    The first thing to note is the data model and the use of nested attributes. Here’s how the order model relates to its associated models:

    class Order < ActiveRecord::Base
      has_many :line_items, :inverse_of => :order
      has_many :payments, :inverse_of => :order
      has_many :shipments, :inverse_of => :order
      has_many :credits, :inverse_of => :order
    
      belongs_to :billing_address, :class_name => "Piggybak::Address"
      belongs_to :shipping_address, :class_name => "Piggybak::Address"
      belongs_to :user
      
      accepts_nested_attributes_for :billing_address, :allow_destroy => true
      accepts_nested_attributes_for :shipping_address, :allow_destroy => true
      accepts_nested_attributes_for :shipments, :allow_destroy => true
      accepts_nested_attributes_for :line_items, :allow_destroy => true
      accepts_nested_attributes_for :payments
    end
    

    An order has many line items, payments, shipments and credits. It belongs to [one] billing and [one] shipping address. It can accept nested attributes for the billing address, shipping address, multiple shipments, line items, and payments. It cannot destroy payments (they can only be marked as refunded). In terms of using ActiveRecord callbacks for an order save, this means that all the nested attributes will also be validated during the save. Validation fails if any nested model data is not valid.

    Step #1: user enters data, and clicks submit

    Step #2: before_validation

    Using a before_validation ActiveRecord callback, a few things happen on the order:

    • Some order defaults are set
    • The order total is reset
    • The order total due is reset

    Step #3: validation

    This happens without a callback. This method will execute validation on both order attributes (email, phone) and nested element attributes (address fields, shipment information, payment information, line_item information).

    Payments have a special validation step here. A custom validation method on the payment attributes is performed to confirm validity of the credit card:

    validates_each :payment_method_id do |record, attr, value|
      if record.new_record?
        credit_card = ActiveMerchant::Billing::CreditCard.new(record.credit_card)
        
        if !credit_card.valid?
          credit_card.errors.each do |key, value|
            if value.any? && !["first_name", "last_name", "type"].include?(key)
              record.errors.add key, value
            end
          end
        end
      end
    end
    

    This bit of code uses ActiveMerchant’s functionality to avoid reproducing business logic for credit card validation. The errors are added on the payment attributes (e.g. card_number, verification_code, expiration date) and presented to the user.

    Step #4: after_validation

    Next, the after_validation callback is used to update totals. It does a few things here:

    • Calculates shipping costs for new shipments only.
    • Calculates tax charge on the order.
    • Subtracts credits on the order, if they exist.
    • Calculates total_due, to be used by payment

    While these calculations could be performed before_validation, after_validation is a bit more performance-friendly since tax and shipping calculations could in theory be expensive (e.g. shipping calculations could require calling an external API for real-time shipping lookup). These calculations are saved until after the order is confirmed to be valid.

    Step #5: before_save part 1

    Next, a before_save callback handles payment (credit card) processing. This must happen after validation has passed, and it can not happen after the order has saved because the user must be notified if it fails. If any before_save method returns false, the entire transaction fails. So in this case, after all validation has passed, and before the order saves, the payment must process successfully.

    Examples of failures here include:

    • Credit card transaction denied for a number of reasons
    • Payment gateway down
    • Payment gateway API information incorrect

    Step #6: before_save part 2

    After the payment processes, another before_save method is called to update the status of the order based on the totals paid. I initially tried placing this in an after_save method, but you tend to experience infinite loops if you try to save inside and after_save callback :)

    Step #7: Save

    Finally, if everything’s gone through, the order is saved.

    Summary

    As I mentioned above, the RailsAdmin controllers were not extended or overridden to handle backroom order processing. All of the order processing is represented in the Order model in these active record callbacks. This also allows for the frontend order processing controller to be fairly lightweight, which is a standard practice for writing clean MVC code.

    Check out the full list of ActiveRecord callbacks here. And check out the Order model for Piggybak here.

    ecommerce open-source piggybak rails


    Comments