spree_core 2.1.12 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (172) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/base_helper.rb +4 -7
  3. data/app/helpers/spree/products_helper.rb +11 -9
  4. data/app/helpers/spree/store_helper.rb +5 -0
  5. data/app/models/spree/ability.rb +4 -0
  6. data/app/models/spree/address.rb +6 -6
  7. data/app/models/spree/adjustment.rb +40 -61
  8. data/app/models/spree/app_configuration.rb +1 -14
  9. data/app/models/spree/calculator.rb +12 -4
  10. data/app/models/spree/calculator/default_tax.rb +42 -38
  11. data/app/models/spree/calculator/flat_percent_item_total.rb +2 -4
  12. data/app/models/spree/calculator/free_shipping.rb +5 -2
  13. data/app/models/spree/calculator/percent_on_line_item.rb +15 -0
  14. data/app/models/spree/calculator/percent_per_item.rb +3 -0
  15. data/app/models/spree/classification.rb +3 -2
  16. data/app/models/spree/credit_card.rb +7 -25
  17. data/app/models/spree/gateway/bogus.rb +5 -5
  18. data/app/models/spree/gateway/bogus_simple.rb +0 -8
  19. data/app/models/spree/image.rb +0 -9
  20. data/app/models/spree/inventory_unit.rb +10 -4
  21. data/app/models/spree/item_adjustments.rb +65 -0
  22. data/app/models/spree/legacy_user.rb +1 -0
  23. data/app/models/spree/line_item.rb +33 -13
  24. data/app/models/spree/option_type.rb +2 -2
  25. data/app/models/spree/option_value.rb +1 -1
  26. data/app/models/spree/order.rb +109 -89
  27. data/app/models/spree/order/checkout.rb +48 -0
  28. data/app/models/spree/order_contents.rb +72 -37
  29. data/app/models/spree/order_inventory.rb +65 -68
  30. data/app/models/spree/order_populator.rb +3 -17
  31. data/app/models/spree/order_updater.rb +63 -44
  32. data/app/models/spree/payment.rb +20 -5
  33. data/app/models/spree/payment/processing.rb +19 -25
  34. data/app/models/spree/payment_capture_event.rb +9 -0
  35. data/app/models/spree/payment_method/check.rb +0 -2
  36. data/app/models/spree/price.rb +1 -1
  37. data/app/models/spree/product.rb +14 -16
  38. data/app/models/spree/product/scopes.rb +4 -6
  39. data/app/models/spree/product_option_type.rb +2 -2
  40. data/app/models/spree/product_property.rb +2 -2
  41. data/app/models/spree/promotion.rb +71 -50
  42. data/app/models/spree/promotion/actions/create_adjustment.rb +31 -32
  43. data/app/models/spree/promotion/actions/create_item_adjustments.rb +83 -0
  44. data/app/models/spree/promotion/actions/free_shipping.rb +36 -0
  45. data/app/models/spree/promotion/rules/first_order.rb +4 -0
  46. data/app/models/spree/promotion/rules/item_total.rb +5 -1
  47. data/app/models/spree/promotion/rules/product.rb +4 -0
  48. data/app/models/spree/promotion/rules/user.rb +5 -6
  49. data/app/models/spree/promotion/rules/user_logged_in.rb +4 -0
  50. data/app/models/spree/promotion_action.rb +1 -5
  51. data/app/models/spree/promotion_handler/cart.rb +38 -0
  52. data/app/models/spree/promotion_handler/coupon.rb +76 -0
  53. data/app/models/spree/promotion_handler/free_shipping.rb +31 -0
  54. data/app/models/spree/promotion_handler/page.rb +24 -0
  55. data/app/models/spree/promotion_rule.rb +15 -7
  56. data/app/models/spree/property.rb +1 -1
  57. data/app/models/spree/return_authorization.rb +7 -1
  58. data/app/models/spree/shipment.rb +113 -49
  59. data/app/models/spree/shipping_calculator.rb +4 -5
  60. data/app/models/spree/shipping_category.rb +2 -2
  61. data/app/models/spree/shipping_method.rb +12 -6
  62. data/app/models/spree/shipping_rate.rb +27 -7
  63. data/app/models/spree/stock/availability_validator.rb +1 -1
  64. data/app/models/spree/stock/estimator.rb +13 -1
  65. data/app/models/spree/stock/package.rb +11 -7
  66. data/app/models/spree/stock/packer.rb +3 -3
  67. data/app/models/spree/stock/quantifier.rb +9 -1
  68. data/app/models/spree/stock_item.rb +11 -6
  69. data/app/models/spree/stock_movement.rb +1 -2
  70. data/app/models/spree/tax_category.rb +6 -1
  71. data/app/models/spree/tax_rate.rb +57 -49
  72. data/app/models/spree/taxon.rb +10 -5
  73. data/app/models/spree/taxonomy.rb +5 -2
  74. data/app/models/spree/variant.rb +33 -16
  75. data/app/models/spree/zone.rb +24 -24
  76. data/app/views/spree/shared/_routes.html.erb +3 -0
  77. data/config/locales/en.yml +42 -26
  78. data/db/migrate/20130213191427_create_default_stock.rb +3 -3
  79. data/db/migrate/20130413230529_add_name_to_spree_credit_cards.rb +5 -0
  80. data/db/migrate/20130414000512_update_name_fields_on_spree_credit_cards.rb +13 -0
  81. data/db/migrate/20130417120035_update_adjustment_states.rb +2 -2
  82. data/db/migrate/20130417123427_add_shipping_rates_to_shipments.rb +1 -1
  83. data/db/migrate/20130509115210_add_number_to_stock_transfer.rb +1 -1
  84. data/db/migrate/20130611054351_rename_shipping_methods_zones_to_spree_shipping_methods_zones.rb +0 -5
  85. data/db/migrate/20130802022321_migrate_tax_categories_to_line_items.rb +7 -5
  86. data/db/migrate/20130807024301_upgrade_adjustments.rb +39 -0
  87. data/db/migrate/20130807024302_rename_adjustment_fields.rb +17 -0
  88. data/db/migrate/20130813004002_add_shipment_total_to_spree_orders.rb +5 -0
  89. data/db/migrate/20130813232134_rename_activators_to_promotions.rb +5 -0
  90. data/db/migrate/20130815000406_add_adjustment_total_to_line_items.rb +5 -0
  91. data/db/migrate/20130815024413_add_adjustment_total_to_shipments.rb +5 -0
  92. data/db/migrate/20130828234942_add_tax_total_to_line_items_shipments_and_orders.rb +8 -0
  93. data/db/migrate/20130830001159_migrate_old_shipping_calculators.rb +1 -1
  94. data/db/migrate/20130903183026_add_code_to_spree_promotion_rules.rb +5 -0
  95. data/db/migrate/20130917024658_remove_promotions_event_name_field.rb +5 -0
  96. data/db/migrate/20130924040529_add_promo_total_to_line_items_and_shipments_and_orders.rb +7 -0
  97. data/db/migrate/20131001013410_remove_unused_credit_card_fields.rb +7 -3
  98. data/db/migrate/20131107132123_add_tax_category_to_variants.rb +6 -0
  99. data/db/migrate/20131118043959_add_included_to_adjustments.rb +5 -0
  100. data/db/migrate/20131118050234_rename_tax_total_fields.rb +11 -0
  101. data/db/migrate/20131118183431_add_line_item_id_to_spree_inventory_units.rb +21 -0
  102. data/db/migrate/20131127001002_add_position_to_classifications.rb +5 -0
  103. data/db/migrate/20131211112807_create_spree_orders_promotions.rb +8 -0
  104. data/db/migrate/20131218054603_add_item_count_to_spree_orders.rb +5 -0
  105. data/db/migrate/20140106224208_rename_permalink_to_slug_for_products.rb +5 -0
  106. data/db/migrate/20140124023232_rename_activator_id_in_rules_and_actions_to_promotion_id.rb +6 -0
  107. data/db/migrate/20140203161722_add_approver_id_and_approved_at_to_orders.rb +6 -0
  108. data/db/migrate/20140204115338_add_confirmation_delivered_to_spree_orders.rb +5 -0
  109. data/db/migrate/20140205120320_create_spree_payment_capture_events.rb +12 -0
  110. data/db/migrate/20140205144710_add_uncaptured_amount_to_payments.rb +5 -0
  111. data/db/migrate/20140207085910_add_tax_category_id_to_shipping_methods.rb +5 -0
  112. data/db/migrate/20140207093021_add_tax_rate_id_to_shipping_rates.rb +5 -0
  113. data/db/migrate/20140211040159_add_pre_tax_amount_to_line_items_and_shipments.rb +6 -0
  114. data/db/migrate/20140213184916_add_more_indexes.rb +13 -0
  115. data/db/migrate/20140219060952_add_considered_risky_to_orders.rb +5 -0
  116. data/lib/generators/spree/dummy/dummy_generator.rb +1 -6
  117. data/lib/generators/spree/install/install_generator.rb +6 -6
  118. data/lib/generators/spree/install/templates/{app/assets/javascripts/admin → vendor/assets/javascripts/spree/backend}/all.js +3 -3
  119. data/lib/generators/spree/install/templates/{app/assets/javascripts/store → vendor/assets/javascripts/spree/frontend}/all.js +3 -3
  120. data/lib/generators/spree/install/templates/{app/assets/stylesheets/store → vendor/assets/stylesheets/spree/backend}/all.css +3 -3
  121. data/lib/generators/spree/install/templates/{app/assets/stylesheets/admin → vendor/assets/stylesheets/spree/frontend}/all.css +3 -3
  122. data/lib/spree/core.rb +21 -8
  123. data/lib/spree/core/calculated_adjustments.rb +0 -40
  124. data/lib/spree/core/controller_helpers.rb +5 -0
  125. data/lib/spree/core/controller_helpers/auth.rb +2 -2
  126. data/lib/spree/core/controller_helpers/common.rb +0 -5
  127. data/lib/spree/core/controller_helpers/order.rb +8 -9
  128. data/lib/spree/core/engine.rb +10 -17
  129. data/lib/spree/core/permalinks.rb +1 -1
  130. data/lib/spree/core/product_duplicator.rb +3 -8
  131. data/lib/spree/core/user_address.rb +1 -1
  132. data/lib/spree/core/validators/email.rb +23 -1
  133. data/lib/spree/core/version.rb +1 -1
  134. data/lib/spree/money.rb +1 -1
  135. data/lib/spree/permitted_attributes.rb +2 -2
  136. data/lib/spree/testing_support/caching.rb +47 -0
  137. data/lib/spree/testing_support/factories/adjustment_factory.rb +11 -2
  138. data/lib/spree/testing_support/factories/credit_card_factory.rb +2 -1
  139. data/lib/spree/testing_support/factories/order_factory.rb +10 -5
  140. data/lib/spree/testing_support/factories/payment_factory.rb +2 -2
  141. data/lib/spree/testing_support/factories/payment_method_factory.rb +3 -3
  142. data/lib/spree/testing_support/factories/promotion_factory.rb +16 -1
  143. data/lib/spree/testing_support/factories/shipment_factory.rb +8 -4
  144. data/lib/spree/testing_support/factories/shipping_method_factory.rb +1 -3
  145. data/lib/spree/testing_support/factories/stock_factory.rb +1 -1
  146. data/lib/spree/testing_support/factories/tax_rate_factory.rb +2 -2
  147. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  148. data/lib/tasks/core.rake +2 -2
  149. data/vendor/assets/fonts/FontAwesome.otf +0 -0
  150. data/vendor/assets/fonts/fontawesome-webfont.eot +0 -0
  151. data/vendor/assets/fonts/fontawesome-webfont.svg +399 -0
  152. data/vendor/assets/fonts/fontawesome-webfont.ttf +0 -0
  153. data/vendor/assets/fonts/fontawesome-webfont.woff +0 -0
  154. data/vendor/assets/stylesheets/font-awesome.scss +1475 -0
  155. metadata +73 -44
  156. data/app/assets/javascripts/admin/handlebar_extensions.js +0 -9
  157. data/app/helpers/spree/admin/adjustments_helper.rb +0 -26
  158. data/app/helpers/spree/admin/images_helper.rb +0 -18
  159. data/app/helpers/spree/promotion_rules_helper.rb +0 -13
  160. data/app/models/spree/activator.rb +0 -29
  161. data/app/models/spree/calculator/per_item.rb +0 -41
  162. data/app/models/spree/stock/remaining_packer.rb +0 -22
  163. data/app/views/spree/payments/_payment.html.erb +0 -18
  164. data/db/migrate/20131118041203_add_tax_total_to_spree_orders.rb +0 -5
  165. data/db/migrate/20131118043021_add_order_id_to_spree_adjustments.rb +0 -6
  166. data/db/migrate/20131118074808_add_included_to_spree_adjustments.rb +0 -5
  167. data/db/migrate/20140415041315_add_user_id_created_by_id_index_to_order.rb +0 -5
  168. data/lib/spree/core/gateway_error.rb +0 -5
  169. data/lib/spree/core/preference_rescue.rb +0 -25
  170. data/lib/spree/core/s3_support.rb +0 -25
  171. data/lib/spree/promo/coupon_applicator.rb +0 -71
  172. data/lib/spree/testing_support/factories/activator_factory.rb +0 -8
