Created
August 12, 2013 15:09
-
-
Save vladimir-e/6211657 to your computer and use it in GitHub Desktop.
Shopping cart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Cart | |
include ActiveAttr::Model | |
attribute :items # has_many | |
attribute :qty, :type => Integer | |
attribute :subtotal_chf, :type => Integer | |
attribute :subtotal_eur, :type => Integer | |
attribute :order_id, :type => Integer | |
def initialize(storage) | |
@storage = storage | |
load | |
self | |
end | |
class << self | |
# ugly shortcut | |
def items_count(storage) | |
( storage[:cart][:items].try(:count) if storage[:cart] ).to_i | |
end | |
end | |
def << (item) | |
if item.valid? | |
item.cart = self | |
existing_item = find(item.id) | |
if existing_item | |
existing_item.qty += item.qty | |
else | |
self.items << item | |
end | |
# don't move it to else condition | |
item.after_add | |
self.qty += item.qty | |
self.subtotal_chf += item.price_chf.cents * item.qty | |
self.subtotal_eur += item.price_eur.cents * item.qty | |
save | |
self | |
end | |
end | |
def find(item_id) | |
items.select{ |ci| ci.id == item_id }.first | |
end | |
def inc(item_id, new_qty) | |
item = find item_id | |
if item | |
item.qty += new_qty.to_i | |
self.qty += new_qty.to_i | |
self.subtotal_chf += item.price_chf.cents * new_qty.to_i | |
self.subtotal_eur += item.price_eur.cents * new_qty.to_i | |
save | |
item | |
end | |
end | |
def dec(item_id, new_qty) | |
item = find item_id | |
if item | |
if (item.qty - new_qty.to_i) < 1 | |
remove item_id | |
else | |
item.qty -= new_qty.to_i | |
self.qty -= new_qty.to_i | |
self.subtotal_chf -= item.price_chf.cents * new_qty.to_i | |
self.subtotal_eur -= item.price_eur.cents * new_qty.to_i | |
save | |
item | |
end | |
end | |
end | |
def remove(item_id) | |
item = find item_id | |
return false if !item | |
items.reject!{ |ci| ci.id == item_id } | |
self.qty -= item.qty | |
self.subtotal_chf -= item.price_chf.cents * item.qty | |
self.subtotal_eur -= item.price_eur.cents * item.qty | |
save | |
item | |
end | |
def currency | |
Shop.current.currency | |
end | |
def shipping_chf | |
0 | |
end | |
def shipping_eur | |
0 | |
end | |
def mwst_chf | |
Shop.mwst(subtotal_chf, shipping_chf).round | |
end | |
def mwst_eur | |
Shop.mwst(subtotal_eur, shipping_eur).round | |
end | |
def total_chf | |
subtotal_chf + mwst_chf | |
end | |
def total_eur | |
subtotal_eur + mwst_eur | |
end | |
[:subtotal, :shipping, :mwst, :total].each do |name| | |
define_method(name) do | |
cents = send("#{name}_#{currency}") | |
Money.new(cents || 0, currency) | |
end | |
end | |
def clear! | |
erase_storage! | |
load | |
end | |
def empty? | |
items.count == 0 | |
end | |
def as_json | |
{ | |
qty: I18n.t('cart.item', count: items.count), | |
subtotal: subtotal.format, | |
} | |
end | |
def to_order | |
{ | |
:subtotal_chf_cents => subtotal_chf, | |
:subtotal_eur_cents => subtotal_eur, | |
:items => items.map(&:to_order) | |
} | |
end | |
def update_attributes(attribs) | |
attribs.each do |key, val| | |
storage[key.to_sym] = val | |
self.send("#{key.to_sym}=", val) | |
end | |
end | |
private | |
def load | |
items_data = storage[:items] || [] | |
self.items = items_data.map do |i| | |
ci = CartItem.new(i) | |
ci.cart = self | |
ci | |
end | |
self.qty = storage[:qty] || 0 | |
self.subtotal_chf = storage[:subtotal_chf] || 0 | |
self.subtotal_eur = storage[:subtotal_eur] || 0 | |
self.order_id = storage[:order_id] | |
end | |
def save | |
storage[:items] = self.items.map(&:serialize) | |
storage[:qty] = self.qty | |
storage[:subtotal_chf] = self.subtotal_chf | |
storage[:subtotal_eur] = self.subtotal_eur | |
storage[:order_id] = self.order_id | |
end | |
def storage | |
@storage[:cart] ||= Hash.new | |
end | |
def erase_storage! | |
@storage[:cart] = nil | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CartItem | |
include ActiveAttr::Model | |
attribute :qty, :type => Integer, :default => 1 | |
attribute :product_id, :type => Integer | |
attribute :subproduct_id, :type => Integer | |
attribute :size_id, :type => Integer | |
attribute :colour_id, :type => Integer | |
attr_accessor :cart | |
validates_presence_of :product_id | |
validate :product_exist | |
def id | |
@id ||= "#{product_id}#{colour_id.to_i}#{size_id.to_i}".to_i | |
end | |
# triggers when valid item added to cart | |
def after_add | |
if subproduct? | |
sp = Product.find(product_id) | |
.subproducts.where(colour_id: colour_id, size_id: size_id) | |
self.subproduct_id = sp.first.id if sp.any? | |
end | |
end | |
def product | |
@product ||= ProductPresenter.new(find_product_record) | |
end | |
def colour | |
if colour_id? | |
@colour ||= Colour.find(colour_id) | |
end | |
end | |
def size | |
if size_id? | |
@size ||= Size.find(size_id) | |
end | |
end | |
def serialize | |
attribs = self.attributes | |
attribs.delete("cart") | |
attribs | |
end | |
def price | |
product.send("price_#{cart.currency}") | |
end | |
def total | |
price * qty | |
end | |
def title | |
@title = product.title | |
if colour_id? | |
@title += " (#{colour.name})" | |
end | |
if size_id? | |
@title += " #{size.name}" | |
end | |
@title | |
end | |
def image | |
@image ||= if colour_id? | |
product.colours_images[colour_id] | |
else | |
product.image | |
end | |
end | |
def subproduct? | |
colour_id? || size_id? | |
end | |
def to_param | |
id | |
end | |
def to_s | |
self.title | |
end | |
def as_json | |
{ | |
id: id, | |
title: title, | |
qty: qty, | |
formatted_price: formatted_price, | |
total: total.format, | |
product_path: Rails.application.routes.url_helpers.product_path(product_id), | |
cart_item_path: Rails.application.routes.url_helpers.cart_item_path(self) | |
} | |
end | |
def to_order | |
{ | |
:qty => qty, | |
:price_chf_cents => price_chf_cents, | |
:price_eur_cents => price_eur_cents, | |
:product_id => product_id, | |
:subproduct_id => subproduct_id, | |
} | |
end | |
def method_missing(name, *args) | |
product.send(name, *args) | |
end | |
private | |
def product_exist | |
errors.add(:product_id, "Product should exist") unless Product.where(:id => self.product_id).any? | |
end | |
def find_product_record | |
if subproduct? && subproduct_id? | |
Subproduct.find subproduct_id | |
else | |
Product.find product_id | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'spec_helper' | |
require "active_attr/rspec" | |
describe Cart do | |
let(:storage) { {:session_id => "e0d8ca9c9341ffad30fa03163b4414a1"} } | |
let(:product) { FactoryGirl.create(:product, :price_eur => 200, :price_chf => 250) } | |
def cart_item(attributes = {}) | |
attributes = { | |
:product_id => product.id, | |
}.merge(attributes) | |
CartItem.new(attributes) | |
end | |
def cart_item_unique(attributes = {}) | |
attributes = { | |
:product_id => FactoryGirl.create(:product, | |
:price_eur_cents => Random.rand(300..2000), | |
:price_chf_cents => Random.rand(450..3000) | |
).id | |
}.merge(attributes) | |
cart_item(attributes) | |
end | |
subject { Cart.new storage } | |
its(:items) { should eql([]) } | |
its(:qty) { should eql(0) } | |
its(:subtotal_chf) { should eql(0) } | |
its(:subtotal_eur) { should eql(0) } | |
its(:total_chf) { should eql(0) } | |
its(:total_eur) { should eql(0) } | |
its(:shipping_chf) { should eql(0) } | |
its(:shipping_eur) { should eql(0) } | |
its(:mwst_chf) { should eql(0) } | |
its(:mwst_eur) { should eql(0) } | |
it { should respond_to(:subtotal) } | |
it { should respond_to(:shipping) } | |
it { should respond_to(:mwst) } | |
it { should respond_to(:total) } | |
it { should respond_to(:<<) } | |
it { should respond_to(:currency) } | |
describe "#<<" do | |
it "doesn't add invalid item" do | |
(subject << cart_item(product_id: 999)).should be_nil | |
subject.items.length.should eql(0) | |
end | |
it "adds cart_item to items" do | |
subject << cart_item_unique | |
subject << cart_item_unique | |
subject << cart_item_unique | |
subject.items.length.should eql(3) | |
end | |
it "only increases qty if same item added" do | |
subject << cart_item(:qty => 1) | |
subject << cart_item(:qty => 3) | |
subject << cart_item(:qty => 5) | |
subject.items.length.should eql(1) | |
subject.qty.should eql(9) | |
subject.items.first.qty.should eql(9) | |
end | |
it "accumulates items quantity in #qty" do | |
subject << cart_item(:qty => 3) | |
subject << cart_item(:qty => 5) | |
subject.qty.should eql(8) | |
end | |
it "accumulates items price in #subtotal in cents" do | |
ci1 = cart_item(:qty => 2) | |
ci1.price_chf = 50 | |
ci1.price_eur = 20 | |
subject << ci1 | |
ci2 = cart_item(:qty => 1) | |
ci2.price_chf = 200 | |
ci2.price_eur = 130 | |
subject << ci2 | |
subject.subtotal_chf.should eql((50*2+200)*100) | |
subject.subtotal_eur.should eql((20*2+130)*100) | |
end | |
it "triggers CartItem#after_add callback" do | |
ci = cart_item | |
ci.should_receive(:after_add) | |
subject << ci | |
end | |
end | |
describe "#find" do | |
it "returns item by item_id" do | |
ci1 = cart_item_unique | |
ci2 = cart_item_unique | |
subject << ci1 | |
subject << ci2 | |
subject << cart_item_unique | |
subject.find(ci2.id).should eql(ci2) | |
subject.find(ci1.id).should eql(ci1) | |
end | |
end | |
describe "#inc" do | |
it "increases item's quantity and other stuff happens automatically" do | |
ci = cart_item_unique(:qty => 3) | |
subject << ci | |
subject.inc(ci.id, 2) | |
subject.qty.should eql(5) | |
subject.subtotal_chf.should eql(ci.price_chf_cents*5) | |
subject.subtotal_eur.should eql(ci.price_eur_cents*5) | |
subject.items.first.qty.should eql(5) | |
end | |
end | |
describe "#dec" do | |
it "descreases and stuff" do | |
ci = cart_item_unique(:qty => 3) | |
subject << ci | |
subject.dec(ci.id, 2) | |
subject.qty.should eql(1) | |
subject.subtotal_chf.should eql(ci.price_chf_cents*1) | |
subject.subtotal_eur.should eql(ci.price_eur_cents*1) | |
subject.items.first.qty.should eql(1) | |
end | |
it "removes item if new qty less than one" do | |
ci = cart_item_unique(:qty => 2) | |
subject << ci | |
subject.dec(ci.id, 3) | |
subject.items.count.should eql(0) | |
subject.subtotal_chf.should eql(0) | |
subject.subtotal_eur.should eql(0) | |
subject.qty.should eql(0) | |
end | |
end | |
describe "#remove" do | |
let (:ci1) { cart_item_unique(:qty => 3) } | |
let (:ci2) { cart_item_unique(:qty => 1) } | |
before(:each) do | |
subject << ci1 | |
subject << ci2 | |
end | |
it "returns false if item not found" do | |
subject.remove(999).should be_false | |
end | |
it "returns removed item" do | |
subject.remove(ci1.id).should eql(ci1) | |
end | |
it "removes item by item_id" do | |
subject.items.count.should eql(2) | |
subject.remove ci1.id | |
subject.items.count.should eql(1) | |
subject.remove ci2.id | |
subject.items.should be_empty | |
subject.send(:storage)[:items].should be_empty | |
end | |
it "changes qty and subtotal" do | |
subject.qty.should eql(4) | |
subject.remove ci1.id | |
subject.qty.should eql(1) | |
subject.remove ci2.id | |
subject.qty.should eql(0) | |
subject.subtotal_chf.should eql(0) | |
subject.subtotal_eur.should eql(0) | |
end | |
end | |
describe "#subtotal" do | |
its(:subtotal) { should be_instance_of(Money) } | |
it "proxy to #subtotal_{current_currency} attribute" do | |
subject.subtotal_chf = 555 | |
subject.subtotal_eur = 777 | |
subject.stub(:currency).and_return(:chf) | |
subject.subtotal.should eql(Money.new(555, :chf)) | |
subject.stub(:currency).and_return(:eur) | |
subject.subtotal.should eql(Money.new(777, :eur)) | |
end | |
end | |
describe "#clear!" do | |
it "resets storage" do | |
subject << cart_item(:qty => 3) | |
subject << cart_item(:qty => 1) | |
subject.clear! | |
subject.send(:storage)[:items].should be_nil | |
subject.send(:storage)[:qty].should be_nil | |
subject.send(:storage)[:total_eur].should be_nil | |
subject.send(:storage)[:total_chf].should be_nil | |
subject.items.should be_empty | |
subject.qty.should eql(0) | |
subject.subtotal_eur.should eql(0) | |
subject.subtotal_chf.should eql(0) | |
end | |
end | |
describe "#load" do | |
let(:storage_mock) { | |
p1 = FactoryGirl.create(:product) | |
p2 = FactoryGirl.create(:product) | |
{ | |
:items => [ | |
{product_id: p1.id, qty: 4}, | |
{product_id: p2.id, qty: 2} | |
], | |
:qty => 6, | |
:subtotal_eur => 600, | |
:subtotal_chf => 800 | |
} | |
} | |
before(:each) do | |
subject.stub(:storage).and_return(storage_mock) | |
subject.send("load") | |
end | |
it "gets values from storage" do | |
subject.qty.should eql(6) | |
subject.subtotal_chf.should eql(800) | |
subject.subtotal_eur.should eql(600) | |
subject.items.count.should eql(2) | |
end | |
it "unserializes items" do | |
ci1 = subject.items.first | |
ci2 = subject.items.last | |
ci1.should be_instance_of(CartItem) | |
ci1.cart.should be_eql(subject) | |
ci1.price_chf.should be_a_kind_of(Money) | |
ci1.price_eur.should be_a_kind_of(Money) | |
end | |
end | |
describe "#to_order" do | |
it "returns Hash with totals and items array" do | |
ci1 = cart_item_unique(:qty => 2) | |
ci2 = cart_item_unique(:qty => 3) | |
total_chf = (ci1.price_chf*ci1.qty)+(ci2.price_chf*ci2.qty) | |
total_eur = (ci1.price_eur*ci1.qty)+(ci2.price_eur*ci2.qty) | |
subject << ci1 | |
subject << ci2 | |
result = subject.to_order | |
result[:subtotal_chf_cents].should eql(total_chf.cents) | |
result[:subtotal_eur_cents].should eql(total_eur.cents) | |
end | |
end | |
describe "#update_attributes" do | |
it "updates value in this instance and in storage" do | |
subject.update_attributes(order_id: 123) | |
subject.order_id.should eql(123) | |
subject.send(:storage)[:order_id].should eql(123) | |
end | |
end | |
it "stores serialized items in storage" do | |
subject << cart_item | |
subject.send(:storage)[:items].first.should be_instance_of(Hash) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'spec_helper' | |
require "active_attr/rspec" | |
describe CartItem do | |
let(:product) do | |
FactoryGirl.create(:product, :price_chf_cents => 5000, | |
:price_eur_cents => 3000) | |
end | |
def valid_item(attribs = {}) | |
CartItem.any_instance.stub(:cart).and_return(Cart.new({:session => :qwerty})) | |
CartItem.new({"product_id" => product.id}.merge(attribs)) | |
end | |
def cart_item_with_subproduct(attribs) | |
product | |
sp = FactoryGirl.build(:subproduct_orphan, {id: 99}.merge(attribs)) | |
product.subproducts << sp | |
ci = valid_item( {:product_id => product.id}.merge(attribs) ) | |
ci.after_add | |
ci | |
end | |
subject { valid_item } | |
it { should have_attribute(:product_id) } | |
it { should have_attribute(:subproduct_id) } | |
it { should have_attribute(:qty).of_type(Integer).with_default_value_of(1) } | |
it { should respond_to(:cart) } | |
its(:price) { should be_a_kind_of(Money) } | |
its(:price) { should eql(product.price) } | |
describe "validates" do | |
it { subject.should be_valid } | |
it "poduct presence" do | |
p = CartItem.new(:product_id => nil) | |
p.should have_errors_on(:product_id) | |
end | |
it "product existence" do | |
p = CartItem.new(:product_id => 999) | |
p.should have_errors_on(:product_id) | |
end | |
end | |
describe "associations" do | |
describe "#product" do | |
it {should respond_to(:product)} | |
its(:product) { should be_instance_of(ProductPresenter) } | |
context "references" do | |
specify "to Product" do | |
ci = valid_item(:product_id => product.id) | |
ci.product.id.should eql(product.id) | |
end | |
specify "to Subproduct if colour and/or size attributes set" do | |
ci = cart_item_with_subproduct colour_id: 333, size_id:5, price_eur_cents: 3990, price_chf_cents: 6990 | |
ci.product.id.should eql(99) | |
ci.product.price_chf.to_s.should eql("69.90") | |
ci.product.price_eur.to_s.should eql("39.90") | |
end | |
end | |
end | |
describe "#colour" do | |
its(:colour) { should be_nil } | |
it "associates with colour if colour_id specified" do | |
colour = FactoryGirl.create(:colour) | |
subject.colour_id = colour.id | |
subject.colour.should eql(colour) | |
end | |
end | |
describe "#size" do | |
its(:size) { should be_nil } | |
it "associates with size if colour_id specified" do | |
size = FactoryGirl.create(:size_with_type) | |
subject.size_id = size.id | |
subject.size.should eql(size) | |
end | |
end | |
end | |
describe "when references to subproduct" do | |
it "should return its attributes" do | |
product.price_chf_cents = 1200 | |
product.price_eur_cents = 900 | |
product.save! | |
ci1 = cart_item_with_subproduct colour_id: 3, size_id: 5, price_chf_cents: 9996, price_eur_cents: 3999 | |
# just to make sure that there is a reference to product | |
ci1.product.product.price_chf.to_s.should eql("12.00") | |
# now actual check | |
ci1.price_chf.to_s.should eql("99.96") | |
ci1.price_eur.to_s.should eql("39.99") | |
end | |
end | |
describe "callbacks" do | |
let(:cart) { Cart.new({:session_id => "e0d8ca9c9341ffad30fa03163b4414a1"}) } | |
describe "#after_add" do | |
context "set subproduct_id when" do | |
specify "colour and size provided" do | |
ci = cart_item_with_subproduct colour_id: 3, size_id: 5 | |
ci.subproduct_id.should eql(99) | |
end | |
specify "only colour provided" do | |
ci = cart_item_with_subproduct colour_id: 3, size_id: nil | |
ci.subproduct_id.should eql(99) | |
end | |
specify "only size provided" do | |
ci = cart_item_with_subproduct colour_id: nil, size_id: 5 | |
ci.subproduct_id.should eql(99) | |
end | |
end | |
it "leaves subproduct_id empty if no colour/size specified" do | |
ci = valid_item(colour_id: nil, size_id: nil) | |
ci.after_add | |
ci.subproduct_id.should be_nil | |
end | |
end | |
end | |
describe "#id" do | |
before(:each) do | |
CartItem.any_instance.stub(:product_id).and_return(999) | |
end | |
it "is a combination of product_id, colour_id and size_id" do | |
item = valid_item(colour_id: 123, size_id: 555) | |
item.id.should be_instance_of(Fixnum) | |
item.id.should eql("999123555".to_i) | |
end | |
it "replaces nil with 0" do | |
item = valid_item(colour_id: 123, size_id: nil) | |
item.id.should eql("9991230".to_i) | |
end | |
it "replaces nil with 0" do | |
item = valid_item(colour_id: nil, size_id: 555) | |
item.id.should eql("9990555".to_i) | |
end | |
end | |
describe "#serialize_item" do | |
it "converts to hash of simple data structures and removes #cart reference" do | |
serialized = subject.serialize | |
serialized.should be_instance_of(Hash) | |
serialized["product_id"].should eql(product.id) | |
serialized["qty"].should eql(1) | |
serialized["cart"].should be_nil | |
# make sure there is only strings, integers and nils left | |
serialized.each do |k,v| | |
[String, Fixnum, NilClass].should include(v.class), "#{k} isn't simple data struct" | |
end | |
end | |
it "holds subproduct_id too" do | |
ci = cart_item_with_subproduct colour_id: 333, size_id:5 | |
serialized = ci.serialize | |
serialized["subproduct_id"].should eql(99) | |
end | |
end | |
describe "#price" do | |
it "uses cart currency" do | |
subject.cart.stub(:currency).and_return(:chf) | |
subject.price.currency.id.should eql(:chf) | |
subject.cart.stub(:currency).and_return(:eur) | |
subject.price.currency.id.should eql(:eur) | |
end | |
end | |
describe "#total" do | |
it "multiplie price by qty" do | |
subject.stub(:price).and_return(300) | |
subject.stub(:qty).and_return(3) | |
subject.total.should eql(900) | |
end | |
end | |
describe "#subproduct?" do | |
it "detects if current item is subproduct based on colour_id or size_id presense" do | |
valid_item(:colour_id => nil, :size_id => nil).subproduct?.should be_false | |
valid_item(:colour_id => 555, :size_id => nil).subproduct?.should be_true | |
valid_item(:colour_id => nil, :size_id => 333).subproduct?.should be_true | |
end | |
end | |
describe "#method_missing" do | |
it "proxy calls to product presenter " do | |
ProductPresenter.any_instance.stub(:formatted_price).and_return("CHF 55.99") | |
subject.formatted_price.should eql("CHF 55.99") | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CartItemsController < ApplicationController | |
def index | |
@cart = Cart.new(session) | |
respond_to do |format| | |
format.html | |
format.json { render cart.to_json } | |
end | |
end | |
def create | |
@item = CartItem.new(params[:cart_item]) | |
cart = Cart.new session | |
respond_to do |format| | |
if cart << @item | |
# in case it was existing item get it again | |
# there may be different qty | |
@item = cart.find(@item.id) | |
format.html { redirect_to product_url(@item.product), notice: I18n.translate("cart.added", :product => @item.title) } | |
format.json { render json: {item: @item.as_json, cart: cart.as_json}, status: :created, location: product_url(@item.product_id) } | |
else | |
format.html { redirect_to :back, alert: I18n.translate("cart.error") } | |
format.json { render json: @item.errors, status: :unprocessable_entity } | |
end | |
end | |
end | |
def update | |
cart = Cart.new(session) | |
id = params[:id].to_i | |
qty = params[:op].to_i | |
if qty > 0 then cart.inc(id, qty) end | |
if qty < 0 then cart.dec(id, qty.abs) end | |
respond_to do |format| | |
format.html { redirect_to :back, notice: I18n.translate("cart.updated") } | |
format.json { render :json => {item: cart.find(id).as_json, cart: cart.as_json} } | |
end | |
end | |
def destroy | |
cart = Cart.new(session) | |
@item = cart.remove(params[:id].to_i) | |
respond_to do |format| | |
format.html { redirect_to :back, notice: I18n.translate("cart.removed", :product => @item) } | |
format.json { render :json => cart.as_json } | |
end | |
end | |
def clear | |
cart = Cart.new session | |
if cart.order_id | |
current_order = current_user.account.orders.where(id: cart.order_id).first | |
if current_order | |
current_order.destroy | |
end | |
end | |
cart.clear! | |
respond_to do |format| | |
format.html { redirect_to :back, notice: I18n.translate("cart.cleared") } | |
format.json { render :json => cart.as_json } | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class OrdersController < ApplicationController | |
before_filter :initialize_cart | |
before_filter :redirect_empty_cart, :only => [:new, :create] | |
before_filter :redirect_invalid_order_id, :only => [:payment, :pay] | |
def new | |
if signed_in? | |
@user = current_user | |
else | |
@user = User.new | |
@user.build_account | |
session[:continue_order_after_sign_in] = 1 | |
end | |
@order = @user.account.orders.build | |
@order.populate_from(@user.account) | |
end | |
def create | |
if signed_in? | |
@user = current_user | |
else | |
@user = User.new | |
@user.build_account | |
end | |
@order = @user.account.orders.new(permitted_params.checkout) | |
@order.setup @cart.to_order | |
@order.ip = request.remote_ip | |
@order.useragent = request.env['HTTP_USER_AGENT'] | |
# if editing order | |
if signed_in? && @cart.order_id | |
# find recently created order | |
current_order = current_user.account.orders.where(id: @cart.order_id).first | |
if current_order | |
# destroy it and create new one with the same id | |
# to avoid messing with updated items or attributes | |
@order.id = current_order.id | |
current_order.destroy | |
end | |
end | |
respond_to do |format| | |
if @order.valid? | |
if @user.new_record? | |
@user.account.populate_from(@order) | |
@user.setup!(email: @order.email) | |
@user.save | |
sign_in(:user, @user) | |
AccountMailer.welcome(@user.account, @user.password).deliver | |
end | |
@order.email = nil | |
@order.save | |
# set current order in case user want to get back and edit items or info | |
@cart.update_attributes(order_id: @order.id) | |
session.delete :continue_order_after_sign_in | |
format.html { redirect_to payment_order_path(@order) } | |
format.json { render json: @order, status: :created, location: payment_order_path } | |
else | |
format.html do | |
flash.now[:alert] = I18n.t('checkout.form_error') | |
render action: "new" | |
end | |
format.json { render json: @order.errors, status: :unprocessable_entity } | |
end | |
end | |
end | |
def payment | |
@order = current_order | |
end | |
def pay | |
@order = current_order | |
@order.payment_method = params[:order][:payment_method] | |
case @order.payment_method | |
when 'prepay' | |
@order.status = :new | |
if @order.save | |
order_success | |
else | |
order_failure | |
end | |
when 'postpay' | |
@order.status = :pending | |
pp = Postpay.new | |
redirect_url = pp.submit_cart_request(@order) | |
if redirect_url | |
@order.save | |
redirect_to redirect_url | |
elsif pp.already_sent? | |
redirect_url = pp.continue_order(@order) | |
redirect_to redirect_url | |
else | |
order_failure(pp.error) | |
end | |
else | |
order_failure | |
end | |
end | |
def order_success | |
@cart.clear! | |
OrderMailer.receipt(@order).deliver | |
OrderMailer.admin_notice(@order).deliver | |
redirect_to confirmation_order_path(@order), notice: I18n.t('checkout.order_success') | |
end | |
private :order_success | |
def order_failure(error_message = I18n.t('checkout.error')) | |
redirect_to payment_order_path(params[:id].to_i), alert: error_message | |
end | |
private :order_failure | |
def postpay_success | |
# check order status to make sure it has been paid | |
# then redirect or display error | |
@order = current_order | |
@order.status = :new | |
@order.save | |
order_success | |
end | |
def postpay_error | |
order_failure | |
end | |
def postpay_back | |
redirect_to payment_order_path(params[:id].to_i) | |
end | |
def confirmation | |
@order = current_order | |
end | |
private | |
def initialize_cart | |
@cart = Cart.new session | |
end | |
def current_order | |
current_user.account.orders.find(params[:id].to_i) | |
end | |
def redirect_empty_cart | |
if @cart.items.count == 0 | |
redirect_to cart_items_path, alert: I18n.t('cart.empty') unless Rails.env.test? | |
end | |
end | |
def redirect_invalid_order_id | |
if @cart.order_id != params[:id].to_i | |
redirect_to new_order_path, alert: I18n.t('checkout.error') unless Rails.env.test? | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'spec_helper' | |
describe OrdersController do | |
describe "GET 'new'" do | |
it "returns http success" do | |
get :new | |
response.should be_success | |
response.should render_template(:new) | |
end | |
end | |
describe "POST 'create'" do | |
let (:valid_params) do | |
{ | |
title: "mr", | |
lastname: "Super", | |
firstname: "Hero", | |
email: "[email protected]", | |
phone: "+7-SUPER-HERO", | |
mobile: "", | |
company: "Avengers", | |
street: "Power st 777 apt. 4", | |
city: "Unrealville", | |
region: "", | |
postcode: "99999", | |
country: "Switzerland", | |
ship_to_billing: "0", | |
shipping_title: "mrs", | |
shipping_lastname: "Blade", | |
shipping_firstname: "Sonya", | |
shipping_street: "Magic st 211", | |
shipping_city: "", | |
shipping_region: "", | |
shipping_postcode: "", | |
shipping_country: "", | |
comment: "Thanks!", | |
terms: "1" | |
} | |
end | |
describe "with valid params" do | |
describe "new customer" do | |
it "creates new user and customer account and order" do | |
expect { | |
post :create, :order => valid_params | |
}.to change(Order, :count).by(1) | |
order = Order.last | |
order.firstname.should eql("Hero") | |
order.account.firstname.should eql("Hero") | |
order.account.lastname.should eql("Super") | |
order.account.email.should eql("[email protected]") | |
response.should redirect_to(payment_order_path(order)) | |
end | |
end | |
describe "existing customer" do | |
before(:each) do | |
@user = FactoryGirl.create(:user, email: "[email protected]") | |
@account = @user.account | |
sign_in @user | |
end | |
it "creates new order, but not account or user records" do | |
expect { | |
post :create, :order => valid_params | |
}.to change(Order, :count).by(1) | |
Account.count.should be(1) | |
User.count.should be(1) | |
Order.last.account_id.should eql(@account.id) | |
end | |
end | |
end | |
describe "with invalid attributes" do | |
let (:invalid_params) do | |
{ | |
company: "Test inc.", | |
account_attributes: { | |
user_attributes: { email: "[email protected]" } | |
} | |
} | |
end | |
it "renders :new and assign unsaved order" do | |
post :create, order: invalid_params | |
assigns(:order).new_record?.should be_true | |
response.should render_template(:new) | |
end | |
it "should not create any db records yet" do | |
post :create, order: invalid_params | |
User.count.should be(0) | |
Account.count.should be(0) | |
Order.count.should be(0) | |
OrderItem.count.should be(0) | |
end | |
end | |
end | |
let(:account) do | |
user = FactoryGirl.create(:user) | |
user.account | |
end | |
let(:order) { FactoryGirl.create(:order, account_id: account.id) } | |
let(:product) { FactoryGirl.create(:product) } | |
describe "order processing" do | |
before(:each) do | |
sign_in account.user | |
end | |
describe "GET 'payment'" do | |
it "returns http success" do | |
get :payment, id: order.id | |
response.should be_success | |
response.should render_template(:payment) | |
assigns(:order).should eql(order) | |
end | |
end | |
describe "POST 'pay'" do | |
describe "payment method" do | |
it "unknown should fail order" do | |
get :pay, id: order.id, order: {payment_method: "unknown"} | |
response.should redirect_to(payment_order_path(order.id)) | |
end | |
describe "prepay" do | |
it "should place order" do | |
get :pay, id: order.id, order: {payment_method: "prepay"} | |
assigns(:order).status.should eql(:new) | |
mails_count = ActionMailer::Base.deliveries.count | |
ActionMailer::Base.deliveries[mails_count - 2].to.should eql([account.email]) | |
ActionMailer::Base.deliveries.last.to.should eql([Shop.current.email]) | |
response.should redirect_to(confirmation_order_path(order)) | |
end | |
end | |
describe "postpay" do | |
it "redirects to postpay gate" do | |
postpay_url = "https://pp-api.mepa-home.de/frontend/checkout/overview.html" | |
Postpay.any_instance.stub(:submit_cart_request).and_return(postpay_url) | |
get :pay, id: order.id, order: {payment_method: "postpay"} | |
assigns(:order).status.should eql(:pending) | |
response.should redirect_to(postpay_url) | |
end | |
it "redirects to gate to continue order" do | |
continue_url = "https://pp-api.mepa-home.de/frontend/checkout/overview.html" | |
Postpay.any_instance.stub(:submit_cart_request).and_return(nil) | |
Postpay.any_instance.stub(:already_sent?).and_return(true) | |
Postpay.any_instance.stub(:continue_order).and_return(continue_url) | |
get :pay, id: order.id, order: {payment_method: "postpay"} | |
response.should redirect_to(continue_url) | |
end | |
it "redirects back if failed" do | |
continue_url = "https://pp-api.mepa-home.de/frontend/checkout/overview.html" | |
Postpay.any_instance.stub(:submit_cart_request).and_return(nil) | |
Postpay.any_instance.stub(:already_sent?).and_return(false) | |
get :pay, id: order.id, order: {payment_method: "postpay"} | |
response.should redirect_to(payment_order_path(order.id)) | |
end | |
end | |
end | |
end | |
describe "returning from postpay" do | |
it "success" do | |
get :postpay_success, id: order.id | |
assigns(:order).status.should eql(:new) | |
mails_count = ActionMailer::Base.deliveries.count | |
ActionMailer::Base.deliveries[mails_count - 2].to.should eql([account.email]) | |
ActionMailer::Base.deliveries.last.to.should eql([Shop.current.email]) | |
response.should redirect_to(confirmation_order_path(order)) | |
end | |
it "back" do | |
get :postpay_back, id: order.id | |
response.should redirect_to(payment_order_path(order.id)) | |
end | |
it "error" do | |
get :postpay_error, id: order.id | |
response.should redirect_to(payment_order_path(order.id)) | |
end | |
end | |
describe "GET 'confirmation'" do | |
it "returns http success" do | |
get :confirmation, id: order.id | |
response.should be_success | |
response.should render_template(:confirmation) | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment