Skip to content
This repository has been archived by the owner on Mar 29, 2018. It is now read-only.

Commit

Permalink
Converted to static version
Browse files Browse the repository at this point in the history
  • Loading branch information
Glenn Murray committed Jun 16, 2008
1 parent 870be22 commit a0a3ddf
Show file tree
Hide file tree
Showing 29 changed files with 890 additions and 133 deletions.
38 changes: 31 additions & 7 deletions README
Expand Up @@ -41,7 +41,7 @@ Within those tags you may use the following tags to display product information:
* <r:shopping:product:code/> - the product's code.
* <r:shopping:product:addtocart/> - an add to cart form for the product.
* <r:shopping:product:description/> - the product's description.
* <r:shopping:product:price/> - the products price.
* <r:shopping:product:price quantity="1"/> - the products price for the given quantity (default quantity=1).

So a simple store page body might contain the following:
<p>Welcome to our store!</p>
Expand Down Expand Up @@ -73,6 +73,20 @@ The tags you may use to create this page part are the same as those which may be
<r:shopping:product:addtocart/>
</p>

= Express Purchase

Use
<r:shopping:product:expresspurchase next_url="/purchase/payment" quantity=1 />

within a shopping:product:each loop.

To produce a button that immediately overrides the existing cart with the specified quantity of this product. If no quantity is specified, then an input field for the quantity is also produced. After processing the controller redirects back to the same page, or next_url if specified.

You can set next_url to point to the eula page or the payment page.

To use with a specific product instead of all products in the each loop, use the tags
<r:shopping:product:each only="prodcode anotherproduct"> ... </r:shopping:product:find>
Where there are multiple products to be selected, list them separated with blanks. Invalid product codes will be ignored.

= Displaying the cart contents
The above product page provides a form for adding a product to our cart but doesn't allow us to view the cart contents or checkout the cart.
Expand All @@ -87,9 +101,9 @@ The main cart contents must appear between <r:shopping:cart:form> ... </r:shoppi

Within <r:shopping:cart:item:each> ... </r:shopping:cart:item:each> tags the following tags are available:
* <r:shopping:cart:item:code /> - The code of the item.
* <r:shopping:cart:item:unitcost /> - The price for one of the item.
* <r:shopping:cart:item:unitcost /> - The price for each item at the current quantity.
* <r:shopping:cart:item:quantity /> - The quantity of the item in the cart.
* <r:shopping:cart:item:subtotal /> - The total cost of all of the item in the cart.
* <r:shopping:cart:item:subtotal /> - The total cost for that quantity of that item in the cart.
* <r:shopping:cart:item:update /> - A textbox where customers may enter a new quantity for the item.
* <r:shopping:cart:item:remove /> - A button to remove the item from the cart.

Expand Down Expand Up @@ -169,10 +183,20 @@ When a user submits a form created using the <r:shopping:checkout:process proces

1. The submitted form data is inspected to make sure the user agreed to the terms and conditions.
2. The cart has a unique identifier added to it.
3. The cart is converted to well-formed XML (examples below).
4. The cart XML is transmitted to processor_url specified in the tags.
5. The response from the processor_url is examined to ensure that the server responded with code 200 (OK).
6. The customer's web browser is redirected to next_url specified in the tags along with the cart id as the GET parameter "cart".
3. If processor_url is specified in tags then
a. The cart is converted to well-formed XML (examples below).
b. The cart XML is transmitted to processor_url specified in the tags.
c. The response from the processor_url is examined to ensure that the server responded with code 200 (OK).
6. The customer's web browser is redirected to next_url specified in the tags. If processor_url is specified in tags then the cart id is passed as the GET parameter "cart".

= Internal Processing

where the next_url is local to this server, then the cart may be accessed via session[:cart]


= External Processing

External processing is enabled by the processor_url specified in tags

After that it is entirely the receiving application's responsibility to turn the cart XML into whatever model it uses and process payment for the cart. A typical process for the external application would be:

Expand Down
12 changes: 12 additions & 0 deletions app/controllers/admin/coupon_controller.rb
@@ -0,0 +1,12 @@
class Admin::CouponController < Admin::AbstractModelController
model_class Coupon
def new
product = Product.find(params[:product_id])
coupon = product.coupons.build
# this is silly
# coupon.product = product unless coupon.product