@@ -4,16 +4,19 @@ module Spree
4
4
 
5
5
  IDENTIFIER_CHARS = (('A'..'Z').to_a + ('0'..'9').to_a - %w(0 1 I O)).freeze
6
6
 
7
- belongs_to :order, class_name: 'Spree::Order', touch: true
7
+ belongs_to :order, class_name: 'Spree::Order', touch: true, inverse_of: :payments
8
8
  belongs_to :source, polymorphic: true
9
9
  belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
10
10
 
11
11
  has_many :offsets, -> { where("source_type = 'Spree::Payment' AND amount < 0 AND state = 'completed'") },
12
12
  class_name: "Spree::Payment", foreign_key: :source_id
13
13
  has_many :log_entries, as: :source
14
+ has_many :state_changes, as: :stateful
15
+ has_many :capture_events, :class_name => 'Spree::PaymentCaptureEvent'
14
16
 
15
17
  before_validation :validate_source
16
18
  before_create :set_unique_identifier
19
+ before_save :update_uncaptured_amount
17
20
 
18
21
  after_save :create_payment_profile, if: :profiles_supported?
19
22
 
@@ -67,6 +70,14 @@ module Spree
67
70
  event :invalidate do
68
71
  transition from: [:checkout], to: :invalid
69
72
  end
