Tuesday, December 05, 2006

BNL: Extracting Sales Person

Versioning on an individual sales person
Before we add version control to our application we need something to version. Logically it seems you would like to have a document history showing compensation agreements for each sales person. To accommodate this requirement, we can pull the sales person from the scripts and save them off separately. The first step is creating migrations that create and alter the tables to match our new intentions.

File: 003_create_sales_people.rb
class CreateSalesPeople < ActiveRecord::Migration
def self.up
create_table :sales_people do |t|
t.column :name, :string
end
end

def self.down
drop_table :sales_people
end
end

File: 004_add_sales_person_to_compensation_script.rb
class AddSalesPersonToCompensationScript < ActiveRecord::Migration
def self.up
add_column :compensation_scripts, :sales_person_id, :integer
add_column :compensation_scripts, :created_at, :datetime
end

def self.down
remove_column :compensation_scripts, :sales_person_id
remove_column :compensation_scripts, :created_at
end
end


Following the above migrations the existing codebase will need to be updated to reflect the new database structure. The "new" page has been updated to include a textbox for entering the sales person's name.

File: new.rhtml
<% form_for :script, @script, :url => { :action => "create" } do |form| %>
Name<br>
<%= form.text_field :name %><br>
<br>
Compensation Rules<br>
<%= form.text_area :logic %><br>
<%= submit_tag %>
<% end %>


Introducing the name textbox requires us to create the new SalesPersonCompensationScript class. This is because a CompensationScript still contains the logic, but the SalesPerson class maintains the name captured in the "name" textbox. The SalesPersonCompensationScript is a simple class that is used only to store the data that will be used by the controller to save a SalesPerson and CompensationScript.

File: sales_person_compensation_script.rb
class SalesPersonCompensationScript
attr_accessor :name, :logic

def initialize(hash=nil)
unless hash.nil?
self.name = hash[:name]
self.logic = hash[:logic]
end
end
end


The PayrollController has been updated to use the SalesPersonCompensationScript class on the "new" view and on the create action. The create action is where the SalesPersonCompensationScript is used to create a CompensationScript and a new SalesPerson if necessary.

File: payroll_controller.rb
class PayrollController < ApplicationController

def index
@sales_people = SalesPerson.find :all
end

def new
@script = SalesPersonCompensationScript.new
end

def create
person_script = SalesPersonCompensationScript.new(params[:script])
script = CompensationScript.create(:logic => person_script.logic)
sales_person = SalesPerson.find_by_name(person_script.name) ||
SalesPerson.create(:name => person_script.name)
sales_person.compensation_scripts << script
redirect_to :action => :index
end

def view
@person = SalesPerson.find(params[:person_id])
end

def execute
@person = SalesPerson.find(params[:person_id])
vocabulary = InlineVocabulary.new(@person.short_name)
logic = @person.active_compensation_script.logic
@compensation = CompensationParser.parse(logic, vocabulary, InlineContext.new)
end

def execute_all
@compensations = {}
SalesPerson.find(:all).each do |person|
vocabulary = SqlVocabulary.new(person.short_name)
logic = person.active_compensation_script.logic
compensation = CompensationParser.parse(logic, vocabulary, SqlContext.new)
@compensations[person.name.to_sym] = compensation.amount
end
end

end


Every action within the PayrollController has been changed to reflect the new domain model that includes a SalesPerson as a concept. The index action now relies on an array of SalesPerson instances instead of an array of CompensationScript instances. The index.rhtml has also been updated to reflect this change.

File: index.rhtml
Employees:
<ul>
<% @sales_people.each do |person| %>
<li>
<%= person.name %> -
<%= link_to 'view', :action => :view, :person_id => person.id %> |
<%= link_to 'execute', :action => :execute, :person_id => person.id %>
</li>
<% end %>
</ul>
<%= link_to 'Execute All', :action => :execute_all %> |
<%= link_to 'Create new compensation script', :action => :new %>


Navigating to the view page now requires a sales person instance to be available.

File: view.rhtml
Compensation rules for <%= @person.name %>
<pre><%= @person.active_compensation_script.logic %></pre>
<%= link_to 'back', :action => :index %>


The execute.rhtml was updated also, but only changed slightly to accommodate the introduction of the sales person concept.

File: execute.rhtml
<%= @person.name %> compensation: <%= number_to_currency @compensation.amount %><br>
<%= link_to 'back', :action => :index %>


The execute action in PayrollController relies on the short name of a sales person, and the view.rhtml page uses the active_compensation_script method of the SalesPerson instance.

File: sales_person.rb
class SalesPerson < ActiveRecord::Base
has_many :compensation_scripts

def active_compensation_script
compensation_scripts.sort.first
end

def short_name
self.name.gsub(/^([A-Z])[a-z]+\s/, '\1').downcase
end
end


The execute_all.rhtml also contains minor updates that reflect the introduction of SalesPerson

File: execute_all.rhtml
<% @compensations.each_pair do |name, amount| %>
<%= name %> compensation: <%= number_to_currency amount %><br>
<% end %>
<%= link_to 'back', :action => :index %>


All these changes also mean the CompensationScript and Root classes have been simplified by removing the name of the employee.

File: compensation_script.rb
class CompensationScript < ActiveRecord::Base
def <=>(other)
other.created_at <=> self.created_at
end
end


File: root.rb
class Root
extend Vocabulary

def initialize(vocabulary, eval_context)
@compensations = []
@vocabulary, @eval_context = vocabulary, eval_context
end

def amount
@compensations.collect do |compensation|
compensation.amount
end.inject { |x, y| x + y }
end

def process(line)
instance_eval(line)
end

phrase :compensate do
@compensations << Compensation.new(@vocabulary, @eval_context)
@compensations.last
end

end


Lastly, the Employee class can be completely removed since Root no longer parses an employee's name.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.