spree_core 2.1.12 → 2.2.0

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