merchant_sidekick 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +12 -0
- data/Changelog.md +38 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +19 -0
- data/README.md +88 -0
- data/Rakefile +10 -0
- data/lib/merchant_sidekick.rb +45 -0
- data/lib/merchant_sidekick/active_merchant/credit_card_payment.rb +117 -0
- data/lib/merchant_sidekick/active_merchant/gateways/authorize_net_gateway.rb +26 -0
- data/lib/merchant_sidekick/active_merchant/gateways/base.rb +29 -0
- data/lib/merchant_sidekick/active_merchant/gateways/bogus_gateway.rb +19 -0
- data/lib/merchant_sidekick/active_merchant/gateways/paypal_gateway.rb +43 -0
- data/lib/merchant_sidekick/addressable/address.rb +400 -0
- data/lib/merchant_sidekick/addressable/addressable.rb +353 -0
- data/lib/merchant_sidekick/buyer.rb +99 -0
- data/lib/merchant_sidekick/gateway.rb +81 -0
- data/lib/merchant_sidekick/install.rb +19 -0
- data/lib/merchant_sidekick/invoice.rb +179 -0
- data/lib/merchant_sidekick/line_item.rb +128 -0
- data/lib/merchant_sidekick/migrations/addressable.rb +47 -0
- data/lib/merchant_sidekick/migrations/billing.rb +100 -0
- data/lib/merchant_sidekick/migrations/shopping_cart.rb +28 -0
- data/lib/merchant_sidekick/money.rb +38 -0
- data/lib/merchant_sidekick/order.rb +244 -0
- data/lib/merchant_sidekick/payment.rb +59 -0
- data/lib/merchant_sidekick/purchase_invoice.rb +180 -0
- data/lib/merchant_sidekick/purchase_order.rb +350 -0
- data/lib/merchant_sidekick/railtie.rb +7 -0
- data/lib/merchant_sidekick/sales_invoice.rb +56 -0
- data/lib/merchant_sidekick/sales_order.rb +122 -0
- data/lib/merchant_sidekick/sellable.rb +88 -0
- data/lib/merchant_sidekick/seller.rb +93 -0
- data/lib/merchant_sidekick/shopping_cart/cart.rb +225 -0
- data/lib/merchant_sidekick/shopping_cart/line_item.rb +152 -0
- data/lib/merchant_sidekick/version.rb +3 -0
- data/merchant_sidekick.gemspec +37 -0
- data/spec/address_spec.rb +153 -0
- data/spec/addressable_spec.rb +250 -0
- data/spec/buyer_spec.rb +203 -0
- data/spec/cart_line_item_spec.rb +58 -0
- data/spec/cart_spec.rb +213 -0
- data/spec/config/merchant_sidekick.yml +10 -0
- data/spec/credit_card_payment_spec.rb +175 -0
- data/spec/fixtures/addresses.yml +97 -0
- data/spec/fixtures/line_items.yml +18 -0
- data/spec/fixtures/orders.yml +24 -0
- data/spec/fixtures/payments.yml +17 -0
- data/spec/fixtures/products.yml +12 -0
- data/spec/fixtures/users.yml +11 -0
- data/spec/gateway_spec.rb +136 -0
- data/spec/invoice_spec.rb +79 -0
- data/spec/line_item_spec.rb +65 -0
- data/spec/order_spec.rb +85 -0
- data/spec/payment_spec.rb +14 -0
- data/spec/purchase_invoice_spec.rb +70 -0
- data/spec/purchase_order_spec.rb +191 -0
- data/spec/sales_invoice_spec.rb +58 -0
- data/spec/sales_order_spec.rb +107 -0
- data/spec/schema.rb +28 -0
- data/spec/sellable_spec.rb +34 -0
- data/spec/seller_spec.rb +201 -0
- data/spec/spec_helper.rb +255 -0
- metadata +201 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
# Implements inbound invoices, i.e. the merchant sells a product to a client.
|
2
|
+
module MerchantSidekick
|
3
|
+
class SalesInvoice < Invoice
|
4
|
+
#--- associations
|
5
|
+
#has_many :sales_orders, :class_name => "MerchantSidekick::SalesOrder"
|
6
|
+
|
7
|
+
#--- instance methods
|
8
|
+
|
9
|
+
def sales_invoice?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
# cash invoice, combines authorization and capture in one step
|
14
|
+
def cash(payment_object, options={})
|
15
|
+
transaction do
|
16
|
+
cash_result = MerchantSidekick::Payment.class_for(payment_object).transfer(
|
17
|
+
gross_total,
|
18
|
+
payment_object,
|
19
|
+
payment_options(options)
|
20
|
+
)
|
21
|
+
self.push_payment(cash_result)
|
22
|
+
|
23
|
+
save(:validate => false)
|
24
|
+
|
25
|
+
if cash_result.success?
|
26
|
+
payment_paid!
|
27
|
+
else
|
28
|
+
transaction_declined!
|
29
|
+
end
|
30
|
+
cash_result
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# returns a hash of additional merchant data passed to authorize
|
35
|
+
# you want to pass in the following additional options
|
36
|
+
#
|
37
|
+
# :ip => ip address of the buyer
|
38
|
+
#
|
39
|
+
def payment_options(options={})
|
40
|
+
{ # general
|
41
|
+
:buyer => self.buyer,
|
42
|
+
:seller => self.seller,
|
43
|
+
:payable => self,
|
44
|
+
# active merchant relevant
|
45
|
+
:customer => "#{self.seller.name} (#{self.seller.id})",
|
46
|
+
:email => self.seller && self.seller.respond_to?(:email) ? self.seller.email : nil,
|
47
|
+
:invoice => self.number,
|
48
|
+
:merchant => self.buyer ? "#{self.buyer.name} (#{self.buyer.id})" : nil,
|
49
|
+
:currency => self.currency,
|
50
|
+
:billing_address => self.billing_address ? self.billing_address.to_merchant_attributes : nil,
|
51
|
+
:shipping_address => self.shipping_address ? self.shipping_address.to_merchant_attributes : nil
|
52
|
+
}.merge(options)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Implements inbound orders, i.e. when merchant sells a product.
|
2
|
+
module MerchantSidekick
|
3
|
+
class SalesOrder < Order
|
4
|
+
belongs_to :sales_invoice, :foreign_key => :invoice_id, :class_name => "::MerchantSidekick::SalesInvoice"
|
5
|
+
|
6
|
+
# Cash the order and generate invoice
|
7
|
+
def cash(payment_object, options={})
|
8
|
+
defaults = { :order_id => number }
|
9
|
+
options = defaults.merge(options).symbolize_keys
|
10
|
+
|
11
|
+
# before_payment
|
12
|
+
seller.send( :before_payment, self ) if seller && seller.respond_to?( :before_payment )
|
13
|
+
|
14
|
+
self.build_addresses
|
15
|
+
self.build_invoice unless self.invoice
|
16
|
+
|
17
|
+
payment = self.invoice.cash(payment_object, options)
|
18
|
+
if payment.success?
|
19
|
+
process_payment!
|
20
|
+
approve_payment!
|
21
|
+
end
|
22
|
+
|
23
|
+
# after_payment
|
24
|
+
buyer.send( :after_payment, self ) if buyer && buyer.respond_to?( :after_payment )
|
25
|
+
payment
|
26
|
+
end
|
27
|
+
|
28
|
+
def sales_order?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
# used in build_invoice to determine which type of invoice
|
33
|
+
def to_invoice_class_name
|
34
|
+
"MerchantSidekick::SalesInvoice"
|
35
|
+
end
|
36
|
+
|
37
|
+
def invoice
|
38
|
+
self.sales_invoice
|
39
|
+
end
|
40
|
+
|
41
|
+
def invoice=(an_invoice)
|
42
|
+
self.sales_invoice = an_invoice
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_invoice #:nodoc:
|
46
|
+
new_invoice = self.build_sales_invoice(
|
47
|
+
:line_items => self.line_items,
|
48
|
+
:net_amount => self.net_total,
|
49
|
+
:tax_amount => self.tax_total,
|
50
|
+
:gross_amount => self.gross_total,
|
51
|
+
:buyer => self.buyer,
|
52
|
+
:seller => self.seller,
|
53
|
+
:origin_address => self.origin_address ? self.origin_address.dup : nil,
|
54
|
+
:billing_address => self.billing_address ? self.billing_address.dup : nil,
|
55
|
+
:shipping_address => self.shipping_address ? self.shipping_address.dup : nil
|
56
|
+
)
|
57
|
+
|
58
|
+
# set new invoice's line items to invoice we just created
|
59
|
+
new_invoice.line_items.each do |li|
|
60
|
+
if li.new_record?
|
61
|
+
li.invoice = new_invoice
|
62
|
+
else
|
63
|
+
li.update_attribute(:invoice, new_invoice)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# copy addresses
|
68
|
+
new_invoice.build_origin_address(self.origin_address.content_attributes) if self.origin_address
|
69
|
+
new_invoice.build_billing_address(self.billing_address.content_attributes) if self.billing_address
|
70
|
+
new_invoice.build_shipping_address(self.shipping_address.content_attributes) if self.shipping_address
|
71
|
+
|
72
|
+
self.invoice = new_invoice
|
73
|
+
|
74
|
+
new_invoice
|
75
|
+
end
|
76
|
+
|
77
|
+
# Builds billing, shipping and origin addresses
|
78
|
+
def build_addresses(options={})
|
79
|
+
raise ArgumentError.new("No address declared for buyer (#{buyer.class.name} ##{buyer.id}), use acts_as_addressable") \
|
80
|
+
unless buyer.respond_to?(:find_default_address)
|
81
|
+
|
82
|
+
# buyer's billing address
|
83
|
+
unless self.default_billing_address
|
84
|
+
if buyer.respond_to?(:billing_address) && buyer.default_billing_address
|
85
|
+
self.build_billing_address(buyer.default_billing_address.content_attributes)
|
86
|
+
else
|
87
|
+
if buyer_default_address = buyer.find_default_address
|
88
|
+
self.build_billing_address(buyer_default_address.content_attributes)
|
89
|
+
else
|
90
|
+
raise ArgumentError.new(
|
91
|
+
"No billing or default address found for buyer (#{buyer.class.name} ##{buyer.id}), use acts_as_addressable")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# buyer's shipping address is optional
|
97
|
+
if buyer.respond_to?(:shipping_address)
|
98
|
+
self.build_shipping_address(buyer.find_shipping_address_or_clone_from(
|
99
|
+
self.billing_address
|
100
|
+
).content_attributes) unless self.default_shipping_address
|
101
|
+
end
|
102
|
+
|
103
|
+
# seller's address for origin address
|
104
|
+
raise ArgumentError.new("No address declared for seller (#{seller.class.name} ##{seller.id}), use acts_as_addressable") \
|
105
|
+
unless seller.respond_to?(:find_default_address)
|
106
|
+
|
107
|
+
unless default_origin_address
|
108
|
+
if seller.respond_to?(:billing_address) && seller.default_billing_address
|
109
|
+
self.build_origin_address(seller.default_billing_address.content_attributes)
|
110
|
+
else
|
111
|
+
if seller_default_address = seller.find_default_address
|
112
|
+
self.build_origin_address(seller_default_address.content_attributes)
|
113
|
+
else
|
114
|
+
raise ArgumentError.new(
|
115
|
+
"No billing or default address found for seller (#{seller.class.name} ##{seller.id}), use acts_as_addressable")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module MerchantSidekick
|
2
|
+
module Sellable
|
3
|
+
def self.included(mod)
|
4
|
+
mod.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
# Declares a model as sellable.
|
10
|
+
#
|
11
|
+
# E.g.
|
12
|
+
#
|
13
|
+
# class Product < ActiveRecord::Base
|
14
|
+
# acts_as_sellable :cents => :price_in_cents, :currency => false
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @product.orders.
|
18
|
+
#
|
19
|
+
def acts_as_sellable(options = {})
|
20
|
+
include MerchantSidekick::Sellable::InstanceMethods
|
21
|
+
extend MerchantSidekick::Sellable::SingletonMethods
|
22
|
+
money :price, options
|
23
|
+
has_many :line_items, :as => :sellable, :class_name => "MerchantSidekick::LineItem"
|
24
|
+
has_many :orders, :through => :line_items, :class_name => "MerchantSidekick::Order"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module SingletonMethods
|
29
|
+
def sellable?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module InstanceMethods
|
35
|
+
|
36
|
+
def sellable?
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
# Funny name, but it returns true if the :price represents
|
41
|
+
# a gross price including taxes. For that there must be a
|
42
|
+
# method called price_is_gross or price_is_gross! as it is
|
43
|
+
# in Issue model
|
44
|
+
# price_is_net? and/or price_is_gross? should be overwritten
|
45
|
+
def price_is_gross?
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
# Opposite of price_is_gross?
|
50
|
+
def price_is_net?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
# This is a product, where the gross and net prices are equal, or in other words
|
55
|
+
# a tax for this product is not applicable, e.g. for $10 purchasing credit
|
56
|
+
# should be overwritten if otherwise
|
57
|
+
def taxable?
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
# There can only be one authorized order per sellable,
|
62
|
+
# so the first authorziation is it!
|
63
|
+
#
|
64
|
+
# Usage:
|
65
|
+
# order = issue.settle
|
66
|
+
# order.capture unless order.nil?
|
67
|
+
# Options:
|
68
|
+
# issue.settle :merchant => person
|
69
|
+
#
|
70
|
+
def settle(options={})
|
71
|
+
self.orders.each do |order|
|
72
|
+
if order.kind == 'authorization' && current_line_item = order.line_items_find(
|
73
|
+
:first, :condition => ["sellable_id => ? AND sellable_type = ?", self.id, self.class.name]
|
74
|
+
)
|
75
|
+
if adjusted_line_item=order.line_items.build( :order => order, :sellable => self )
|
76
|
+
current_line_items.destroy
|
77
|
+
order.build_addresses
|
78
|
+
order.update
|
79
|
+
order.save!
|
80
|
+
return order
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module MerchantSidekick
|
2
|
+
module Seller #:nodoc:
|
3
|
+
|
4
|
+
def self.included(mod)
|
5
|
+
mod.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
# Defines helper methods for a person selling items.
|
11
|
+
#
|
12
|
+
# E.g.
|
13
|
+
#
|
14
|
+
# class Merchant
|
15
|
+
# acts_as_seller
|
16
|
+
# ...
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # Selling a product to @customer
|
20
|
+
# @merchant.sell_to @customer, @products
|
21
|
+
#
|
22
|
+
# # Alternative syntax
|
23
|
+
# @merchant.sell @products, :to => @customer
|
24
|
+
#
|
25
|
+
def acts_as_seller(options={})
|
26
|
+
include MerchantSidekick::Seller::InstanceMethods
|
27
|
+
has_many :orders, :as => :seller, :dependent => :destroy, :class_name => "::MerchantSidekick::Order"
|
28
|
+
has_many :invoices, :as => :seller, :dependent => :destroy, :class_name => "::MerchantSidekick::Invoice"
|
29
|
+
has_many :sales_orders, :as => :seller, :class_name => "::MerchantSidekick::SalesOrder"
|
30
|
+
has_many :sales_invoices, :as => :seller, :class_name => "::MerchantSidekick::SalesInvoice"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module InstanceMethods
|
35
|
+
|
36
|
+
def sell_to(buyer, *arguments)
|
37
|
+
sell(arguments, :to => buyer)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sell sellables (line_items) and add them to a sales order
|
41
|
+
# The seller will be this person.
|
42
|
+
#
|
43
|
+
# e.g.
|
44
|
+
#
|
45
|
+
# seller.sell(@product, :buyer => @buyer)
|
46
|
+
#
|
47
|
+
def sell(*arguments)
|
48
|
+
sellables = []
|
49
|
+
options = default_sell_options
|
50
|
+
|
51
|
+
# distinguish between options and attributes
|
52
|
+
arguments = arguments.flatten
|
53
|
+
arguments.each do |argument|
|
54
|
+
case argument.class.name
|
55
|
+
when 'Hash'
|
56
|
+
options.merge! argument
|
57
|
+
else
|
58
|
+
sellables << (argument.is_a?(MerchantSidekick::ShoppingCart::Cart) ? argument.line_items : argument)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
sellables.flatten!
|
62
|
+
sellables.reject! {|s| s.blank?}
|
63
|
+
|
64
|
+
raise ArgumentError.new("No sellable (e.g. product) model provided") if sellables.empty?
|
65
|
+
raise ArgumentError.new("Sellable models must have a :price") unless sellables.all? {|sellable| sellable.respond_to? :price}
|
66
|
+
|
67
|
+
self.sales_orders.build do |so|
|
68
|
+
so.buyer = options[:to]
|
69
|
+
so.build_addresses
|
70
|
+
|
71
|
+
sellables.each do |sellable|
|
72
|
+
if sellable && sellable.respond_to?(:before_add_to_order)
|
73
|
+
sellable.send(:before_add_to_order, self)
|
74
|
+
sellable.reload unless sellable.new_record?
|
75
|
+
end
|
76
|
+
li = LineItem.new(:sellable => sellable, :order => so)
|
77
|
+
so.line_items.push(li)
|
78
|
+
sellable.send(:after_add_to_order, self) if sellable && sellable.respond_to?(:after_add_to_order)
|
79
|
+
end
|
80
|
+
self
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
# override in model, e.g. :to => @customer
|
87
|
+
def default_sell_options
|
88
|
+
{}
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# The Cart class implements a non-persistant shopping cart.
|
2
|
+
# It provides methods to add, remove and update "sellable" items. Each sellable
|
3
|
+
# item added to the cart is converted to a cart line item. It is recommended
|
4
|
+
# to make use of the shopping cart's indirection of purchasing cart line items
|
5
|
+
# as products often change there properties, i.e. price, description, etc., as
|
6
|
+
# shown in the following example:
|
7
|
+
#
|
8
|
+
# Purchase w/ cart (recommended) | Simple purchase wo/ cart
|
9
|
+
# ------------------------------------+------------------------------------
|
10
|
+
# @cart = Cart.new | @order = @buyer.purchase @products
|
11
|
+
# @cart.add @products |
|
12
|
+
# @order = @buyer.purchase @cart |
|
13
|
+
#
|
14
|
+
module MerchantSidekick
|
15
|
+
module ShoppingCart
|
16
|
+
class Cart
|
17
|
+
attr_reader :line_items
|
18
|
+
attr_reader :currency
|
19
|
+
attr_accessor :options
|
20
|
+
|
21
|
+
def initialize(currency_code = 'USD', options = {})
|
22
|
+
@currency = currency_code
|
23
|
+
@options = {:currency_code => currency_code}.merge(options)
|
24
|
+
empty!
|
25
|
+
end
|
26
|
+
|
27
|
+
# Adds a single or array of sellable products to the cart and
|
28
|
+
# returns the cart line items.
|
29
|
+
#
|
30
|
+
# E.g.
|
31
|
+
#
|
32
|
+
# @cart.add @sellable
|
33
|
+
# @cart.add @sellable, 4
|
34
|
+
# @cart.add [@sellable1, @sellable2]
|
35
|
+
#
|
36
|
+
def add(stuff, quantity = 1, options = {})
|
37
|
+
if stuff.is_a?(Array)
|
38
|
+
stuff.inject([]) {|result, element| result << add(element, quantity, options)}
|
39
|
+
elsif stuff.is_a?(MerchantSidekick::ShoppingCart::LineItem)
|
40
|
+
self.add_cart_line_item(stuff, options)
|
41
|
+
else
|
42
|
+
# assuming it is a "product" (e.g. sellable) instance
|
43
|
+
self.add_product(stuff, quantity, options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Removes an item from the cart
|
48
|
+
def remove(stuff, options={})
|
49
|
+
if stuff.is_a?(MerchantSidekick::ShoppingCart::LineItem)
|
50
|
+
self.remove_cart_line_item(stuff, options)
|
51
|
+
else
|
52
|
+
self.remove_product(stuff, options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
alias_method :delete, :remove
|
56
|
+
|
57
|
+
# Updates an existing line_item with quantity by product or line_item
|
58
|
+
# instance. If quanity is <= 0, the item will be removed.
|
59
|
+
def update(stuff, quantity, options={})
|
60
|
+
if stuff.is_a?(MerchantSidekick::ShoppingCart::LineItem)
|
61
|
+
self.update_cart_line_item(stuff, quantity, options)
|
62
|
+
else
|
63
|
+
self.update_product(stuff, quantity, options)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Finds an instance of line item, by product or line_item
|
68
|
+
#
|
69
|
+
# E.g.
|
70
|
+
#
|
71
|
+
# @cart.find(:first, @product) # -> @li
|
72
|
+
# @cart.find(:all, @product) # -> [@li1, @li2]
|
73
|
+
#
|
74
|
+
def find(what, stuff, options={})
|
75
|
+
if stuff.is_a?(MerchantSidekick::ShoppingCart::LineItem)
|
76
|
+
self.find_line_items(what, stuff, options)
|
77
|
+
else
|
78
|
+
self.find_line_items_by_product(what, stuff, options)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Remove all line items from cart
|
83
|
+
def empty!
|
84
|
+
@line_items = []
|
85
|
+
end
|
86
|
+
|
87
|
+
# Check to see if cart is empty?
|
88
|
+
def empty?
|
89
|
+
@line_items.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
# Evaluates the total amount of a sum as all item prices * quantity
|
93
|
+
def total_amount
|
94
|
+
sum = ::Money.new(1, self.currency)
|
95
|
+
@line_items.each {|li| sum += li.total_amount}
|
96
|
+
sum -= ::Money.new(1, self.currency)
|
97
|
+
sum
|
98
|
+
end
|
99
|
+
alias_method :total, :total_amount
|
100
|
+
|
101
|
+
# counts number of line items.
|
102
|
+
def line_items_count
|
103
|
+
self.line_items.size
|
104
|
+
end
|
105
|
+
|
106
|
+
# counts number of entities line_items * quantities.
|
107
|
+
def items_count
|
108
|
+
counter = 0
|
109
|
+
self.line_items.each do |item|
|
110
|
+
counter += item.quantity
|
111
|
+
end
|
112
|
+
counter
|
113
|
+
end
|
114
|
+
|
115
|
+
# Create a product line from a product (sellable) and copies
|
116
|
+
# all attributes that could be modified later
|
117
|
+
def cart_line_item(product, quantity = 1, line_options = {})
|
118
|
+
raise "No price column available for '#{product.class.name}'" unless product.respond_to?(:price)
|
119
|
+
# we need to set currency explicitly here for correct money conversion of the cart_line_item
|
120
|
+
MerchantSidekick::ShoppingCart::LineItem.new do |line_item|
|
121
|
+
line_item.options = self.options.merge(line_options)
|
122
|
+
line_item.currency = self.currency
|
123
|
+
line_item.quantity = quantity
|
124
|
+
line_item.product = product
|
125
|
+
line_item
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Return a list of cart line items from an array of products, e.g. Products
|
130
|
+
def cart_line_items(products)
|
131
|
+
products.map {|p| self.cart_line_item(p)}
|
132
|
+
end
|
133
|
+
|
134
|
+
# cart options setter
|
135
|
+
def options=(some_options = {})
|
136
|
+
@options = some_options.to_hash
|
137
|
+
end
|
138
|
+
|
139
|
+
# cart options getter
|
140
|
+
def options
|
141
|
+
@options || {}
|
142
|
+
end
|
143
|
+
|
144
|
+
protected
|
145
|
+
|
146
|
+
# Add product line
|
147
|
+
# Returns cart total price
|
148
|
+
def add_cart_line_item(newitem, options={})
|
149
|
+
return nil if newitem.nil?
|
150
|
+
item = find(:first, newitem)
|
151
|
+
if item
|
152
|
+
# existing item found, update item quantity and add total_price
|
153
|
+
item.quantity += newitem.quantity
|
154
|
+
else
|
155
|
+
# not in cart yet
|
156
|
+
item = newitem
|
157
|
+
@line_items << item
|
158
|
+
end
|
159
|
+
item
|
160
|
+
end
|
161
|
+
|
162
|
+
# Add purchasable, which most likely will be a product
|
163
|
+
# Returns the total price
|
164
|
+
def add_product(a_product, quantity=1, options={})
|
165
|
+
return nil if a_product.nil?
|
166
|
+
item = find(:first, a_product)
|
167
|
+
if item
|
168
|
+
item.quantity += quantity
|
169
|
+
else
|
170
|
+
item = self.cart_line_item(a_product, quantity, options)
|
171
|
+
@line_items << item
|
172
|
+
end
|
173
|
+
item
|
174
|
+
end
|
175
|
+
|
176
|
+
# Remove a product line and adjust the total price
|
177
|
+
def remove_cart_line_item(a_cart_line_item, options={})
|
178
|
+
deleted_line_item = nil
|
179
|
+
item_to_remove = find(:first, a_cart_line_item)
|
180
|
+
deleted_line_item = @line_items.delete(item_to_remove) if item_to_remove
|
181
|
+
deleted_line_item
|
182
|
+
end
|
183
|
+
|
184
|
+
# Remove a purchasable and adjust the total price
|
185
|
+
def remove_product(a_product, options={})
|
186
|
+
deleted_line_item = nil
|
187
|
+
item_to_remove = find(:first, a_product)
|
188
|
+
deleted_line_item = @line_items.delete(item_to_remove) if item_to_remove
|
189
|
+
deleted_line_item
|
190
|
+
end
|
191
|
+
|
192
|
+
# updates quantity by line item
|
193
|
+
def update_cart_line_item(a_line_item, quantity, options={})
|
194
|
+
return remove(a_line_item, options) if quantity <= 0
|
195
|
+
item = find(:first, a_line_item)
|
196
|
+
item.quantity = quantity if item
|
197
|
+
item
|
198
|
+
end
|
199
|
+
|
200
|
+
# updates quantity py product
|
201
|
+
def update_product(a_product, quantity, options={})
|
202
|
+
return remove(a_product, options) if quantity <= 0
|
203
|
+
item = find(:first, a_product)
|
204
|
+
item.quantity = quantity if item
|
205
|
+
item
|
206
|
+
end
|
207
|
+
|
208
|
+
def find_line_items(what, a_cart_line_item, options={})
|
209
|
+
if :all == what
|
210
|
+
@line_items.select { |i| i.product_id == a_cart_line_item.product.id && i.product_type == a_cart_line_item.product.class.base_class.name }
|
211
|
+
elsif :first == what
|
212
|
+
@line_items.find { |i| i.product_id == a_cart_line_item.product.id && i.product_type == a_cart_line_item.product.class.base_class.name }
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def find_line_items_by_product(what, a_product, options={})
|
217
|
+
if :all == what
|
218
|
+
@line_items.select { |i| i.product_id == a_product.id && i.product_type == a_product.class.base_class.name }
|
219
|
+
elsif :first == what
|
220
|
+
@line_items.find { |i| i.product_id == a_product.id && i.product_type == a_product.class.base_class.name }
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|