self.model = coupon
render :template => "admin/#{ model_symbol }/edit" if handle_new_or_edit_post
end
end
12 changes: 12 additions & 0 deletions app/controllers/admin/product_price_controller.rb
@@ -0,0 +1,12 @@
class Admin::ProductPriceController < Admin::AbstractModelController
model_class ProductPrice
def new
product = Product.find(params[:product_id])
product_price = product.product_prices.build
# this is silly
# product_price.product = product unless product_price.product

self.model = product_price
render :template => "admin/#{ model_symbol }/edit" if handle_new_or_edit_post
end
end
86 changes: 76 additions & 10 deletions app/controllers/cart_controller.rb
@@ -1,8 +1,44 @@
require 'net/https'
require 'uri'
require 'digest/md5'
require 'ostruct'

class CartController < ActionController::Base

PRICE_NOT_AVAILABLE = 'N/A'

def savings_for_product_quantity
result = PRICE_NOT_AVAILABLE
begin
product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to lookup invalid savings using product id: #{params[:id]}")
else
base_pp = product.first_product_price
if base_pp
base_price = base_pp.price
quantity = params[:quantity].to_i
this_price = product.price_for_quantity(quantity)
result = sprintf("%4.2f", (quantity.to_f * (base_price - this_price))) if this_price
end
end
render :text => result
end

def price_for_product_quantity
result = PRICE_NOT_AVAILABLE
begin
product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to lookup invalid price using product id: #{params[:id]}")
else
quantity = params[:quantity].to_i
this_price = product.price_for_quantity(quantity)
result = sprintf("%4.2f", this_price) if this_price
end
render :text => result
end

def add_or_update_in_cart
begin
product = Product.find(params[:id])
Expand All @@ -15,6 +51,19 @@ def add_or_update_in_cart
redirect_to :back
end

def expresspurchase
begin
product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to add invalid product to cart using product id: #{params[:id]}")
else
empty_cart
@cart = find_cart
@cart.add_product_or_increase_quantity(product, params[:quantity].to_i)
end
redirect_to params[:next_url] ? params[:next_url] : :back
end

def process_cart_change
# determine which input was used to submit the cart and update or remove accordingly
if params[:submit_type] == "update" || params[:submit_type] == "notajax" && params[:update_submit]
Expand All @@ -37,18 +86,18 @@ def submit_to_processor
# do nothing if the user did not accept the eula
redirect_to :back and return unless params[:agree]


uri = URI.parse( params[:processor_url] )
site = Net::HTTP.new( uri.host, uri.port )
res = site.post( uri.path, contents_xml, { 'Content-Type' => 'text/xml; charset=utf-8' } )
assign_cart_id
unless params[:processor_url].blank?
uri = URI.parse( params[:processor_url] )
site = Net::HTTP.new( uri.host, uri.port )
res = site.post( uri.path, contents_xml, { 'Content-Type' => 'text/xml; charset=utf-8' } )

raise "Processor did not respond with status 200. Instead gave " + res.code.to_s unless res.code == "200"
raise "Processor did not respond with status 200. Instead gave " + res.code.to_s unless res.code == "200"

logger.debug "Response from order processor contained: " + res.body

logger.debug "Response from order processor contained: " + res.body
end
cart = find_cart

redirect_to params[:next_url] + "?cart=#{cart.id}"
redirect_to params[:next_url] + (params[:processor_url].blank? ? '' : "?cart=#{cart.id}")
end

def self.form_to_add_or_update_product_in_cart( product )
Expand All @@ -61,6 +110,17 @@ def self.form_to_add_or_update_product_in_cart( product )
</form> )
end

def self.form_to_express_purchase_product( product, next_url, quantity, src )
quantity_input_type = quantity ? 'hidden' : 'text'
%Q( <form action="/shopping_trike/cart/expresspurchase" method="post"
>
<input type="hidden" id="product_id" name="id" value="#{ product.id }" />
<input id="product_quantity" name="quantity" size="5" type="#{quantity_input_type}" value="#{quantity}" />
<input id="product_next_url" name="next_url" type="hidden" value="#{next_url}" />
<input type="image" name="commit" type="submit" src="#{src}" value="express purchase" />
</form> )
end

