TrustCommerce payment history & invoicing

August 28th, 2007 Plugins

Lately I've been hearing more and more from developers and companies who are using the my TrustCommerce subscription gem or plugin.

One of the questions I recently received was "How do you handle syncing transaction history, send monthly invoices, etc.?"

The way I've done this in apps is to create two tables to handle subscription and subscription transaction data.

Example tables

create_table :subscriptions do |t|
  t.column :account_id,                :integer,   :null => false
  t.column :created_on,                :datetime,  :null => false
  t.column :billing_id,                :string
  t.column :length,                    :integer,   :null => false
  t.column :cents,                     :integer,   :null => false
  t.column :billing_full_name,         :string
  t.column :billing_address,           :string
  t.column :billing_zip_code,          :string
  t.column :billing_country,           :string
  t.column :billing_card_type,         :string
  t.column :billing_credit_card,       :string
  t.column :billing_expiration_date,   :datetime   
end

add_index :subscriptions, :account_id

create_table :subscription_transactions do |t|
  t.column :subscription_id,          :integer,   :null => false
  t.column :account_id,               :integer,   :null => false
  t.column :transaction_date,         :datetime,  :null => false
  t.column :transaction_id,           :string,    :null => false
  t.column :transaction_type,         :string,    :null => false
  t.column :amount,                   :decimal,   :null => false
  t.column :card_number,              :string,    :null => false
  t.column :card_type,                :string,    :null => false
  t.column :cardholder_name,          :string,    :null => false     
end

add_index :subscription_transactions, :subscription_id
add_index :subscription_transactions, :account_id

Then we can create an agent to grab recent transactions from the vault:

class TrustcommerceSyncAgent

  # time_ago examples: 
  #   30m => 30 minutes
  #   1h  => 1 hour
  #   3d  => 3 days
  def self.sync(time_ago = nil)
    time = case time_ago
      when /^(\d+)m$/ then Time.now.utc - $1.to_i.minute
      when /^(\d+)h$/ then Time.now.utc - $1.to_i.hour
      when /^(\d+)d$/ then Time.now.utc - $1.to_i.day
      else Time.now.utc - 1.hour
    end
    # --- [ find recently created paying subscriptions ] ---
    subscriptions = Subscription.find(:all, :conditions => ['cents > 0 AND created_on > ?', time])
    subscriptions.each do |subscription|
      sync_subscription(subscription)
    end
  end

  private  

    def self.sync_subscription(subscription)

      # --- [ get TC data ] ---
      tc = TrustCommerce::Subscription.query(
        :querytype  => 'transaction',
        :billingid  => subscription.billing_id
      )      
      return if !tc.kind_of? Net::HTTPOK

      # --- [ create index by CSV header ]
      field_names = tc.body.split("\n")[0].split(',')
      indexes = field_names.inject({}) {|h, field| h[field.to_sym] = field_names.index(field); h }

      # --- [ build transaction array ] ---
      transactions = tc.body.split("\n")
      transactions.shift # get rid of header

      transactions.each do |line|
        transaction = line.split(',')

        #log_transaction(indexes, transaction)

        subscription_transaction = subscription.transactions.find_by_transaction_id(transaction[indexes[:transid]])
        if subscription_transaction.nil?
          SubscriptionTransaction.create(
            :subscription_id    => subscription.id,
            :account_id         => subscription.account_id,
            :transaction_date   => convert_date(transaction[indexes[:trans_date]]),
            :transaction_id     => transaction[indexes[:transid]],
            :transaction_type   => transaction[indexes[:action_name]],
            :amount             => transaction[indexes[:bank_amount]],
            :card_number        => transaction[indexes[:cc]],
            :card_type          => convert_card_type(transaction[indexes[:media_name]]),
            :cardholder_name    => transaction[indexes[:name]]
          )
        end      
      end

    end

    # TC returns mm-dd-yyyy HH:mm:ss
    def self.convert_date(str)
      date = str.split(' ')[0].split('-')
      Time.local(date[2], date[0], date[1])
    end

    # TC returns VISA-D, MC-D, AMEX-D
    def self.convert_card_type(str)
      case str
        when /VISA/i  then 'Visa'
        when /MC/i    then 'MasterCard'
        when /AMEX/i  then 'American Express'
        else str
      end
    end

    # --- [ helpful for debugging ] ---
    def self.log_transaction(indexes, transaction)
      puts '---------- transaction --------------------'
      indexes.each{|k,v| puts "#{k} => #{transaction[indexes[k]]}" if !transaction[indexes[k]].blank? }
      puts '-------------------------------------------'
    end

end

You can play around with this on the console:

$ ./script/runner -e production "TrustcommerceSyncAgent.sync('3h')"

and then install into cron in production.

In terms of monthly invoices, a simple after_create hook should work nice:

class SubscriptionTransaction < ActiveRecord::Base
  after_create :send_invoice
  ...
end

By the way, I've added TrustcommerceSyncAgent to the gem and plugin.

Hope this helps getting your monthly billing groove on...

--- --- ---

9 Comments

  1. Comment by Marston A, SugarStats on 08/28/07
    Very nice solution, wish this came out when we were asking the same question :-) Though we looked at it and for our personal needs found out we didn't NEED emailable invoices and settled on a solution that allowed users to view their history and invoices from within the app and print them. It was kind of like this solution but without the email/polling. Great writeup.
  2. Comment by ian on 08/28/07
    Why not just get the data real time from TC? That way you don't have to worry about any syncing.
  3. Comment by Zack on 08/28/07
    Ian: When you tell TC to bill a customer every certain number of days (ex: 30 days), you don't know exactly when they'll be making the charge. All you know is they will process the charge sometime during that day. Hope that makes sense.
  4. Comment by raecoo on 08/31/07
    so good
  5. Comment by Matt on 09/04/07
    I'm confused, I thought the point was to let TrustCommerce store the cc info. Why are you storing it in your db?
  6. Comment by Matt on 09/04/07
    Also, how does TC notify you if one of your recurring payments fails? Or if a customer's cc is about to expire?
  7. Comment by Zack on 09/04/07
    Matt: I'm not storing the cc info in the db - just the transaction details so I can display a payment history page and allow customers to print invoices, etc.
  8. Comment by Matt on 09/04/07
    I see. So for billing_credit_card you only store that last 4 digits I assume? I also assume you use the billing_expiration_date to send out your own "card about to expire" emails? This all looks really cool, I'm still curious how TC notifies you if one of your recurring payments fails though. Thanks a lot for all of this.
  9. Comment by Zack on 09/05/07
    Matt: You can query the Vault with a billingid querytype with a key/value pair of state:pastdue. This should give you the billingids that are overdue on payment. Or you can log into the Vault and run the report manually. An email is also sent when this occurs.

Commenting is closed for this article.