73
+
74
+ after_transition do |payment, transition|
75
+ payment.state_changes.create!(
76
+ previous_state: transition.from,
77
+ next_state: transition.to,
78
+ name: 'payment',
79
+ )
80
+ end
70
81
  end
71
82
 
72
83
  def currency
@@ -83,7 +94,7 @@ module Spree
83
94
  case amount
84
95
  when String
85
96
  separator = I18n.t('number.currency.format.separator')
86
- number = amount.delete("^0-9-#{separator}\.").tr(separator, '.')
97
+ number = amount.delete("^0-9-#{separator}").tr(separator, '.')
87
98
  number.to_d if number.present?
88
99
  end || amount
89
100
  end
@@ -93,7 +104,7 @@ module Spree
93
104
  end
94
105
 
95
106
  def credit_allowed
96
- amount - offsets_total.abs
107
+ amount - offsets_total
97
108
  end
98
109
 
99
110
  def can_credit?
@@ -102,8 +113,8 @@ module Spree
102
113
 
103
114
  # see https://github.com/spree/spree/issues/981
104
115
  def build_source
105
- return unless new_record?
106
- if source_attributes.present? && source.blank? && payment_method.try(:payment_source_class)
116
+ return if source_attributes.nil?
117
+ if payment_method and payment_method.payment_source_class
107
118
  self.source = payment_method.payment_source_class.new(source_attributes)
