Slide 1

Slide 1 text

The Money Train Selling Music using Ruby on Rails Benjamin Curtis 21st Century Music

Slide 2

Slide 2 text

Who is this guy?

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

tesly.com agilewebdevelopment.com bencurtis.com stympy on #rubyonrails

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Why build a store? Because you’d be out of a job if you told your client to just use Shopify :)

Slide 7

Slide 7 text

“It’s just...” But it usually isn’t

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Getting Started • Product info and product shots • Merchant acct. and gateway • Shipping vendors • SSL certificate • Site design

Slide 10

Slide 10 text

Products and Variants Variant Product ProductVariant LineItem Sale

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Payment Processing

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Gift Certificates suck

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Working with Sales Carts, Sales, and Shipments

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Taxes The customer giveth and the government taketh away

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Returns and Refunds

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Shipping ShipMethod Country PostalCode ShipZone ShipRate

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Affiliates

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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



Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Mocks

Slide 36

Slide 36 text

Just do it

Slide 37

Slide 37 text

And...

Slide 38

Slide 38 text

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