def self.cart_form_start_fragment
%Q( <form action="/shopping_trike/cart/process_cart_change" method="post" id="shopping_trike_cart_form" onsubmit="new Ajax.Request('/shopping_trike/cart/process_cart_change',
{asynchronous:true, evalScripts:true, parameters:Form.serialize(this), onSuccess:cart_update}); return false;">
Expand Down Expand Up @@ -109,6 +169,7 @@ def self.cart_ajaxify_script( url_base )
</script> )
end


private
def update_in_cart( prod_id, quantity )
cart = find_cart
Expand All @@ -135,10 +196,14 @@ def empty_cart

def contents_xml
cart = find_cart
cart.id = create_cart_id
cart.xml
end

def assign_cart_id
cart = find_cart
cart.id = create_cart_id
end

# This is similar to how session keys are generated. Use this for unique cart ids and
# _never_ use the session key as we may open ourselves to fixation attacks.
def create_cart_id
Expand All @@ -151,4 +216,5 @@ def create_cart_id
md5.update('foobar')
md5.hexdigest
end

end
9 changes: 9 additions & 0 deletions app/helpers/admin/product_helper.rb
@@ -0,0 +1,9 @@
module Admin::ProductHelper
def currently_used_product_categories
Product.find_by_sql("select product_category from products group by product_category;").collect {|p| "'#{p.product_category}'" }.to_sentence
end
def dom_id(record, prefix = nil)
prefix ||= 'new' unless record.id
[ prefix, record.class.name.singularize, record.id ].compact * '_'
end
end
85 changes: 52 additions & 33 deletions app/models/cart.rb
@@ -1,27 +1,46 @@
class Cart
attr_reader :items
attr_accessor :id
attr_accessor :gst_charged

def initialize
def initialize(options = {:gst_charged => true})
@items = []
options.each_pair {|k, v| self.send(:"#{k}=", v) }
end

def add_product_or_increase_quantity(product, quantity)
current_item = cart_item_for_product( product )
if current_item
current_item.quantity += quantity
else
current_item = CartItem.new(product, quantity)
@items << current_item
if product.is_a?(Coupon)
matching_product = cart_item_for_product( product.product )
if coupon || (quantity > 1)
raise(ArgumentError, "You can only add one Coupon per order")
elsif matching_product.nil?
raise(ArgumentError, "Invalid coupon (no matching products in your cart)")
elsif !product.current?
raise(ArgumentError, "Coupon has expired or is otherwise not valid")
else
matching_product.apply_coupon(product)
end
else # product is a Product
current_item = cart_item_for_product( product )
if current_item
current_item.quantity += quantity
else
current_item = CartItem.new(product, quantity)
@items << current_item
end
end

tidy
end

def set_quantity(product, quantity)
current_item = cart_item_for_product( product )
current_item.quantity = quantity
tidy
end

def override_with_product_quantity(product, quantity)
current_item = CartItem.new(product, quantity)
@items = [ current_item ]
tidy
end

Expand All @@ -35,45 +54,45 @@ def quantity_of_product( product )
item.quantity if item
end

def gst_charged?
@gst_charged
end

def ex_gst_total
grand_total = 0
items.each do |item|
grand_total += item.subtotal(false)
end
grand_total
end

def total
grand_total = 0
items.each do |item|
grand_total += item.subtotal_price
grand_total += item.subtotal(@gst_charged)
end
grand_total
end

def xml
xml = Builder::XmlMarkup.new
xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
xml.cart{
xml.id(id)
xml.items{
items.each do |item|
xml.item do
xml.code(item.product.code)
xml.description(item.product.description)
xml.quantity(item.quantity)
xml.unitcost(item.product.price)
xml.subtotal(item.subtotal_price)
end
end
}
xml.total(total)
}

xml.target!

def gst_amount
total - ex_gst_total
end

private
def cart_item_for_product( product )
@items.find {|item| item.product == product}
items.find {|item| item.product == product}
end

def tidy
@items.each do |item|
items.each do |item|
# every item must have a quantity of at least one
remove_product( item.product ) if item.quantity < 1
end
end

def coupon
item = items.select {|item| item.coupon? }.first
item.product if item
end

end

0 comments on commit a0a3ddf

Please sign in to comment.