Skip to content

Instantly share code, notes, and snippets.

@theturtle32
Last active August 29, 2015 14:21
Show Gist options
  • Save theturtle32/8e2ae967e9828c0a2b7a to your computer and use it in GitHub Desktop.
Save theturtle32/8e2ae967e9828c0a2b7a to your computer and use it in GitHub Desktop.
Rails Repair Shop Example for Eric

Models

app/models/customer.rb

class Customer < ActiveRecord::Base
  has_many :tickets
  has_many :item_ownerships
  has_many :items, through: :item_ownerships
  has_many :comments, as: :commentable
  
  validates :name, presence: true
end

app/models/item.rb

class Item < ActiveRecord::Base
  validates :name, presence: true
end

app/models/item_ownership.rb

class ItemOwnership < ActiveRecord::Base
  # This class models the ownership relationship between items and customers
  # For example, you'd have just one entry in the "items" table for an
  # iPhone 6, and a separate row in the "item_ownerships" table for every
  # customer who owns one.
  
  # The "items" table defines the various types of items, and the "item_ownerships"
  # table records each unique customer/item combination by referencing the item
  # type id from the "items" table and the customer id from the "customers"
  # table.
  belongs_to :item
  belongs_to :customer
  
  # Require both a customer and item type to be specified
  validates :customer, presence: true
  validates :item, presence: true
end

app/models/ticket.rb

class Ticket < ActiveRecord::Base
  belongs_to :customer
  has_and_belongs_to_many :item_ownerships
  has_and_belongs_to_many :service_tasks
  has_many :comments, as: :commentable
  
  # Can't create ticket without both customer and description.
  # Associated items are optional.
  validates :description, presence: true
  validates :customer, presence: true
  
  def total_services_price
    self.service_tasks.map(&:price).reduce(:+)
  end

  def total_parts_price
    self.service_tasks.map(&:total_parts_price).reduce(:+)
  end
  
  def total_parts_cost
    self.service_tasks.map(&:total_parts_cost).reduce(:+)
  end
end

app/models/service_task.rb

class ServiceTask < ActiveRecord::Base
  has_many :part_requirements
  has_many :parts, through: :part_requirements
  
  validates :description, presence: true
  validates :price, numericality: true
  
  def total_parts_price
    self.parts.map(&:price).reduce(:+)
  end
  
  def total_parts_cost
    self.parts.map(&:cost).reduce(:+)
  end
end

app/models/part.rb

class Part < ActiveRecord::Base
  belongs_to :part_requirement
  
  validates :name, presence: true
  validates :price, numericality: true
  validates :cost, numericality: true
end

app/models/part_requirement.rb

class PartRequirement < ActiveRecord::Base
  belongs_to :service_task
  belongs_to :part
  
  validates :quantity, {
    numericality: { greater_than: 0 }
  }
end

app/models/comments.rb

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
  
  validates :commentable, presence: true
  validates :text, presence: true
end

Database Migration

db/migrations/create_initial_tables.rb

class CreateInitialTables < ActiveRecord::Migration
  def change
    create_table :customers do |t|
      t.timestamps
      t.string :name
      # add any other fields you want for customers here
    end
    
    create_table :items do |t|
      t.timestamps
      t.string :name
      # add any other fields you want here
    end
    
    create_table :item_ownerships do |t|
      t.timestamps
      t.references :customer, index: true
      t.references :item, index: true
      t.timestamps
    end
    
    create_table :tickets do |t|
      t.timestamps
      t.string     :description
      t.references :customer, index: true
    end
    
    create_join_table :items, :tickets do |t|
      t.index :item_id
      t.index :ticket_id
    end
    
    create_table :service_tasks do |t|
      t.timestamps
      t.string  :description
      t.decimal :price, precision: 8, scale: 2
    end
    
    create_join_table :service_tasks, :tickets do |t|
      t.index :service_task_id
      t.index :ticket_id
    end
    
    create_table :parts do |t|
      t.timestamps
      t.string  :name
      t.decimal :price, precision: 8, scale: 2
      t.decimal :cost,  precision: 8, scale: 2
    end
    
    create_table :part_requirements do |t|
      t.timestamps
      t.integer    :quantity
      t.references :part
      t.references :service_task
    end
    
    create_table :comments do |t|
      t.timestamps
      t.references :commentable, polymorphic: true, index: true
      t.text       :text
    end
  end
end

Usage

Run Database Migrations

$ rake db:migrate

Rails console for tinkering

$ rails console
# Lets add an iPhone 6 to the items table
iphone6 = Item.create(name: "iPhone 6")

# Now we'll create a service task definition for replacing an iPhone 6 screen
replaceScreen = ServiceTask.create(description: "Replace iPhone 6 Screen", price: 30.00)

# We can associate the parts needed (the actual screen) with the service task.
# First, create the part:
iphone6Screen = Part.create(name: "iPhone 6 Screen", cost: 30.00, price: 65.00)
# Then add a new PartRequirement to the ServiceTask we just created:
replaceScreen.part_requirements.create(part: iphone6Screen, quantity: 1)


# Oh look, our first customer!
brian = Customer.create(name: "Brian McKelvey")
# And he has an iPhone 6.
brian.items << Item.find_by_name("iPhone 6")

# Now we can create a ticket for the broken screen:
ticket = Ticket.create(customer: brian, description: 'Needs screen replaced.')
# ...and add the "Replace iPhone 6 Screen" task to the ticket:
ticket.service_tasks << replaceScreen
# ...and add not just any old iPhone 6, but Brian's iPhone 6:
ticket.item_ownerships << brian.item_ownerships.last

# Because the "iPhone 6 Screen" part is linked to the "Replace iPhone 6 Screen"
# service task, which is linked to the customer's ticket, we can now calculate
# how much the parts will cost in addition to the labor cost.

puts "Price breakdown:"
puts "Labor: $ #{sprintf("%.2f", ticket.total_services_price)}"
puts "Parts: $ #{sprintf("%.2f", ticket.total_parts_price)}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment