The Money Train

The Money Train

Selling music using Ruby on Rails / Building a storefront with Rails

Presented at RailsConf 2006

9b0968d25731bc92a98c3e0b77e6d2ce?s=128

Benjamin Curtis

June 23, 2006
Tweet

Transcript

  1. The Money Train Selling Music using Ruby on Rails Benjamin

    Curtis 21st Century Music
  2. Who is this guy?

  3. None
  4. tesly.com agilewebdevelopment.com bencurtis.com stympy on #rubyonrails

  5. Buy my book! http://www.agilewebdevelopment.com/book

  6. Why build a store? Because you’d be out of a

    job if you told your client to just use Shopify :)
  7. “It’s just...” But it usually isn’t

  8. Code stats 0 1,500 3,000 4,500 6,000 Code LOC Test

    LOC
  9. Getting Started • Product info and product shots • Merchant

    acct. and gateway • Shipping vendors • SSL certificate • Site design
  10. Products and Variants Variant Product ProductVariant LineItem Sale

  11. Product Differentiation class
Album < Product 

belongs_to
:artist 

has_many
:tracks,
:order
=>
'position',
 



:dependent
=>
true, 



:foreign_key
=>
'product_id' 



    

def
self.featured_products 



find(:all,
:include
=>
:artist,
 





:conditions
=>
[
'featured
is
not
null'
], 





:order
=>
'featured
desc') 

end end
  12. Payment Processing

  13. Payment Methods Credit Cards, Bank Accounts, and PayPal (oh my!)

  14. Pass the buck class
Sale < ActiveRecord::Base 

def
validate_on_create 



... 



amount_to_auth
=
self.total_price
-
self.voucher_total 





    



self.payment_transaction
=
 





self.payment_method.authorize(amount_to_auth)
if
 







amount_to_auth
>
0 



...



 

rescue
PaymentMethodError
=>
error 



errors.add_to_base(error.message) 



return
false 

end end
  15. The buck stops here (kinda) class
PaymentMethod < ActiveRecord::Base 

def
authorize(amount) 



processor
=
Payment::TrustCommerce.new(

    





'billingid'
=>
self.processor_id) 



if
!processor.process('preauth',
 





'amount'
=>
amount.to_s) 





raise
PaymentMethodError,
processor.error 



end 



self.payment_transactions.create( 





:processor
=>
processor,
 





:action
=>
'Authorization') 

end end
  16. Sensitive Data • Objective: Speedy checkout for customers • Problem:

    CC data == liability • Solution: Pass the buck • Alternate solution: Encryption Or see http:/ / blog.leetsoft.com/ articles/2006/03/14/ simple-encryption for thoughts on handling the encryption yourself
  17. Gift Certificates suck

  18. class
LineItem < ActiveRecord::Base 

has_many
:line_item_product_options 

has_many
:product_options,
 



:through
=>
:line_item_product_options 

def
product_options=(options) 



options
=
[
options
]
unless
options.respond_to?(:each) 



options.each
do
|opt| 





self.line_item_product_options.build(

    







:product_option_id
=>
opt.id, 







:price
=>
opt.price,
:weight
=>
opt.weight) 



end 

end 

 

def
product_option_names 



self.product_options.collect(&:name).to_sentence 

end end Product Options
  19. Working with Sales Carts, Sales, and Shipments

  20. Discounts class
Discount < ActiveRecord::Base 

def
calculate(subtotal) 



return
0
unless
subtotal.to_i
>
0 



if
self.percent 





(self.amount
/
100.0
*
subtotal).round 



else 





self.amount
>
subtotal
?
subtotal
:
self.amount

    



end 

end end
  21. Discounts module
LineItemCollection 

def
total_price 



self.total.to_i
+
self.shipping_cost.to_i
+
 





self.tax.to_i
-
self.calculate_discount 

end 

 

def
calculate_discount 



self.discount
?
self.discount.calculate( 





self.total.to_i)
:
0

    

end end
  22. Taxes The customer giveth and the government taketh away

  23. Taxes class
Tax 

#
Return
the
tax
rate
(float)
for
an
address
object. 

def
self.for_address(address) 



return
0
unless
address.is_a?(Address) 



return
0.088
if
address.state
and 





address.state.abbreviation
==
'WA' 



return
0 

end 

def
self.on_product(product,

    price, address) 



return
0
unless
product.taxable?
and
price.to_i
>
0
and
 





!address.nil? 



return
(price.to_i
*
(address.is_a?(Address)
?
 





for_address(address)
:
address)).round 

end end
  24. Taxes class
LineItem < ActiveRecord::Base 

def
self.for_product_variant(product_variant_id, address = nil) 



pv
=
ProductVariant.find(product_variant_id,
 





:include
=>
[
:variant,
:product
])
rescue
nil

    



return
false
if
pv.nil?
||
!pv.can_order? 



self.new(:product_variant
=>
pv,
 





:price
=>
pv.price,
:cost
=>
pv.cost, 





:quantity
=>
1,
:weight
=>
pv.weight.to_f, 





:tax
=>
Tax.on_product(pv.product,
pv.price,
 







address)) 

end end
  25. Returns and Refunds

  26. class
Sale < ActiveRecord::Base 

def
refund(item) 



transaction
do 





remainder
=
self.credit_vouchers( 







item.total(!item.shipped?),
item.shipped?) 





if
remainder
>
0
and
item.shipped? 







self.payment_method.refund(self,
remainder) 





end

    



end 

end

 end Refund class
LineItem < ActiveRecord::Base 

def
total(include_options = true) 



(self.price
*
self.quantity)
+
self.tax
+
 





(include_options
?
 







self.product_options.collect(&:price).sum
:
0) 

end end
  27. Shipping ShipMethod Country PostalCode ShipZone ShipRate

  28. Code stats 0 1,500 3,000 4,500 6,000 Code LOC Test

    LOC
  29. Affiliates

  30. Affiliate in da house class
ApplicationController < ActionController::Base 

before_filter
:affiliate_check 

def
affiliate_check 



if
affiliate
=
params[:affiliate]

    





cookies[:affiliate]
=
{
:value
=>
affiliate,
 







:expires
=>
1.week.from_now
} 



end 

end end
  31. Capture the affiliate sale class
CartController < ApplicationController 

def
checkout 



... 



@sale
=
Sale.new(:account
=>
@session[:user],


    





:payment_method
=>
@session[:payment], 





:cart
=>
@cart, 





:affiliate
=>
Account.find_by_affiliate_token( 







cookies[:affiliate])) 



... 

end end
  32. Credit the affiliate class
Shipment < ActiveRecord::Base 

after_create
:credit_affiliate 

def
credit_affiliate 



if
self.affiliate 





self.affiliate_credits.create(

    







:account
=>
self.affiliate, 







:amount
=>
(self.total
*
 









self.affiliate.percentage(self.id)).round) 



end 

end end


  33. Testing Be test-driven... think of the children!

  34. Test stats Tests 620 Assertions 2,436 Test LOC 5,836

  35. Mocks

  36. Just do it

  37. And...

  38. Buy my book! http://www.agilewebdevelopment.com/book Thanks :)