spree_exactor 1.1.1.112920126

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.rspec +1 -0
  2. data/Gemfile.lock +231 -0
  3. data/README.md +21 -0
  4. data/app/assets/javascripts/admin/exactor_settings.js +2 -0
  5. data/app/assets/javascripts/admin/spree_exactor.js +1 -0
  6. data/app/assets/javascripts/store/spree_exactor.js +1 -0
  7. data/app/assets/stylesheets/admin/spree_exactor.css +3 -0
  8. data/app/assets/stylesheets/exactor_settings.css +4 -0
  9. data/app/assets/stylesheets/store/spree_exactor.css +3 -0
  10. data/app/controllers/spree/admin/exactor_settings_controller.rb +34 -0
  11. data/app/helpers/exactor_api_objects.rb +387 -0
  12. data/app/helpers/exactor_api_service.rb +96 -0
  13. data/app/helpers/spree_exactor_connector.rb +380 -0
  14. data/app/models/spree/adjustment_decorator.rb +41 -0
  15. data/app/models/spree/calculator/exactor_tax_calculator.rb +87 -0
  16. data/app/models/spree/exactor_setting.rb +140 -0
  17. data/app/models/spree/line_item_decorator.rb +31 -0
  18. data/app/models/spree/order_decorator.rb +126 -0
  19. data/app/models/spree/payment_decorator.rb +15 -0
  20. data/app/models/spree/tax_category_decorator.rb +31 -0
  21. data/app/models/spree/tax_rate_decorator.rb +46 -0
  22. data/app/models/spree/user_decorator.rb +7 -0
  23. data/app/models/spree/zone_decorator.rb +31 -0
  24. data/app/overrides/spree_exactor_overrides.rb +27 -0
  25. data/app/views/spree/admin/exactor_settings/show.html.erb +113 -0
  26. data/config/locales/en.yml +10 -0
  27. data/config/routes.rb +6 -0
  28. data/db/migrate/20120624223025_create_exactor_settings.rb +20 -0
  29. data/db/migrate/20120627130223_add_exactor_info_to_order.rb +8 -0
  30. data/db/migrate/20120705103207_add_exactor_indicator_to_tax_rates.rb +5 -0
  31. data/db/migrate/20120713152512_modify_dependent_entities.rb +6 -0
  32. data/db/migrate/20120813124740_extend_user.rb +5 -0
  33. data/db/migrate/20120815104739_add_exactor_tor_inline_tax.rb +5 -0
  34. data/lib/generators/spree_exactor/install/install_generator.rb +80 -0
  35. data/lib/generators/spree_exactor/uninstall/uninstall_generator.rb +24 -0
  36. data/lib/spree_exactor/engine.rb +25 -0
  37. data/lib/spree_exactor.rb +2 -0
  38. data/spree_exactor.gemspec +66 -0
  39. 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