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,99 @@
|
|
1
|
+
module MerchantSidekick
|
2
|
+
module Buyer
|
3
|
+
|
4
|
+
def self.included(mod)
|
5
|
+
mod.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
# Defines helper methods for a person buying items.
|
11
|
+
#
|
12
|
+
# E.g.
|
13
|
+
#
|
14
|
+
# class Client < ActiveRecord::Base
|
15
|
+
# acts_as_buyer
|
16
|
+
# ...
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # Simple purchase
|
20
|
+
# # => @client.purchase @products
|
21
|
+
#
|
22
|
+
# # Purchase referencing a seller
|
23
|
+
# # => @client.purchase @products, :from => @merchant
|
24
|
+
#
|
25
|
+
# # Same as above
|
26
|
+
# # => @client.purchase_from @merchant, @products
|
27
|
+
#
|
28
|
+
def acts_as_buyer
|
29
|
+
include MerchantSidekick::Buyer::InstanceMethods
|
30
|
+
has_many :orders, :as => :buyer, :dependent => :destroy, :class_name => "::MerchantSidekick::Order"
|
31
|
+
has_many :invoices, :as => :buyer, :dependent => :destroy, :class_name => "::MerchantSidekick::Invoice"
|
32
|
+
has_many :purchase_orders, :as => :buyer, :class_name => "::MerchantSidekick::PurchaseOrder"
|
33
|
+
has_many :purchase_invoices, :as => :buyer, :class_name => "::MerchantSidekick::PurchaseInvoice"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module InstanceMethods
|
38
|
+
|
39
|
+
# like purchase but forces the seller parameter, instead of
|
40
|
+
# taking it as a :seller option
|
41
|
+
def purchase_from(seller, *arguments)
|
42
|
+
purchase(arguments, :from => seller)
|
43
|
+
end
|
44
|
+
|
45
|
+
# purchase creates a purchase order based on
|
46
|
+
# the given sellables, e.g. product, or basically
|
47
|
+
# anything that has a price attribute.
|
48
|
+
#
|
49
|
+
# E.g.
|
50
|
+
#
|
51
|
+
# buyer.purchase(product, :seller => seller)
|
52
|
+
#
|
53
|
+
def purchase(*arguments)
|
54
|
+
sellables = []
|
55
|
+
options = default_purchase_options
|
56
|
+
|
57
|
+
# distinguish between options and attributes
|
58
|
+
arguments = arguments.flatten
|
59
|
+
arguments.each do |argument|
|
60
|
+
case argument.class.name
|
61
|
+
when 'Hash'
|
62
|
+
options.merge! argument
|
63
|
+
else
|
64
|
+
sellables << (argument.is_a?(MerchantSidekick::ShoppingCart::Cart) ? argument.line_items : argument)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
sellables.flatten!
|
68
|
+
sellables.reject! {|s| s.blank?}
|
69
|
+
|
70
|
+
raise ArgumentError.new("No sellable (e.g. product) model provided") if sellables.empty?
|
71
|
+
raise ArgumentError.new("Sellable models must have a :price") unless sellables.all? {|sellable| sellable.respond_to? :price}
|
72
|
+
|
73
|
+
self.purchase_orders.build do |po|
|
74
|
+
po.buyer = self
|
75
|
+
po.seller = options[:from]
|
76
|
+
po.build_addresses
|
77
|
+
sellables.each do |sellable|
|
78
|
+
if sellable && sellable.respond_to?(:before_add_to_order)
|
79
|
+
sellable.send(:before_add_to_order, self)
|
80
|
+
sellable.reload unless sellable.new_record?
|
81
|
+
end
|
82
|
+
li = LineItem.new(:sellable => sellable, :order => po)
|
83
|
+
po.line_items.push(li)
|
84
|
+
sellable.send(:after_add_to_order, self) if sellable && sellable.respond_to?(:after_add_to_order)
|
85
|
+
end
|
86
|
+
self
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
# override in model, e.g. :from => @merchant
|
93
|
+
def default_purchase_options
|
94
|
+
{}
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# Base class for all merchant sidekick gateway implementations.
|
2
|
+
module MerchantSidekick
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def default_gateway
|
6
|
+
MerchantSidekick::Gateway.default_gateway
|
7
|
+
end
|
8
|
+
|
9
|
+
def default_gateway=(value)
|
10
|
+
MerchantSidekick::Gateway.default_gateway = value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Gateway
|
15
|
+
cattr_accessor :config_path
|
16
|
+
cattr_accessor :config_file_name
|
17
|
+
@@config_file_name = "merchant_sidekick.yml"
|
18
|
+
cattr_accessor :config
|
19
|
+
cattr_accessor :default_gateway # -> sets default gateway as instance or class (symbol) optional
|
20
|
+
cattr_accessor :gateway # -> caches gateway instance in decendants, e.g. in PaypalGateway.gateway
|
21
|
+
|
22
|
+
class << self
|
23
|
+
|
24
|
+
# Returns the gateway type name derived from the class name
|
25
|
+
# independent of the module name, e.g. :authorize_net_gateway
|
26
|
+
def type
|
27
|
+
name.split("::").last ? name.split("::").last.underscore.to_sym : name.underscore.to_sym
|
28
|
+
end
|
29
|
+
|
30
|
+
def config_path(file_name = nil)
|
31
|
+
unless @@config_path
|
32
|
+
@@config_path = "#{Rails.root}/config/#{file_name || config_file_name}"
|
33
|
+
end
|
34
|
+
@@config_path
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns configuration hash. By default the configuration is read from
|
38
|
+
# a YAML file from the Rails config/merchant_sidekick.yml path.
|
39
|
+
#
|
40
|
+
# E.g.
|
41
|
+
#
|
42
|
+
# # config/merchant_sidekick.yml
|
43
|
+
# development:
|
44
|
+
# login_id: foo
|
45
|
+
# transaction_key: bar
|
46
|
+
# mode: test
|
47
|
+
# production:
|
48
|
+
# ...
|
49
|
+
#
|
50
|
+
# or
|
51
|
+
#
|
52
|
+
# # config/merchant_sidekick.yml
|
53
|
+
# development:
|
54
|
+
# authorize_net_gateway:
|
55
|
+
# login_id: foo
|
56
|
+
# transaction_key: bar
|
57
|
+
# mode: test
|
58
|
+
# paypal_gateway:
|
59
|
+
# api_username: seller_XYZ_biz_api1.example.com
|
60
|
+
# api_password: ABCDEFG123456789
|
61
|
+
# signature: AsPC9BjkCyDFQXbStoZcgqH3hpacAX3IenGazd35.nEnXJKR9nfCmJDu
|
62
|
+
# pem_file_name: config/paypal.pem
|
63
|
+
# mode: test
|
64
|
+
# production:
|
65
|
+
# ...
|
66
|
+
#
|
67
|
+
def config
|
68
|
+
unless @@config
|
69
|
+
@@config = YAML.load_file(config_path)[Rails.env].symbolize_keys
|
70
|
+
@@config = @@config[type].symbolize_keys if @@config[type]
|
71
|
+
end
|
72
|
+
@@config
|
73
|
+
end
|
74
|
+
|
75
|
+
def default_gateway
|
76
|
+
@@default_gateway || raise("No gateway instance assigned, try e.g. MerchantSidekick::Gateway.default_gateway = ActiveMerchant::Billing::BogusGateway.new")
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
|
4
|
+
module MerchantSidekick
|
5
|
+
class Install < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
extend ActiveRecord::Generators::Migration
|
8
|
+
|
9
|
+
source_root File.expand_path('../migrations', __FILE__)
|
10
|
+
|
11
|
+
# Copies the migration template to db/migrate.
|
12
|
+
def copy_files(*args)
|
13
|
+
migration_template 'billing.rb', 'db/migrate/create_merchant_sidekick_billing_tables.rb'
|
14
|
+
migration_template 'shopping_cart.rb', 'db/migrate/create_merchant_sidekick_shopping_cart_tables.rb'
|
15
|
+
migration_template 'addressable.rb', 'db/migrate/create_merchant_sidekick_addressable_tables.rb'
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# Baseclass for in- and outbound invoices.
|
2
|
+
module MerchantSidekick
|
3
|
+
class Invoice < ActiveRecord::Base
|
4
|
+
include AASM
|
5
|
+
# include ActionView::Helpers::TextHelper
|
6
|
+
self.table_name = "invoices"
|
7
|
+
|
8
|
+
attr_accessor :authorization
|
9
|
+
|
10
|
+
belongs_to :seller, :polymorphic => true
|
11
|
+
belongs_to :buyer, :polymorphic => true
|
12
|
+
has_many :line_items, :class_name => "MerchantSidekick::LineItem"
|
13
|
+
belongs_to :order, :class_name => "MerchantSidekick::Order"
|
14
|
+
has_many :payments, :as => :payable, :dependent => :destroy, :class_name => "MerchantSidekick::Payment"
|
15
|
+
|
16
|
+
money :net_amount, :cents => :net_cents, :currency => :currency
|
17
|
+
money :tax_amount, :cents => :tax_cents, :currency => :currency
|
18
|
+
money :gross_amount, :cents => :gross_cents, :currency => :currency
|
19
|
+
has_address :origin, :billing, :shipping
|
20
|
+
|
21
|
+
#--- state machine
|
22
|
+
aasm :column => "status" do
|
23
|
+
state :pending, :enter => :enter_pending, :exit => :exit_pending, :initial => true
|
24
|
+
state :authorized, :enter => :enter_authorized, :exit => :exit_authorized
|
25
|
+
state :paid, :enter => :enter_paid, :exit => :exit_paid
|
26
|
+
state :voided, :enter => :enter_voided, :exit => :exit_voided
|
27
|
+
state :refunded, :enter => :enter_refunded, :exit => :exit_refunded
|
28
|
+
state :payment_declined, :enter => :enter_payment_declined, :exit => :exit_payment_declined
|
29
|
+
|
30
|
+
event :payment_paid do
|
31
|
+
transitions :from => :pending, :to => :paid, :guard => :guard_payment_paid_from_pending
|
32
|
+
end
|
33
|
+
|
34
|
+
event :payment_authorized do
|
35
|
+
transitions :from => :pending, :to => :authorized, :guard => :guard_payment_authorized_from_pending
|
36
|
+
transitions :from => :payment_declined, :to => :authorized, :guard => :guard_payment_authorized_from_payment_declined
|
37
|
+
end
|
38
|
+
|
39
|
+
event :payment_captured do
|
40
|
+
transitions :from => :authorized, :to => :paid, :guard => :guard_payment_captured_from_authorized
|
41
|
+
end
|
42
|
+
|
43
|
+
event :payment_voided do
|
44
|
+
transitions :from => :authorized, :to => :voided, :guard => :guard_payment_voided_from_authorized
|
45
|
+
end
|
46
|
+
|
47
|
+
event :payment_refunded do
|
48
|
+
transitions :from => :paid, :to => :refunded, :guard => :guard_payment_refunded_from_paid
|
49
|
+
end
|
50
|
+
|
51
|
+
event :transaction_declined do
|
52
|
+
transitions :from => :pending, :to => :payment_declined, :guard => :guard_transaction_declined_from_pending
|
53
|
+
transitions :from => :payment_declined, :to => :payment_declined, :guard => :guard_transaction_declined_from_payment_declined
|
54
|
+
transitions :from => :authorized, :to => :authorized, :guard => :guard_transaction_declined_from_authorized
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# state transition callbacks
|
59
|
+
def enter_pending; end
|
60
|
+
def enter_authorized; end
|
61
|
+
def enter_paid; end
|
62
|
+
def enter_voided; end
|
63
|
+
def enter_refunded; end
|
64
|
+
def enter_payment_declined; end
|
65
|
+
|
66
|
+
def exit_pending; end
|
67
|
+
def exit_authorized; end
|
68
|
+
def exit_paid; end
|
69
|
+
def exit_voided; end
|
70
|
+
def exit_refunded; end
|
71
|
+
def exit_payment_declined; end
|
72
|
+
|
73
|
+
# event guard callbacks
|
74
|
+
def guard_transaction_declined_from_authorized; true; end
|
75
|
+
def guard_transaction_declined_from_payment_declined; true; end
|
76
|
+
def guard_transaction_declined_from_pending; true; end
|
77
|
+
def guard_payment_refunded_from_paid; true; end
|
78
|
+
def guard_payment_voided_from_authorized; true; end
|
79
|
+
def guard_payment_captured_from_authorized; true; end
|
80
|
+
def guard_payment_authorized_from_payment_declined; true; end
|
81
|
+
def guard_payment_authorized_from_pending; true; end
|
82
|
+
def guard_payment_paid_from_pending; true; end
|
83
|
+
|
84
|
+
#--- scopes
|
85
|
+
scope :paid, :conditions => {:status => "paid"}
|
86
|
+
|
87
|
+
#--- callbacks
|
88
|
+
before_save :number
|
89
|
+
|
90
|
+
#--- instance methods
|
91
|
+
alias_method :current_state, :aasm_current_state
|
92
|
+
|
93
|
+
def number
|
94
|
+
self[:number] ||= Order.generate_unique_id
|
95
|
+
end
|
96
|
+
|
97
|
+
# returns a hash of additional merchant data passed to authorize
|
98
|
+
# you want to pass in the following additional options
|
99
|
+
#
|
100
|
+
# :ip => ip address of the buyer
|
101
|
+
#
|
102
|
+
def payment_options(options={})
|
103
|
+
{}.merge(options)
|
104
|
+
end
|
105
|
+
|
106
|
+
# From payments, returns :credit_card, etc.
|
107
|
+
def payment_type
|
108
|
+
payments.first.payment_type if payments
|
109
|
+
end
|
110
|
+
alias_method :payment_method, :payment_type
|
111
|
+
|
112
|
+
# Human readable payment type
|
113
|
+
def payment_type_display
|
114
|
+
self.payment_type.to_s.titleize
|
115
|
+
end
|
116
|
+
alias_method :payment_method_display, :payment_type_display
|
117
|
+
|
118
|
+
# Net total amount
|
119
|
+
def net_total
|
120
|
+
self.net_amount ||= line_items.inject(::Money.new(0, self.currency || ::Money.default_currency.iso_code)) {|sum,line| sum + line.net_amount}
|
121
|
+
end
|
122
|
+
|
123
|
+
# Calculates tax and sets the tax_amount attribute
|
124
|
+
# It adds tax_amount across all line_items
|
125
|
+
def tax_total
|
126
|
+
self.tax_amount = line_items.inject(::Money.new(0, self.currency || ::Money.default_currency.iso_code)) {|sum,line| sum + line.tax_amount}
|
127
|
+
self.tax_amount
|
128
|
+
end
|
129
|
+
|
130
|
+
# Gross amount including tax
|
131
|
+
def gross_total
|
132
|
+
self.gross_amount ||= self.net_total + self.tax_total
|
133
|
+
end
|
134
|
+
|
135
|
+
# Same as gross_total
|
136
|
+
def total
|
137
|
+
self.gross_total
|
138
|
+
end
|
139
|
+
|
140
|
+
# updates the order and all contained line_items after an address has changed
|
141
|
+
# or an order item was added or removed. The order can only be evaluated if the
|
142
|
+
# created state is active. The order is saved if it is an existing order.
|
143
|
+
# Returns true if evaluation happend, false if not.
|
144
|
+
def evaluate
|
145
|
+
result = false
|
146
|
+
self.line_items.each(&:evaluate)
|
147
|
+
self.calculate
|
148
|
+
result = save(false) unless self.new_record?
|
149
|
+
result
|
150
|
+
end
|
151
|
+
|
152
|
+
protected
|
153
|
+
|
154
|
+
# override in subclass
|
155
|
+
def purchase_invoice?
|
156
|
+
false
|
157
|
+
end
|
158
|
+
|
159
|
+
# marks sales invoice, override in subclass
|
160
|
+
def sales_invoice?
|
161
|
+
false
|
162
|
+
end
|
163
|
+
|
164
|
+
def push_payment(a_payment)
|
165
|
+
a_payment.payable = self
|
166
|
+
self.payments.push(a_payment)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Recalculates the order, adding order lines, tax and gross totals
|
170
|
+
def calculate
|
171
|
+
=begin
|
172
|
+
self.net_amount = nil
|
173
|
+
self.tax_amount = nil
|
174
|
+
self.gross_amount = nil
|
175
|
+
=end
|
176
|
+
self.total
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# Line Items used in orders and invoices
|
2
|
+
#
|
3
|
+
# LineItem::tax_rate_class_name = 'TaxRate'
|
4
|
+
#
|
5
|
+
module MerchantSidekick
|
6
|
+
class LineItem < ActiveRecord::Base
|
7
|
+
self.table_name = "line_items"
|
8
|
+
|
9
|
+
#--- accessors
|
10
|
+
cattr_accessor :tax_rate_class_name
|
11
|
+
|
12
|
+
#--- associations
|
13
|
+
belongs_to :order, :class_name => "::MerchantSidekick::Order"
|
14
|
+
belongs_to :invoice, :class_name => "::MerchantSidekick::Invoice"
|
15
|
+
belongs_to :sellable, :polymorphic => true
|
16
|
+
|
17
|
+
#--- mixins
|
18
|
+
money :net_amount, :cents => :net_cents, :currency => "currency"
|
19
|
+
money :tax_amount, :cents => :tax_cents, :currency => "currency"
|
20
|
+
money :gross_amount, :cents => :gross_cents, :currency => "currency"
|
21
|
+
|
22
|
+
#--- callbacks
|
23
|
+
before_save :save_sellable
|
24
|
+
|
25
|
+
#--- instance methods
|
26
|
+
|
27
|
+
# set #amount when adding sellable. This method is aliased to <tt>sellable=</tt>.
|
28
|
+
def sellable_with_price=(a_sellable)
|
29
|
+
calculate(a_sellable)
|
30
|
+
self.sellable_without_price = a_sellable
|
31
|
+
end
|
32
|
+
alias_method_chain :sellable=, :price
|
33
|
+
|
34
|
+
# There used to be a money :amount declration. When we added
|
35
|
+
# taxable line items, we assume that amount will refer to the
|
36
|
+
# net_amount (net_cents)
|
37
|
+
def amount
|
38
|
+
self.net_amount
|
39
|
+
end
|
40
|
+
alias_method :net_total, :amount
|
41
|
+
|
42
|
+
# Amount amounts to the net, for compatibility reasons
|
43
|
+
def amount=(net_money_amount)
|
44
|
+
self.net_amount = net_money_amount
|
45
|
+
end
|
46
|
+
alias_method :net_total=, :amount=
|
47
|
+
|
48
|
+
# shorter for gross_amount
|
49
|
+
def total
|
50
|
+
self.gross_amount
|
51
|
+
end
|
52
|
+
alias_method :gross_total, :total
|
53
|
+
|
54
|
+
# short for tax_amount
|
55
|
+
def tax
|
56
|
+
self.tax_amount
|
57
|
+
end
|
58
|
+
alias_method :tax_total, :tax
|
59
|
+
|
60
|
+
# calculates the amounts, like after an address change in the order and tries to save
|
61
|
+
# the line_item unless locked
|
62
|
+
# TODO find a better way to determine if the line item can still be updated
|
63
|
+
def evaluate
|
64
|
+
calculate(self.sellable)
|
65
|
+
save(false) unless new_record?
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
def calculate(a_sellable)
|
71
|
+
if a_sellable && a_sellable.price
|
72
|
+
tax_rate_class = tax_rate_class_name.camelize.constantize rescue nil
|
73
|
+
|
74
|
+
# calculate tax amounts
|
75
|
+
#
|
76
|
+
# If we want to provide tax rates based on the order's billing address location,
|
77
|
+
# we require a class method,
|
78
|
+
#
|
79
|
+
# e.g.
|
80
|
+
#
|
81
|
+
# Tax.find_tax_rate({:origin => {...}, :destination => {...}})
|
82
|
+
# # where each hash provides :country_code => 'DE', :state_code => 'BY'
|
83
|
+
|
84
|
+
if tax_rate_class && a_sellable.respond_to?(:taxable?) && a_sellable.send(:taxable?)
|
85
|
+
# find tax rate for billing address, country/province
|
86
|
+
self.tax_rate = tax_rate_class.find_tax_rate(
|
87
|
+
:origin => order && order.origin_address ? order.origin_address.content_attributes : {},
|
88
|
+
:destination => order && order.shipping_address ? order.shipping_address.content_attributes : {},
|
89
|
+
:sellable => a_sellable
|
90
|
+
)
|
91
|
+
|
92
|
+
if a_sellable.respond_to?( :price_is_net? ) && a_sellable.send( :price_is_net? )
|
93
|
+
# gross = net + tax
|
94
|
+
self.net_amount = a_sellable.price
|
95
|
+
this_cents, this_currency = self.net_amount.cents, self.net_amount.currency
|
96
|
+
this_tax_cents = ( Float( this_cents * self.tax_rate / 10) / 10 ).round
|
97
|
+
self.tax_amount = ::Money.new(this_tax_cents, this_currency)
|
98
|
+
self.gross_amount = self.net_amount + self.tax_amount
|
99
|
+
else # price is gross
|
100
|
+
# net = gross - tax
|
101
|
+
this_gross_cents, this_currency = a_sellable.price.cents, a_sellable.price.currency
|
102
|
+
this_net_cents = (this_gross_cents * Float(100) / Float(100 + self.tax_rate)).round
|
103
|
+
self.net_amount = ::Money.new(this_net_cents, this_currency)
|
104
|
+
self.gross_amount = a_sellable.price
|
105
|
+
self.tax_amount = self.gross_amount - self.net_amount
|
106
|
+
end
|
107
|
+
else
|
108
|
+
# net = gross, tax = 0
|
109
|
+
self.net_amount = a_sellable.price
|
110
|
+
self.gross_amount = a_sellable.price
|
111
|
+
self.tax_amount = ::Money.new(0, a_sellable.currency)
|
112
|
+
self.tax_rate = 0
|
113
|
+
end
|
114
|
+
else
|
115
|
+
self.net_cents = 0
|
116
|
+
self.gross_cents = 0
|
117
|
+
self.tax_cents = 0
|
118
|
+
self.tax_rate = 0
|
119
|
+
self.currency = "USD"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def save_sellable
|
124
|
+
sellable.save if sellable && sellable.new_record?
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|