I'm using the rails state_machine plugin (looks better than aasm) - it looks great - however, I've implemented it for a credit card processing system I wrote and it looks a bit strange... the code looks very un-DRY...
I wonder if anyone can cast a quick eye over it and let me know what they think. In particular, to me the bits that doesn't look very DRY are:
- the use of :do_screen and :do_submit_to_processor - am I missing something?
- the need to create an event for every state
I'm sure it's because I'm missing something really obvious - if so I would appreciate any help you can give me, or code examples if you have any.
CreditCardTransaction Model
class CreditCardTransaction < BillingTransaction
include StateLogger
attr_accessor :message
cattr_accessor :gateway
belongs_to :parent, :polymorphic => true
belongs_to :credit_card, :foreign_key => "parent_id"
belongs_to :person
validates_presence_of :parent_id
before_validation :get_person, :generate_external_reference
before_save :set_description
state_machine :initial => :pending do
before_transition any => any, :do => :log_state_change
after_transition any => :screening , :do => :do_screen
after_transition any => :submiting_to_processor, :do => :do_submit_to_processor
event :screen do
transition any => :screening
end
event :pass_screening do
transition :screening => :passed_screening
end
event :fail_screening do
transition :screening => :failed_screening
end
event :submit_to_processor do
transition any => :submiting_to_processor
end
event :accepted_by_processor do
transition :submiting_to_processor => :success
end
event :declined_by_processor do
transition :submiting_to_processor => :declined_by_processor
end
end
def process!
self.screen if can_screen?
self.submit_to_processor if can_submit_to_processor?
self
end
# Returns true if successful (valid active card, no duplicate, no fraud etc)
def do_screen
success = true
self.message ||= ""
# Check card is active
unless self.credit_card and self.credit_card.can_process?
success = false
self.message += "Invalid credit card or not allow to purchase with this card (check card state)\n"
end
# Find duplicate transaction
duplicate_transactions = self.find_duplicates
unless duplicate_transactions.blank?
success = false
duplicate_transactions.each do |t|
self.message += "Duplicate transactions found: #{t.id}\n"
end
end
# Add preliminary anti-fraud stuff here...
# Set flags and return results
if success
pass_screening
else
fail_screening
log_progress(self.message)
end
success
end
def do_submit_to_processor(transition, options = {})
begin
response = gateway.purchase(amount_in_cents, self.credit_card, options.with(
:order_id => self.external_reference,
:billing_address => self.credit_card.billing_address
))
success = response.success?
reference = response.authorization
self.message = response.message
receipt = response.params
test = response.test
rescue ActiveMerchant::ActiveMerchantError => e
success = false
reference = nil
self.message = e.message
receipt = {}
test = gateway.test?
end
if success
self.accepted_by_processor
else
self.declined_by_processor
end
# Log progress
["success", "reference", "self.message", "receipt", "test"].each do |param_name|
log_progress(param_name + ": " + eval(param_name).inspect)
end
end
def amount_in_cents
(self.amount * 100).to_i
end
def amount_in_dollars
self.amount
end
def find_duplicates
# The fields below together defines a unique entry, given a certain amount of time
time_limit = 5.minutes
method_name = "find_all_by_"
params = ""
# screen by last_digits because cannot do search on encrypted field
[:person_id, :amount, :parent_type].each do |field|
method_name += "_and_" unless method_name == "find_all_by_"
method_name += field.to_s
params += ", " unless params.blank?
params += self[field] ? "'#{self[field]}'" : "nil"
end
sql = "#{self.class}.#{method_name}( #{params}, :conditions => ['created_at > ? and state = ?', time_limit.ago.utc, 'success' ] )"
# puts sql
results = eval sql
# now compare encrypted field
duplicates = []
results.each do |result|
duplicates << result if result.id != self.id && result.credit_card &&
result.credit_card.number == self.credit_card.number &&
result.credit_card.person == self.credit_card.person
end
duplicates
end
private
def get_person
self.person = self.parent.person if self.parent
end
# generate random order_id for gateway
def generate_external_reference
self.external_reference = ActiveSupport::SecureRandom.hex
end
def set_description
self.description = "Payment by #{self.credit_card.type.to_s.capitalize} card ending #{self.credit_card.last_digits}"
end
end
state_logger.rb
module ClassMethods
# Returns all states (had to dig into source for this, prob is a better way...)
def states
state_machine.states.map(&:value)
end
end
# Puts ClassMethods into this module
# from http://blog.jayfields.com/2006/12/ruby-instance-and-class-methods-from.html
def self.included(base)
base.extend(ClassMethods)
end
protected
def log_progress(content)
self.update_attribute( :log, [self[:log], content].compact.join("\n\n") )
end
def log_state_change(transition)
event, from, to = transition.event, transition.from_name, transition.to_name
self.log = [ self[:log], "#{event}: State changed from #{from} to #{to} at #{Time.now}" ].compact.join("\n\n")
end