spree_exactor 1.1.1.112920126
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/Gemfile.lock +231 -0
- data/README.md +21 -0
- data/app/assets/javascripts/admin/exactor_settings.js +2 -0
- data/app/assets/javascripts/admin/spree_exactor.js +1 -0
- data/app/assets/javascripts/store/spree_exactor.js +1 -0
- data/app/assets/stylesheets/admin/spree_exactor.css +3 -0
- data/app/assets/stylesheets/exactor_settings.css +4 -0
- data/app/assets/stylesheets/store/spree_exactor.css +3 -0
- data/app/controllers/spree/admin/exactor_settings_controller.rb +34 -0
- data/app/helpers/exactor_api_objects.rb +387 -0
- data/app/helpers/exactor_api_service.rb +96 -0
- data/app/helpers/spree_exactor_connector.rb +380 -0
- data/app/models/spree/adjustment_decorator.rb +41 -0
- data/app/models/spree/calculator/exactor_tax_calculator.rb +87 -0
- data/app/models/spree/exactor_setting.rb +140 -0
- data/app/models/spree/line_item_decorator.rb +31 -0
- data/app/models/spree/order_decorator.rb +126 -0
- data/app/models/spree/payment_decorator.rb +15 -0
- data/app/models/spree/tax_category_decorator.rb +31 -0
- data/app/models/spree/tax_rate_decorator.rb +46 -0
- data/app/models/spree/user_decorator.rb +7 -0
- data/app/models/spree/zone_decorator.rb +31 -0
- data/app/overrides/spree_exactor_overrides.rb +27 -0
- data/app/views/spree/admin/exactor_settings/show.html.erb +113 -0
- data/config/locales/en.yml +10 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20120624223025_create_exactor_settings.rb +20 -0
- data/db/migrate/20120627130223_add_exactor_info_to_order.rb +8 -0
- data/db/migrate/20120705103207_add_exactor_indicator_to_tax_rates.rb +5 -0
- data/db/migrate/20120713152512_modify_dependent_entities.rb +6 -0
- data/db/migrate/20120813124740_extend_user.rb +5 -0
- data/db/migrate/20120815104739_add_exactor_tor_inline_tax.rb +5 -0
- data/lib/generators/spree_exactor/install/install_generator.rb +80 -0
- data/lib/generators/spree_exactor/uninstall/uninstall_generator.rb +24 -0
- data/lib/spree_exactor/engine.rb +25 -0
- data/lib/spree_exactor.rb +2 -0
- data/spree_exactor.gemspec +66 -0
- metadata +197 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
module Spree
|
2
|
+
class Adjustment
|
3
|
+
alias :super_update! :update!
|
4
|
+
|
5
|
+
#Spree sometimes lock adjustments. We will make to calculate Exactor Tax anyway!
|
6
|
+
def update!(src = nil)
|
7
|
+
if originator.present? and originator.is_a?(TaxRate) and originator.is_exactor_default?
|
8
|
+
self.locked = false
|
9
|
+
self.label = "Sales Tax"
|
10
|
+
self.update_attribute_without_callbacks(:label, label)
|
11
|
+
end
|
12
|
+
super_update!(src)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
Adjustment.class_eval do
|
18
|
+
after_save :on_save
|
19
|
+
after_destroy :on_destroy
|
20
|
+
|
21
|
+
def on_save
|
22
|
+
fire_update()
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_destroy
|
26
|
+
fire_update()
|
27
|
+
end
|
28
|
+
|
29
|
+
def is_exactor_taxable?
|
30
|
+
return exactor_inline_tax!=0
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def fire_update
|
35
|
+
if adjustable.completed?
|
36
|
+
adjustable.exactor_is_edit="edit_lineitem"
|
37
|
+
end
|
38
|
+
adjustable.on_update()
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'spree_exactor_connector'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
class Spree::Calculator::ExactorTaxCalculator < Spree::Calculator
|
5
|
+
|
6
|
+
def self.description
|
7
|
+
"Exactor sales tax compliance provider"
|
8
|
+
end
|
9
|
+
|
10
|
+
def calculate_hash_from_address(address)
|
11
|
+
address_line1 = "#{address.address1.to_s.gsub(/\s+/, "")}|#{address.address2.to_s.gsub(/\s+/, "")}|#{address.city.to_s.strip}"
|
12
|
+
address_line2= "#{address.state.to_s}|#{address.state_name.to_s.strip()}|#{address.zipcode}|#{address.country.to_s.strip}"
|
13
|
+
return "#{address_line1}|#{address_line2}".downcase()
|
14
|
+
end
|
15
|
+
|
16
|
+
def calculate_hash_from_items(items)
|
17
|
+
result = ""
|
18
|
+
items.each { |item| result+="|#{item.quantity}|#{item.price}|" }
|
19
|
+
return result
|
20
|
+
end
|
21
|
+
|
22
|
+
def calc_adjustment_amount(computable)
|
23
|
+
amount=0
|
24
|
+
computable.adjustments.each do |adjustment|
|
25
|
+
if ! adjustment.originator.present? or (adjustment.originator.present? and (!adjustment.originator.is_a?(Spree::TaxRate)))
|
26
|
+
if adjustment.eligible?
|
27
|
+
amount+=adjustment.amount
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
return amount
|
32
|
+
end
|
33
|
+
|
34
|
+
def calculate_hash(order)
|
35
|
+
addresses = "#{calculate_hash_from_address(order.ship_address)}|#{calculate_hash_from_address(order.bill_address)}"
|
36
|
+
amounts = "#{calculate_hash_from_items(order.line_items)}|#{order.ship_total}|#{calc_adjustment_amount(order)}"
|
37
|
+
return Digest::MD5.hexdigest("#{addresses}|#{amounts}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def is_needed_to_calculate_tax?(computable)
|
41
|
+
order_hash = calculate_hash(computable)
|
42
|
+
current_order_hash = Spree::Order.find(computable.id).exactor_order_hash_sum
|
43
|
+
result = order_hash!=current_order_hash
|
44
|
+
if result
|
45
|
+
computable.update_column(:exactor_order_hash_sum, order_hash)
|
46
|
+
end
|
47
|
+
return result
|
48
|
+
end
|
49
|
+
|
50
|
+
def update_line_items_with_tax_info(computable, response)
|
51
|
+
computable.line_items.each do |line_item|
|
52
|
+
line_item.update_attribute_without_callbacks(:exactor_inline_tax, response.get_line_item_tax("_#{line_item.id}"))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def compute(computable=nil)
|
57
|
+
if computable
|
58
|
+
settings = Spree::ExactorSetting.get_settings
|
59
|
+
if settings and (Date.current < settings.effective_date)
|
60
|
+
return 0
|
61
|
+
end
|
62
|
+
if (!computable.line_items.empty? and !computable.shipping_method.nil?) or computable.completed?
|
63
|
+
#Workaroud of double request to Exactor: without shipping cost and with included shipping cost.
|
64
|
+
#we need to return at least any non-zero value in order to have tax adjustment created. When we will have shipment
|
65
|
+
#we will send real transaction to Exactor
|
66
|
+
if computable.adjustments.shipping.empty? and !computable.completed?
|
67
|
+
return 0.001
|
68
|
+
end
|
69
|
+
exactor = SpreeExactorConnector::SpreeExactorConnector.new
|
70
|
+
if is_needed_to_calculate_tax?(computable)
|
71
|
+
response = exactor.calculate_tax_for_order(computable)
|
72
|
+
if response.is_valid
|
73
|
+
computable.update_column(:exactor_invoice_transaction, response.invoice_id)
|
74
|
+
update_line_items_with_tax_info(computable, response)
|
75
|
+
return response.total_tax
|
76
|
+
else
|
77
|
+
computable.update_column(:exactor_order_hash_sum, nil)
|
78
|
+
computable.errors.add(:base, "Unable to calculate tax: #{response.error_code}:#{response.error_description}")
|
79
|
+
return 0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
return computable.tax_total
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'exactor_api_objects'
|
2
|
+
|
3
|
+
module Spree
|
4
|
+
class ExactorSettingsValidator < ActiveModel::Validator
|
5
|
+
|
6
|
+
def validate(record)
|
7
|
+
unless record.errors.empty?
|
8
|
+
return
|
9
|
+
end
|
10
|
+
connector = SpreeExactorConnector::SpreeExactorConnector.new
|
11
|
+
response = connector.send_test_transaction(record.get_ship_from, record.merchant_id, record.user_id)
|
12
|
+
if (response.nil?)
|
13
|
+
record.errors[:base] << "We are unable to communicate with Exactor"
|
14
|
+
elsif response.has_key? 'ErrorResponse'
|
15
|
+
record.errors[:base] << "Invalid Exactor Settings: #{response['ErrorResponse'][0]['ErrorDescription'][0]}. (Code #{response['ErrorResponse'][0]['ErrorCode'][0]}) "
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module Spree
|
25
|
+
|
26
|
+
class ExactorOption
|
27
|
+
attr_accessor :id
|
28
|
+
attr_accessor :name
|
29
|
+
|
30
|
+
def initialize (id, name)
|
31
|
+
self.id = id
|
32
|
+
self.name = name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class ExactorSetting < ActiveRecord::Base
|
37
|
+
|
38
|
+
class COMMIT_OPTIONS
|
39
|
+
TRANSACTION_STATE ="trstate"
|
40
|
+
PAYMENT_STATE = "paystate"
|
41
|
+
SHIPMENT_STATE ="shistate"
|
42
|
+
|
43
|
+
def self.to_list
|
44
|
+
return [TRANSACTION_STATE, PAYMENT_STATE, SHIPMENT_STATE]
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
class SKU_SOURCES
|
51
|
+
SKU_FIELD ="prodsku"
|
52
|
+
TAX_CATEGORY = "prodcategory"
|
53
|
+
|
54
|
+
def self.to_list
|
55
|
+
return [SKU_FIELD, TAX_CATEGORY]
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
belongs_to :country, :foreign_key=>"country_id"
|
61
|
+
belongs_to :state, :foreign_key=>"state_id"
|
62
|
+
|
63
|
+
|
64
|
+
attr_accessible :merchant_id, :user_id, :effective_date, :sku_source, :commit_option, :shipping_include_handling
|
65
|
+
attr_accessible :ship_from_name, :ship_from_street1, :ship_from_street2,
|
66
|
+
:ship_from_city, :ship_from_zip, :country, :state, :country_id, :state_id, :state_name
|
67
|
+
|
68
|
+
after_create :create_additional_entities
|
69
|
+
after_update :create_additional_entities
|
70
|
+
|
71
|
+
before_validation :set_states
|
72
|
+
|
73
|
+
validate :is_valid_state?
|
74
|
+
validates_presence_of :user_id
|
75
|
+
validates_format_of :merchant_id, :with=> /^\d{8}$/, :message => "must consist of 8 digits"
|
76
|
+
validates_size_of :ship_from_street1, :ship_from_city, :within=>2..100
|
77
|
+
validates_format_of :ship_from_zip, :with => /^\d{5}(-?\d{4})?$/, :message =>"has invalid format"
|
78
|
+
validates_inclusion_of :sku_source, :in=> SKU_SOURCES.to_list()
|
79
|
+
validates_inclusion_of :commit_option, :in=> COMMIT_OPTIONS.to_list()
|
80
|
+
|
81
|
+
validates_with ExactorSettingsValidator
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
def is_valid_state?
|
87
|
+
if state_id.nil? and state_name.to_s.empty?
|
88
|
+
errors[:state] << "is required field"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
#called after create
|
93
|
+
def create_additional_entities
|
94
|
+
TaxRate.find_or_create_exactor_tax_rate()
|
95
|
+
end
|
96
|
+
|
97
|
+
#called before validation
|
98
|
+
def set_states
|
99
|
+
unless country.states.include?(state) or state.nil?
|
100
|
+
@attributes["state_id"] = nil
|
101
|
+
else
|
102
|
+
@attributes["state_name"] = nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def all_commit_options
|
107
|
+
return [
|
108
|
+
ExactorOption.new(COMMIT_OPTIONS::TRANSACTION_STATE, "Commit based on transaction state"),
|
109
|
+
ExactorOption.new(COMMIT_OPTIONS::PAYMENT_STATE, "Commit based on payment state"),
|
110
|
+
ExactorOption.new(COMMIT_OPTIONS::SHIPMENT_STATE, "Commit based on shipping state")
|
111
|
+
]
|
112
|
+
end
|
113
|
+
|
114
|
+
def all_sku_sources
|
115
|
+
return [
|
116
|
+
ExactorOption.new(SKU_SOURCES::SKU_FIELD, "Use SKU field for SKU"),
|
117
|
+
ExactorOption.new(SKU_SOURCES::TAX_CATEGORY, "Use product tax category for SKU")
|
118
|
+
]
|
119
|
+
end
|
120
|
+
|
121
|
+
def get_ship_from
|
122
|
+
companyAddress = AddressType.new
|
123
|
+
companyAddress.FullName= ship_from_name
|
124
|
+
companyAddress.Street1= ship_from_street1
|
125
|
+
companyAddress.Street2= ship_from_street2
|
126
|
+
companyAddress.City= ship_from_city
|
127
|
+
companyAddress.StateOrProvince= state_id.nil?() ? state_name : state
|
128
|
+
companyAddress.PostalCode= ship_from_zip
|
129
|
+
companyAddress.Country= country
|
130
|
+
return companyAddress
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
def self.get_settings
|
135
|
+
settings = Spree::ExactorSetting.all
|
136
|
+
return settings.empty?() ? nil:settings.first()
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Spree
|
2
|
+
LineItem.class_eval do
|
3
|
+
attr_accessible :exactor_inline_tax
|
4
|
+
|
5
|
+
after_save :on_save
|
6
|
+
after_destroy :on_destroy
|
7
|
+
#before save
|
8
|
+
|
9
|
+
def on_save
|
10
|
+
fire_update()
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_destroy
|
14
|
+
fire_update()
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_exactor_taxable?
|
18
|
+
return exactor_inline_tax!=0
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def fire_update
|
23
|
+
if order.completed?
|
24
|
+
order.exactor_is_edit="edit_lineitem"
|
25
|
+
end
|
26
|
+
order.on_update()
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Spree
|
2
|
+
class Order
|
3
|
+
alias :super_finalize! :finalize!
|
4
|
+
alias :super_after_cancel :after_cancel
|
5
|
+
alias :super_after_resume :after_resume
|
6
|
+
alias :super_update_adjustments :update_adjustments
|
7
|
+
alias :super_update_payment_state :update_payment_state
|
8
|
+
alias :super_update_shipment_state :update_shipment_state
|
9
|
+
|
10
|
+
#check out handler
|
11
|
+
def finalize!
|
12
|
+
exactor_connector = SpreeExactorConnector::SpreeExactorConnector.new
|
13
|
+
exactor_connector.do_checkout!(self)
|
14
|
+
super_finalize!
|
15
|
+
end
|
16
|
+
|
17
|
+
#cancel order handler
|
18
|
+
def after_cancel
|
19
|
+
exactor_connector = SpreeExactorConnector::SpreeExactorConnector.new
|
20
|
+
exactor_connector.do_cancel!(self)
|
21
|
+
update_column(:exactor_order_hash_sum,nil)
|
22
|
+
super_after_cancel
|
23
|
+
end
|
24
|
+
|
25
|
+
#when order was resumed handler
|
26
|
+
def after_resume
|
27
|
+
update_adjustments
|
28
|
+
exactor_connector = SpreeExactorConnector::SpreeExactorConnector.new
|
29
|
+
exactor_connector.do_checkout!(self)
|
30
|
+
super_after_resume
|
31
|
+
end
|
32
|
+
|
33
|
+
def edit_order
|
34
|
+
exactor_connector = SpreeExactorConnector::SpreeExactorConnector.new
|
35
|
+
if (!exactor_invoice_transaction.nil?)
|
36
|
+
if (!exactor_commit_transaction.nil?)
|
37
|
+
exactor_connector.do_cancel!(self)
|
38
|
+
end
|
39
|
+
exactor_connector.do_checkout!(self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def update_payment_state
|
44
|
+
super_update_payment_state
|
45
|
+
exactor_connector = SpreeExactorConnector::SpreeExactorConnector.new
|
46
|
+
if (payment_state=='paid')
|
47
|
+
exactor_connector.do_payment_completed!(self)
|
48
|
+
elsif current_payment and current_payment.state == 'void' and current_payment.previous_state == 'completed'
|
49
|
+
exactor_connector.do_refund_payment!(self)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def update_shipment_state
|
54
|
+
super_update_shipment_state
|
55
|
+
if (shipment_state=='shipped')
|
56
|
+
exactor_connector = SpreeExactorConnector::SpreeExactorConnector.new
|
57
|
+
exactor_connector.do_shipp
|
58
|
+
ed!(self)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_tax_charge!
|
63
|
+
# destroy any previous adjustments (eveything is recalculated from scratch)
|
64
|
+
price_adjustments.each(&:destroy)
|
65
|
+
if (adjustments.tax.reload.size!=1)
|
66
|
+
adjustments.tax.each(&:destroy)
|
67
|
+
TaxRate.match(self).each { |rate| rate.adjust(self) }
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
def is_needed_to_create_adjustment?
|
72
|
+
settings = Spree::ExactorSetting.get_settings
|
73
|
+
if self.completed? and ['complete', 'canceled', 'resumed'].include?(state)
|
74
|
+
if settings and completed_at >= settings.effective_date
|
75
|
+
return true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
|
81
|
+
#this method is intended to add adjustment to old transactions which was created without our plugin
|
82
|
+
def update_adjustments
|
83
|
+
if !is_needed_to_create_adjustment?
|
84
|
+
super_update_adjustments
|
85
|
+
return
|
86
|
+
end
|
87
|
+
count_of_tax_adj = adjustments.reload.tax.size
|
88
|
+
current_tax_adj = adjustments.reload.tax.first
|
89
|
+
unless count_of_tax_adj == 0 or
|
90
|
+
(count_of_tax_adj==1 and !current_tax_adj.originator.nil? and current_tax_adj.originator.is_exactor_default?)
|
91
|
+
adjustments.tax.each(&:destroy)
|
92
|
+
return
|
93
|
+
end
|
94
|
+
if (count_of_tax_adj == 0)
|
95
|
+
TaxRate.match(self).each { |rate| rate.adjust(self) }
|
96
|
+
end
|
97
|
+
super_update_adjustments
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
Order.class_eval do
|
102
|
+
attr_accessible :exactor_invoice_transaction, :exactor_commit_transaction, :exactor_order_hash_sum, :exactor_is_edit
|
103
|
+
before_save :on_update
|
104
|
+
after_save :on_saved
|
105
|
+
attr_accessor :current_payment
|
106
|
+
|
107
|
+
#before save
|
108
|
+
def on_update
|
109
|
+
if (adjustments.tax.reload.size==0 and !shipping_method.nil?)
|
110
|
+
create_tax_charge!
|
111
|
+
end
|
112
|
+
update_adjustments
|
113
|
+
if ['complete', 'resumed'].include?(state) and !changed_attributes.keys().include?('state')
|
114
|
+
if (!exactor_commit_transaction or exactor_invoice_transaction) and !exactor_is_edit.to_s.empty?
|
115
|
+
edit_order
|
116
|
+
end
|
117
|
+
end
|
118
|
+
return errors.empty?
|
119
|
+
end
|
120
|
+
|
121
|
+
#after save
|
122
|
+
def on_saved
|
123
|
+
update_column(:exactor_is_edit, nil);
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Spree
|
2
|
+
Payment.class_eval do
|
3
|
+
attr_accessor :previous_state
|
4
|
+
before_save :populate_order
|
5
|
+
|
6
|
+
|
7
|
+
def populate_order
|
8
|
+
if (self.changed_attributes.include?('state'))
|
9
|
+
self.previous_state=self.changed_attributes['state'].to_s.clone()
|
10
|
+
end
|
11
|
+
order.current_payment=self
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|