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.
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