108
119
  end
109
120
  end
@@ -179,5 +190,9 @@ module Spree
179
190
  def generate_identifier
180
191
  Array.new(8){ IDENTIFIER_CHARS.sample }.join
181
192
  end
193
+
194
+ def update_uncaptured_amount
195
+ self.uncaptured_amount = amount - capture_events.sum(:amount)
196
+ end
182
197
  end
183
198
  end
@@ -27,28 +27,31 @@ module Spree
27
27
  gateway_action(source, :authorize, :pend)
28
28
  end
29
29
 
30
+ # Captures the entire amount of a payment.
30
31
  def purchase!
31
32
  started_processing!
32
- gateway_action(source, :purchase, :complete)
33
+ result = gateway_action(source, :purchase, :complete)
34
+ # This won't be called if gateway_action raises a GatewayError
35
+ capture_events.create!(amount: amount)
33
36
  end
34
37
 
35
- def capture!
38
+ # Takes the amount in cents to capture.
39
+ # Can be used to capture partial amounts of a payment.
40
+ def capture!(amount=nil)
41
+ amount ||= money.money.cents
36
42
  return true if completed?
37
43
  started_processing!
38
44
  protect_from_connection_error do
39
45
  check_environment
40
-
41
- if payment_method.payment_profiles_supported?
42
- # Gateways supporting payment profiles will need access to credit card object because this stores the payment profile information
43
- # so supply the authorization itself as well as the credit card, rather than just the authorization code
44
- response = payment_method.capture(self, source, gateway_options)
45
- else
46
- # Standard ActiveMerchant capture usage
47
- response = payment_method.capture(money.money.cents,
48
- response_code,
49
- gateway_options)
50
- end
51
-
46
+ # Standard ActiveMerchant capture usage
47
+ response = payment_method.capture(
48
+ amount,
49
+ response_code,
50
+ gateway_options
51
+ )
52
+
53
+ money = ::Money.new(amount, Spree::Config[:currency])
54
+ capture_events.create!(amount: money.to_f)
52
55
  handle_response(response, :complete, :failure)
