spree_exactor 1.1.1.112920126
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/.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
|