53
56
  end
54
57
  end
@@ -108,14 +111,6 @@ module Spree
108
111
  end
109
112
  end
110
113
 
111
- def cancel!
112
- if payment_method.respond_to?(:cancel)
113
- payment_method.cancel(response_code)
114
- else
115
- credit!
116
- end
117
- end
118
-
119
114
  def partial_credit(amount)
120
115
  return if amount > credit_allowed
121
116
  started_processing!
@@ -123,7 +118,6 @@ module Spree
123
118
  end
124
119
 
125
120
  def gateway_options
126
- order.reload
127
121
  options = { :email => order.email,
128
122
  :customer => order.email,
129
123
  :customer_id => order.user_id,
@@ -135,9 +129,9 @@ module Spree
135
129
  :order_id => gateway_order_id }
136
130
 
137
131
  options.merge!({ :shipping => order.ship_total * 100,
138
- :tax => order.tax_total * 100,
132
+ :tax => order.additional_tax_total * 100,
139
133
  :subtotal => order.item_total * 100,
140
- :discount => order.discount_total * 100,
134
+ :discount => order.promo_total * 100,
141
135
  :currency => currency })
142
136
 
143
137
  options.merge!({ :billing_address => order.bill_address.try(:active_merchant_hash),
@@ -0,0 +1,9 @@
1
+ module Spree
2
+ class PaymentCaptureEvent < ActiveRecord::Base
3
+ belongs_to :payment, class_name: 'Spree::Payment'
4
+
5
+ def display_amount
6
+ Spree::Money.new(amount, { currency: payment.currency })
7
+ end
8
+ end
9
+ end
@@ -18,8 +18,6 @@ module Spree
18
18
  ActiveMerchant::Billing::Response.new(true, "", {}, {})
19
19
  end
20
20
 
21
- def cancel(response); end
22
-
23
21
  def void(*args)
24
22
  ActiveMerchant::Billing::Response.new(true, "", {}, {})
25
23
  end
@@ -1,7 +1,7 @@
1
1
  module Spree
2
2
  class Price < ActiveRecord::Base
3
3
  acts_as_paranoid
4
- belongs_to :variant, class_name: 'Spree::Variant'
4
+ belongs_to :variant, class_name: 'Spree::Variant', inverse_of: :prices
5
5
 
6
6
  validate :check_price
7
7
  validates :amount, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
@@ -20,18 +20,21 @@
20
20
 
21
21
  module Spree
22
22
  class Product < ActiveRecord::Base
23
+ extend FriendlyId
24
+ friendly_id :name, use: :slugged
25
+
23
26
  acts_as_paranoid
24
- has_many :product_option_types, dependent: :destroy
27
+ has_many :product_option_types, dependent: :destroy, inverse_of: :product
25
28
  has_many :option_types, through: :product_option_types
26
- has_many :product_properties, dependent: :destroy
29
+ has_many :product_properties, dependent: :destroy, inverse_of: :product
27
30
  has_many :properties, through: :product_properties
28
31
 
29
- has_many :classifications, dependent: :delete_all
32
+ has_many :classifications, dependent: :delete_all, inverse_of: :product
30
33
  has_many :taxons, through: :classifications
31
34
  has_and_belongs_to_many :promotion_rules, join_table: :spree_products_promotion_rules
32
35
 
33
36
  belongs_to :tax_category, class_name: 'Spree::TaxCategory'
34
- belongs_to :shipping_category, class_name: 'Spree::ShippingCategory'
37
+ belongs_to :shipping_category, class_name: 'Spree::ShippingCategory', inverse_of: :products
35
38
 
36
39
  has_one :master,
37
40
  -> { where is_master: true },
@@ -62,6 +65,8 @@ module Spree
62
65
  after_create :add_properties_and_option_types_from_prototype
63
66
  after_create :build_variants_from_option_values_hash, if: :option_values_hash
64
67
  after_save :save_master
68
+ after_save :touch
69
+ after_touch :touch_taxons
65
70
 
66
71
  delegate :images, to: :master, prefix: true
67
72
  alias_method :images, :master_images
@@ -69,24 +74,20 @@ module Spree
69
74
  has_many :variant_images, -> { order(:position) }, source: :images, through: :variants_including_master
70
75
 
71
76
  validates :name, presence: true
72
- validates :permalink, presence: true
73
77
  validates :price, presence: true, if: proc { Spree::Config[:require_master_price] }
74
78
  validates :shipping_category_id, presence: true
79
+ validates :slug, length: { minimum: 3 }
75
80
 
76
81
  attr_accessor :option_values_hash
77
82
 
78
83
  accepts_nested_attributes_for :product_properties, allow_destroy: true, reject_if: lambda { |pp| pp[:property_name].blank? }
79
84
 
80
- make_permalink order: :name
81
-
82
85
  alias :options :product_option_types
83
86
 
84
87
  after_initialize :ensure_master
85
88
 
86
- before_destroy :punch_permalink
87
-
88
89
  def to_param
89
- permalink.present? ? permalink : (permalink_was || name.to_s.to_url)
90
+ slug
90
91
  end
91
92
 
92
93
  # the master variant is not a member of the variants array
@@ -131,11 +132,8 @@ module Spree
131
132
  !!deleted_at
132
133
  end
133
134
 
134
- # determine if product is available.
135
- # deleted products and products with nil or future available_on date
136
- # are not available
137
135
  def available?
138
- !(available_on.nil? || available_on.future?) && !deleted?
136
+ !(available_on.nil? || available_on.future?)
139
137
  end
140
138
 
141
139
  # split variants list into hash which shows mapping of opt value onto matching variants
@@ -251,8 +249,8 @@ module Spree
251
249
  self.master ||= Variant.new
252
250
  end
253
251
 
254
- def punch_permalink
255
- update_attribute :permalink, "#{Time.now.to_i}_#{permalink}" # punch permalink with date prefix
252
+ def touch_taxons
253
+ self.taxons.each(&:touch)
256
254
  end
257
255
  end
258
256
  end
@@ -67,11 +67,9 @@ module Spree
67
67
  #
68
68
  # SELECT COUNT(*) ...
69
69
  add_search_scope :in_taxon do |taxon|
70
- select("spree_products.id, spree_products.*").
71
- where(id: Classification.select('spree_products_taxons.product_id').
72
- joins(:taxon).
73
- where(Taxon.table_name => { :id => taxon.self_and_descendants.pluck(:id) })
74
- )
70
+ includes(:classifications).
71
+ where("spree_products_taxons.taxon_id" => taxon.self_and_descendants.pluck(:id)).
72
+ order("spree_products_taxons.position ASC")
75
73
  end
76
74
 
77
75
  # This scope selects products in all taxons AND all its descendants
@@ -216,7 +214,7 @@ module Spree
216
214
  distinct_fields = ["id", sort_column].compact.join(",")
217
215
  select("DISTINCT ON(#{distinct_fields}) spree_products.*")
218
216
  else
219
- scoped
217
+ all
220
218
  end
221
219
  else
222
220
  select("DISTINCT spree_products.*")
@@ -1,7 +1,7 @@
1
1
  module Spree
2
2
  class ProductOptionType < ActiveRecord::Base
3
- belongs_to :product, class_name: 'Spree::Product'
4
- belongs_to :option_type, class_name: 'Spree::OptionType'
3
+ belongs_to :product, class_name: 'Spree::Product', inverse_of: :product_option_types
4
+ belongs_to :option_type, class_name: 'Spree::OptionType', inverse_of: :product_option_types
5
5
  acts_as_list scope: :product
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  module Spree
2
2
  class ProductProperty < ActiveRecord::Base
3
- belongs_to :product, touch: true, class_name: 'Spree::Product'
4
- belongs_to :property, class_name: 'Spree::Property'
3
+ belongs_to :product, touch: true, class_name: 'Spree::Product', inverse_of: :product_properties
4
+ belongs_to :property, class_name: 'Spree::Property', inverse_of: :product_properties
5
5
 
6
6
  validates :property, presence: true
7
7
  validates :value, length: { maximum: 255 }
@@ -1,75 +1,94 @@
1
1
  module Spree
2
- class Promotion < Spree::Activator
2
+ class Promotion < ActiveRecord::Base
3
3
  MATCH_POLICIES = %w(all any)
4
4
  UNACTIVATABLE_ORDER_STATES = ["complete", "awaiting_return", "returned"]
5
5
 
6
- Activator.event_names << 'spree.checkout.coupon_code_added'
7
- Activator.event_names << 'spree.content.visited'
8
-
9
- has_many :promotion_rules, foreign_key: :activator_id, autosave: true, dependent: :destroy
6
+ has_many :promotion_rules, autosave: true, dependent: :destroy
10
7
  alias_method :rules, :promotion_rules
11
8
 
12
- has_many :promotion_actions, foreign_key: :activator_id, autosave: true, dependent: :destroy
9
+ has_many :promotion_actions, autosave: true, dependent: :destroy
13
10
  alias_method :actions, :promotion_actions
14
11
 
12
+ has_and_belongs_to_many :orders, join_table: 'spree_orders_promotions'
13
+
15
14
  accepts_nested_attributes_for :promotion_actions, :promotion_rules
16
15
 
17
16
  validates_associated :rules
18
17
 
19
18
  validates :name, presence: true
20
- validates :code, presence: true, if: lambda{|r| r.event_name == 'spree.checkout.coupon_code_added' }
21
- validates :path, presence: true, if: lambda{|r| r.event_name == 'spree.content.visited' }
19
+ validates :path, uniqueness: true, allow_blank: true
22
20
  validates :usage_limit, numericality: { greater_than: 0, allow_nil: true }
21
+ validates :description, length: { maximum: 255 }
22
+
23
+ before_save :normalize_blank_values
23
24
 
24
25
  def self.advertised
25
26
  where(advertise: true)
26
27
  end
27
28
 
28
- def self.with_code
29
- where(event_name: 'spree.checkout.coupon_code_added')
29
+ def self.with_coupon_code(coupon_code)
30
+ where("lower(code) = ?", coupon_code.strip.downcase).first
31
+ end
32
+
33
+ def self.active
34
+ where('starts_at IS NULL OR starts_at < ?', Time.now).
35
+ where('expires_at IS NULL OR expires_at > ?', Time.now)
36
+ end
37
+
38
+ def self.order_activatable?(order)
39
+ order && !UNACTIVATABLE_ORDER_STATES.include?(order.state)
40
+ end
41
+
42
+ def expired?
43
+ starts_at && Time.now < starts_at || expires_at && Time.now > expires_at
30
44
  end
31
45
 
32
46
  def activate(payload)
33
- if order_activatable?(payload[:order]) && eligible?(payload[:order])
34
- # make sure code is always downcased (old databases might have mixed case codes)
35
- if code.present?
36
- event_code = payload[:coupon_code]
37
- return unless event_code == self.code.downcase.strip
38
- end
39
-
40
- if path.present?
41
- return unless path == payload[:path]
42
- end
43
-
44
- actions.each do |action|
45
- action.perform(payload)
46
- end
47
-
48
- return true
47
+ order = payload[:order]
48
+ return unless self.class.order_activatable?(order)
49
+
50
+ # Track results from actions to see if any action has been taken.
51
+ # Actions should return nil/false if no action has been taken.
52
+ # If an action returns true, then an action has been taken.
53
+ results = actions.map do |action|
54
+ action.perform(payload)
49
55
  end
50
- false
56
+ # If an action has been taken, report back to whatever activated this promotion.
57
+ action_taken = results.include?(true)
58
+
59
+ if action_taken
60
+ # connect to the order
61
+ # create the join_table entry.
62
+ self.orders << order
63
+ self.save
64
+ end
65
+
66
+ return action_taken
51
67
  end
52
68
 
53
69
  # called anytime order.update! happens
54
- def eligible?(order)
55
- return false if expired? || usage_limit_exceeded?(order)
56
- rules_are_eligible?(order, {})
70
+ def eligible?(promotable)
71
+ return false if expired? || usage_limit_exceeded?(promotable)
72
+ rules_are_eligible?(promotable, {})
57
73
  end
58
74
 
59
- def rules_are_eligible?(order, options = {})
75
+ def rules_are_eligible?(promotable, options = {})
76
+ # Promotions without rules are eligible by default.
60
77
  return true if rules.none?
61
- eligible = lambda { |r| r.eligible?(order, options) }
78
+ eligible = lambda { |r| r.eligible?(promotable, options) }
79
+ specific_rules = rules.for(promotable)
80
+ return true if specific_rules.none?
62
81
  if match_policy == 'all'
63
- rules.all?(&eligible)
82
+ # If there are rules for this promotion, but no rules for this
83
+ # particular promotable, then the promotion is ineligible by default.
84
+ specific_rules.any? && specific_rules.all?(&eligible)
64
85
  else
65
- rules.any?(&eligible)
86
+ # If there are no rules for this promotable, then this will return false.
87
+ # If there are rules for this promotable, but they are ineligible, this will return false.
88
+ specific_rules.any?(&eligible)
66
89
  end
67
90
  end
68
91
 
69
- def order_activatable?(order)
70
- order && !UNACTIVATABLE_ORDER_STATES.include?(order.state)
71
- end
72
-
73
92
  # Products assigned to all product rules
74
93
  def products
75
94
  @products ||= self.rules.to_a.inject([]) do |products, rule|
@@ -77,29 +96,31 @@ module Spree
77
96
  end.flatten.uniq
78
97
  end
79
98
 
80
- def usage_limit_exceeded?(order = nil)
81
- usage_limit.present? && usage_limit > 0 && adjusted_credits_count(order) >= usage_limit
99
+ def product_ids
100
+ products.map(&:id)
82
101
  end
83
102
 
84
- def adjusted_credits_count(order)
85
- return credits_count if order.nil?
86
- credits_count - (exists_on_order?(order) ? 1 : 0)
103
+ def usage_limit_exceeded?(promotable)
104
+ usage_limit.present? && usage_limit > 0 && adjusted_credits_count(promotable) >= usage_limit
105
+ end
106
+
107
+ def adjusted_credits_count(promotable)
108
+ credits_count - promotable.adjustments.promotion.where(:source_id => actions.pluck(:id)).count
87
109
  end
88
110
 
89
111
  def credits
90
- Adjustment.eligible.promotion.where(originator_id: actions.map(&:id))
112
+ Adjustment.eligible.promotion.where(source_id: actions.map(&:id))
91
113
  end
92
114
 
93
115
  def credits_count
94
116
  credits.count
95
117
  end
96
118
 
97
- def code=(coupon_code)
98
- write_attribute(:code, (coupon_code.downcase.strip rescue nil))
99
- end
100
-
101
- def exists_on_order?(order)
102
- actions.any? { |a| a.credit_exists_on_order?(order) }
119
+ private
120
+ def normalize_blank_values
121
+ [:code, :path].each do |column|
122
+ self[column] = nil if self[column].blank?
123
+ end
103
124
  end
104
125
  end
105
126
  end