spree_core 5.3.6 → 5.4.0.beta

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 (243) hide show
  1. checksums.yaml +4 -4
  2. data/app/finders/spree/orders/find_complete.rb +33 -2
  3. data/app/finders/spree/stores/find_default.rb +17 -0
  4. data/app/finders/spree/variants/visible_finder.rb +1 -0
  5. data/app/helpers/spree/addresses_helper.rb +1 -2
  6. data/app/helpers/spree/base_helper.rb +5 -4
  7. data/app/jobs/spree/api_key_touch_job.rb +9 -0
  8. data/app/jobs/spree/images/save_from_url_job.rb +23 -47
  9. data/app/models/concerns/spree/admin_user_methods.rb +32 -0
  10. data/app/models/concerns/spree/number_as_param.rb +5 -3
  11. data/app/models/concerns/spree/prefixed_id.rb +82 -0
  12. data/app/models/concerns/spree/product_scopes.rb +33 -18
  13. data/app/models/concerns/spree/publishable.rb +47 -47
  14. data/app/models/concerns/spree/{multi_store_resource.rb → store_scoped_resource.rb} +1 -10
  15. data/app/models/concerns/spree/stores/markets.rb +124 -0
  16. data/app/models/concerns/spree/user_methods.rb +7 -7
  17. data/app/models/concerns/spree/user_payment_source.rb +2 -0
  18. data/app/models/concerns/spree/user_roles.rb +1 -0
  19. data/app/models/spree/ability.rb +13 -0
  20. data/app/models/spree/address.rb +30 -0
  21. data/app/models/spree/adjustment.rb +2 -0
  22. data/app/models/spree/api_key.rb +47 -0
  23. data/app/models/spree/asset.rb +2 -0
  24. data/app/models/spree/authentication/strategies/base_strategy.rb +55 -0
  25. data/app/models/spree/authentication/strategies/email_password_strategy.rb +47 -0
  26. data/app/models/spree/base.rb +1 -0
  27. data/app/models/spree/calculator.rb +2 -0
  28. data/app/models/spree/country.rb +56 -0
  29. data/app/models/spree/coupon_code.rb +2 -0
  30. data/app/models/spree/credit_card.rb +2 -0
  31. data/app/models/spree/current.rb +9 -4
  32. data/app/models/spree/customer_group.rb +2 -0
  33. data/app/models/spree/customer_return.rb +2 -0
  34. data/app/models/spree/data_feed.rb +2 -0
  35. data/app/models/spree/digital.rb +2 -0
  36. data/app/models/spree/digital_link.rb +2 -0
  37. data/app/models/spree/export.rb +5 -4
  38. data/app/models/spree/fulfilment_changer.rb +8 -2
  39. data/app/models/spree/gateway/bogus.rb +60 -0
  40. data/app/models/spree/gateway_customer.rb +2 -0
  41. data/app/models/spree/gift_card.rb +2 -0
  42. data/app/models/spree/gift_card_batch.rb +2 -4
  43. data/app/models/spree/image.rb +18 -0
  44. data/app/models/spree/import.rb +5 -3
  45. data/app/models/spree/import_mapping.rb +2 -0
  46. data/app/models/spree/import_row.rb +2 -0
  47. data/app/models/spree/import_schemas/customers.rb +21 -0
  48. data/app/models/spree/imports/customers.rb +9 -0
  49. data/app/models/spree/integration.rb +2 -0
  50. data/app/models/spree/inventory_unit.rb +2 -0
  51. data/app/models/spree/invitation.rb +6 -6
  52. data/app/models/spree/legacy_admin_user.rb +31 -0
  53. data/app/models/spree/legacy_user.rb +19 -1
  54. data/app/models/spree/line_item.rb +15 -4
  55. data/app/models/spree/log_entry.rb +2 -0
  56. data/app/models/spree/market.rb +83 -0
  57. data/app/models/spree/market_country.rb +25 -0
  58. data/app/models/spree/metafield.rb +2 -0
  59. data/app/models/spree/metafield_definition.rb +2 -0
  60. data/app/models/spree/newsletter_subscriber.rb +2 -0
  61. data/app/models/spree/option_type.rb +2 -0
  62. data/app/models/spree/option_value.rb +2 -0
  63. data/app/models/spree/order/address_book.rb +2 -1
  64. data/app/models/spree/order.rb +75 -47
  65. data/app/models/spree/payment.rb +6 -1
  66. data/app/models/spree/payment_capture_event.rb +2 -0
  67. data/app/models/spree/payment_method.rb +59 -1
  68. data/app/models/spree/payment_session.rb +109 -0
  69. data/app/models/spree/payment_sessions/bogus.rb +4 -0
  70. data/app/models/spree/payment_setup_session.rb +79 -0
  71. data/app/models/spree/payment_source.rb +2 -0
  72. data/app/models/spree/permission_sets/default_customer.rb +19 -0
  73. data/app/models/spree/policy.rb +2 -0
  74. data/app/models/spree/post.rb +2 -0
  75. data/app/models/spree/post_category.rb +2 -0
  76. data/app/models/spree/price.rb +26 -2
  77. data/app/models/spree/price_list.rb +2 -0
  78. data/app/models/spree/price_rule.rb +2 -0
  79. data/app/models/spree/price_rules/market_rule.rb +19 -0
  80. data/app/models/spree/product.rb +41 -29
  81. data/app/models/spree/promotion/rules/country.rb +1 -1
  82. data/app/models/spree/promotion.rb +3 -1
  83. data/app/models/spree/promotion_action.rb +2 -0
  84. data/app/models/spree/promotion_category.rb +2 -0
  85. data/app/models/spree/promotion_rule.rb +2 -0
  86. data/app/models/spree/prototype.rb +2 -0
  87. data/app/models/spree/refund.rb +2 -4
  88. data/app/models/spree/refund_reason.rb +2 -0
  89. data/app/models/spree/reimbursement/credit.rb +2 -0
  90. data/app/models/spree/reimbursement.rb +2 -0
  91. data/app/models/spree/reimbursement_type.rb +2 -0
  92. data/app/models/spree/report.rb +3 -1
  93. data/app/models/spree/return_authorization.rb +2 -0
  94. data/app/models/spree/return_authorization_reason.rb +2 -0
  95. data/app/models/spree/return_item.rb +2 -0
  96. data/app/models/spree/role.rb +2 -0
  97. data/app/models/spree/shipment.rb +11 -1
  98. data/app/models/spree/shipping_category.rb +2 -0
  99. data/app/models/spree/shipping_method.rb +2 -0
  100. data/app/models/spree/shipping_method_category.rb +2 -0
  101. data/app/models/spree/shipping_rate.rb +2 -0
  102. data/app/models/spree/state_change.rb +2 -0
  103. data/app/models/spree/stock_item.rb +2 -0
  104. data/app/models/spree/stock_location.rb +2 -0
  105. data/app/models/spree/stock_movement.rb +2 -0
  106. data/app/models/spree/stock_transfer.rb +2 -1
  107. data/app/models/spree/store.rb +110 -179
  108. data/app/models/spree/store_credit.rb +2 -4
  109. data/app/models/spree/store_credit_category.rb +2 -0
  110. data/app/models/spree/store_credit_event.rb +2 -0
  111. data/app/models/spree/store_credit_type.rb +2 -0
  112. data/app/models/spree/store_product.rb +2 -0
  113. data/app/models/spree/tax_category.rb +2 -0
  114. data/app/models/spree/tax_rate.rb +2 -0
  115. data/app/models/spree/taxon.rb +4 -1
  116. data/app/models/spree/taxon_image.rb +8 -0
  117. data/app/models/spree/taxon_rule.rb +2 -0
  118. data/app/models/spree/taxonomy.rb +2 -0
  119. data/app/models/spree/user_identity.rb +81 -0
  120. data/app/models/spree/variant.rb +13 -3
  121. data/app/models/spree/webhook_delivery.rb +2 -0
  122. data/app/models/spree/webhook_endpoint.rb +2 -17
  123. data/app/models/spree/wished_item.rb +15 -0
  124. data/app/models/spree/wishlist.rb +2 -8
  125. data/app/models/spree/zone.rb +2 -6
  126. data/app/presenters/spree/filters/price_presenter.rb +1 -0
  127. data/app/presenters/spree/filters/price_range_presenter.rb +1 -0
  128. data/app/presenters/spree/filters/quantified_price_range_presenter.rb +1 -0
  129. data/app/presenters/spree/product_summary_presenter.rb +1 -0
  130. data/app/services/spree/addresses/helper.rb +22 -3
  131. data/app/services/spree/cart/associate.rb +19 -6
  132. data/app/services/spree/checkout/select_shipping_method.rb +13 -1
  133. data/app/services/spree/classifications/reposition.rb +5 -0
  134. data/app/services/spree/data_feeds/google/required_attributes.rb +8 -3
  135. data/app/services/spree/gift_cards/apply.rb +4 -5
  136. data/app/services/spree/imports/row_processors/customer.rb +70 -0
  137. data/app/services/spree/orders/approve.rb +5 -3
  138. data/app/services/spree/orders/cancel.rb +9 -4
  139. data/app/services/spree/products/prepare_nested_attributes.rb +1 -1
  140. data/app/services/spree/sample_data/import_runner.rb +54 -0
  141. data/app/services/spree/sample_data/loader.rb +78 -0
  142. data/app/services/spree/seeds/admin_user.rb +2 -3
  143. data/app/services/spree/seeds/all.rb +1 -0
  144. data/app/services/spree/seeds/api_keys.rb +16 -0
  145. data/app/services/spree/seeds/stores.rb +2 -4
  146. data/app/sorters/spree/orders/sort.rb +4 -0
  147. data/app/subscribers/spree/product_metrics_subscriber.rb +4 -4
  148. data/config/brakeman.ignore +120 -0
  149. data/config/locales/en.yml +20 -5
  150. data/db/migrate/20250923141900_create_spree_user_identities.rb +17 -0
  151. data/db/migrate/20260123000000_create_spree_api_keys.rb +19 -0
  152. data/db/migrate/20260131000000_add_thumbnail_id_to_spree_variants_and_products.rb +9 -0
  153. data/db/migrate/20260213000000_create_spree_payment_sessions.rb +27 -0
  154. data/db/migrate/20260218000000_create_spree_payment_setup_sessions.rb +24 -0
  155. data/db/migrate/20260220000000_create_spree_markets.rb +29 -0
  156. data/db/sample_data/customers.csv +21 -0
  157. data/db/sample_data/metafield_definitions.rb +7 -0
  158. data/db/sample_data/orders.rb +131 -0
  159. data/db/sample_data/payment_methods.rb +17 -0
  160. data/db/sample_data/posts.rb +7 -0
  161. data/db/sample_data/products.csv +1083 -0
  162. data/db/sample_data/promotions.rb +8 -0
  163. data/db/sample_data/shipping_methods.rb +39 -0
  164. data/lib/generators/spree/authentication/devise/devise_generator.rb +2 -2
  165. data/lib/generators/spree/authentication/dummy/dummy_generator.rb +54 -0
  166. data/lib/generators/spree/authentication/dummy/templates/authentication_helpers.rb.tt +52 -0
  167. data/lib/generators/spree/authentication/dummy/templates/create_spree_admin_users.rb.tt +33 -0
  168. data/lib/generators/spree/dummy/dummy_generator.rb +1 -1
  169. data/lib/spree/core/configuration.rb +1 -1
  170. data/lib/spree/core/controller_helpers/common.rb +6 -0
  171. data/lib/spree/core/controller_helpers/currency.rb +5 -0
  172. data/lib/spree/core/controller_helpers/order.rb +5 -1
  173. data/lib/spree/core/controller_helpers/store.rb +1 -1
  174. data/lib/spree/core/dependencies.rb +1 -1
  175. data/lib/spree/core/engine.rb +17 -4
  176. data/lib/spree/core/pricing/context.rb +6 -3
  177. data/lib/spree/core/product_filters.rb +14 -0
  178. data/lib/spree/core/query_filters/comparable.rb +4 -0
  179. data/lib/spree/core/query_filters/text.rb +4 -0
  180. data/lib/spree/core/version.rb +1 -1
  181. data/lib/spree/core.rb +4 -4
  182. data/lib/spree/database_type_utilities.rb +7 -0
  183. data/lib/spree/events.rb +17 -10
  184. data/lib/spree/money.rb +2 -9
  185. data/lib/spree/permitted_attributes.rb +19 -7
  186. data/lib/spree/testing_support/capybara_config.rb +2 -2
  187. data/lib/spree/testing_support/common_rake.rb +15 -4
  188. data/lib/spree/testing_support/factories/api_key_factory.rb +19 -0
  189. data/lib/spree/testing_support/factories/custom_domain_factory.rb +7 -5
  190. data/lib/spree/testing_support/factories/import_factory.rb +12 -0
  191. data/lib/spree/testing_support/factories/market_factory.rb +35 -0
  192. data/lib/spree/testing_support/factories/order_factory.rb +3 -1
  193. data/lib/spree/testing_support/factories/payment_method_factory.rb +2 -0
  194. data/lib/spree/testing_support/factories/payment_session_factory.rb +47 -0
  195. data/lib/spree/testing_support/factories/payment_setup_session_factory.rb +31 -0
  196. data/lib/spree/testing_support/factories/price_rule_factory.rb +10 -0
  197. data/lib/spree/testing_support/factories/user_identity_factory.rb +15 -0
  198. data/lib/spree/testing_support/store.rb +3 -2
  199. data/lib/spree/webhooks.rb +7 -7
  200. data/lib/tasks/images.rake +20 -0
  201. data/lib/tasks/markets.rake +40 -0
  202. data/lib/tasks/sample_data.rake +15 -0
  203. data/spec/fixtures/files/customers_import.csv +4 -0
  204. metadata +88 -63
  205. data/LICENSE.md +0 -57
  206. data/app/finders/spree/stores/find_current.rb +0 -28
  207. data/app/models/spree/custom_domain.rb +0 -59
  208. data/app/presenters/spree/csv/formula_sanitizer.rb +0 -28
  209. data/app/serializers/spree/events/asset_serializer.rb +0 -22
  210. data/app/serializers/spree/events/base_serializer.rb +0 -61
  211. data/app/serializers/spree/events/customer_return_serializer.rb +0 -20
  212. data/app/serializers/spree/events/digital_link_serializer.rb +0 -20
  213. data/app/serializers/spree/events/digital_serializer.rb +0 -18
  214. data/app/serializers/spree/events/export_serializer.rb +0 -22
  215. data/app/serializers/spree/events/gift_card_batch_serializer.rb +0 -24
  216. data/app/serializers/spree/events/gift_card_serializer.rb +0 -29
  217. data/app/serializers/spree/events/image_serializer.rb +0 -9
  218. data/app/serializers/spree/events/import_row_serializer.rb +0 -23
  219. data/app/serializers/spree/events/import_serializer.rb +0 -24
  220. data/app/serializers/spree/events/invitation_serializer.rb +0 -28
  221. data/app/serializers/spree/events/line_item_serializer.rb +0 -31
  222. data/app/serializers/spree/events/newsletter_subscriber_serializer.rb +0 -21
  223. data/app/serializers/spree/events/order_serializer.rb +0 -39
  224. data/app/serializers/spree/events/payment_serializer.rb +0 -24
  225. data/app/serializers/spree/events/post_category_serializer.rb +0 -20
  226. data/app/serializers/spree/events/post_serializer.rb +0 -26
  227. data/app/serializers/spree/events/price_serializer.rb +0 -22
  228. data/app/serializers/spree/events/product_serializer.rb +0 -24
  229. data/app/serializers/spree/events/promotion_serializer.rb +0 -32
  230. data/app/serializers/spree/events/refund_serializer.rb +0 -23
  231. data/app/serializers/spree/events/reimbursement_serializer.rb +0 -22
  232. data/app/serializers/spree/events/report_serializer.rb +0 -23
  233. data/app/serializers/spree/events/return_authorization_serializer.rb +0 -22
  234. data/app/serializers/spree/events/return_item_serializer.rb +0 -27
  235. data/app/serializers/spree/events/shipment_serializer.rb +0 -24
  236. data/app/serializers/spree/events/stock_item_serializer.rb +0 -22
  237. data/app/serializers/spree/events/stock_movement_serializer.rb +0 -22
  238. data/app/serializers/spree/events/stock_transfer_serializer.rb +0 -22
  239. data/app/serializers/spree/events/store_credit_serializer.rb +0 -30
  240. data/app/serializers/spree/events/user_serializer.rb +0 -18
  241. data/app/serializers/spree/events/variant_serializer.rb +0 -34
  242. data/app/serializers/spree/events/wished_item_serializer.rb +0 -20
  243. data/app/serializers/spree/events/wishlist_serializer.rb +0 -22
@@ -8,6 +8,8 @@ require_dependency 'spree/order/gift_card'
8
8
 
9
9
  module Spree
10
10
  class Order < Spree.base_class
11
+ has_prefix_id :or # Stripe: or_
12
+
11
13
  PAYMENT_STATES = %w(balance_due credit_owed failed paid void)
12
14
  SHIPMENT_STATES = %w(backorder canceled partial pending ready shipped)
13
15
  LINE_ITEM_REMOVABLE_STATES = %w(cart address delivery payment confirm resumed)
@@ -26,7 +28,6 @@ module Spree
26
28
  include Spree::Order::GiftCard
27
29
 
28
30
  include Spree::NumberIdentifier
29
- include Spree::NumberAsParam
30
31
  include Spree::SingleStoreResource
31
32
 
32
33
  publishes_lifecycle_events
@@ -98,7 +99,7 @@ module Spree
98
99
  acts_as_taggable_on :tags
99
100
  acts_as_taggable_tenant :store_id
100
101
 
101
- ASSOCIATED_USER_ATTRIBUTES = [:user_id, :email, :created_by_id, :bill_address_id, :ship_address_id]
102
+ ASSOCIATED_USER_ATTRIBUTES = [:user_id, :email, :bill_address_id, :ship_address_id]
102
103
 
103
104
  belongs_to :user, class_name: "::#{Spree.user_class}", optional: true, autosave: true
104
105
  belongs_to :created_by, class_name: "::#{Spree.admin_user_class}", optional: true
@@ -121,6 +122,7 @@ module Spree
121
122
  has_many :state_changes, as: :stateful, class_name: 'Spree::StateChange'
122
123
  has_many :line_items, -> { order(:created_at) }, inverse_of: :order, class_name: 'Spree::LineItem'
123
124
  has_many :payments, class_name: 'Spree::Payment'
125
+ has_many :payment_sessions, inverse_of: :order, class_name: 'Spree::PaymentSession'
124
126
  has_many :return_authorizations, inverse_of: :order, class_name: 'Spree::ReturnAuthorization'
125
127
  has_many :adjustments, -> { order(:created_at) }, as: :adjustable, class_name: 'Spree::Adjustment'
126
128
  end
@@ -183,6 +185,7 @@ module Spree
183
185
  validates :shipment_total, MONEY_VALIDATION
184
186
  validates :promo_total, NEGATIVE_MONEY_VALIDATION
185
187
  validates :total, MONEY_VALIDATION
188
+ validate :currency_must_be_supported_by_store
186
189
 
187
190
  delegate :update_totals, :persist_totals, to: :updater
188
191
  delegate :merge!, to: :merger
@@ -235,6 +238,36 @@ module Spree
235
238
  left_joins(:bill_address).where(arel_table[:email].lower.eq(query.downcase)).or(where(conditions.reduce(:or)))
236
239
  end
237
240
 
241
+ # Find an order by prefixed ID first, falling back to number, then integer id for backwards compatibility
242
+ # @param param [String] the prefixed ID, number, or integer id to search for
243
+ # @return [Spree::Order, nil] the found order or nil
244
+ def self.find_by_param(param)
245
+ return nil if param.blank?
246
+
247
+ # Try prefixed ID first (new format)
248
+ if param.to_s.include?('_')
249
+ decoded = decode_prefixed_id(param)
250
+ order = find_by(id: decoded) if decoded
251
+ return order if order
252
+ end
253
+
254
+ # Try number (legacy format)
255
+ order = find_by(number: param)
256
+ return order if order
257
+
258
+ # Fall back to id (numeric legacy format) - only if param looks like an integer
259
+ find_by(id: param) if param.to_s.match?(/\A\d+\z/)
260
+ end
261
+
262
+ # Find an order by prefixed ID first, falling back to number, then integer id for backwards compatibility
263
+ # Raises ActiveRecord::RecordNotFound if not found
264
+ # @param param [String] the prefixed ID, number, or integer id to search for
265
+ # @return [Spree::Order] the found order
266
+ # @raise [ActiveRecord::RecordNotFound] if order not found
267
+ def self.find_by_param!(param)
268
+ find_by_param(param) || raise(ActiveRecord::RecordNotFound.new("Couldn't find Order with param=#{param}"))
269
+ end
270
+
238
271
  # Use this method in other gems that wish to register their own custom logic
239
272
  # that should be called after Order#update
240
273
  def self.register_update_hook(hook)
@@ -275,9 +308,9 @@ module Spree
275
308
  # @return [Boolean]
276
309
  def order_refunded?
277
310
  return false if item_count.zero?
278
- return false if refunds_total.zero?
279
311
 
280
- payment_state.in?(%w[void failed]) || refunds_total == total_minus_store_credits - additional_tax_total.abs
312
+ (payment_state.in?(%w[void failed]) && refunds_total.positive?) ||
313
+ refunds_total == total_minus_store_credits - additional_tax_total.abs
281
314
  end
282
315
 
283
316
  def refunds_total
@@ -389,25 +422,13 @@ module Spree
389
422
  end
390
423
 
391
424
  # Associates the specified user with the order.
425
+ # Delegates to {Spree::Cart::Associate} service.
426
+ #
427
+ # @param user [Spree.user_class] the user to associate with the order
428
+ # @param override_email [Boolean] whether to override the order email with the user's email
429
+ # @return [Spree::ServiceModule::Result]
392
430
  def associate_user!(user, override_email = true)
393
- self.user = user
394
- self.email = user.email if override_email
395
- # we need to check if user is of admin user class to avoid mismatch type error
396
- # in a scenario where we have separate classes for admin and regular users
397
- self.created_by ||= user if user.is_a?(Spree.admin_user_class)
398
- self.bill_address ||= user.bill_address
399
- self.ship_address ||= user.ship_address
400
-
401
- changes = slice(*ASSOCIATED_USER_ATTRIBUTES)
402
-
403
- # immediately persist the changes we just made, but don't use save
404
- # since we might have an invalid address associated
405
- ActiveRecord::Base.connected_to(role: :writing) do
406
- self.class.unscoped.where(id: self).update_all(changes)
407
- end
408
-
409
- # Manually publish update event since update_all bypasses callbacks
410
- publish_event('order.updated') if changes.present?
431
+ Spree.cart_associate_service.call(guest_order: self, user: user, override_email: override_email)
411
432
  end
412
433
 
413
434
  def disassociate_user!
@@ -555,6 +576,10 @@ module Spree
555
576
  payments.valid.completed.size == payments.valid.size && payments.valid.sum(:amount) >= total
556
577
  end
557
578
 
579
+ def payment_methods
580
+ @payment_methods ||= store.payment_methods.active.available_on_front_end.select { |pm| pm.available_for_order?(self) }
581
+ end
582
+
558
583
  def available_payment_methods(store = nil)
559
584
  Spree::Deprecation.warn('`Order#available_payment_methods` is deprecated and will be removed in Spree 5.5. Use `collect_frontend_payment_methods` instead.')
560
585
 
@@ -752,30 +777,23 @@ module Spree
752
777
  !payments.risky.empty?
753
778
  end
754
779
 
780
+ # Cancels the order and records the canceler.
781
+ # Delegates to {Spree::Orders::Cancel} service.
782
+ #
783
+ # @param user [Spree.user_class, nil] the user who canceled the order
784
+ # @param canceled_at [Time, nil] the time of cancellation (defaults to current time)
785
+ # @return [Spree::ServiceModule::Result]
755
786
  def canceled_by(user, canceled_at = nil)
756
- canceled_at ||= Time.current
757
- changes = { canceler_id: user.id, canceled_at: canceled_at }
758
-
759
- transaction do
760
- update_columns(changes)
761
- cancel!
762
- end
763
-
764
- # Manually publish update event since update_columns bypasses callbacks
765
- publish_event('order.canceled')
787
+ Spree.order_cancel_service.call(order: self, canceler: user, canceled_at: canceled_at)
766
788
  end
767
789
 
768
- def approved_by(user)
769
- approved_at = Time.current
770
- changes = { approver_id: user.id, approved_at: approved_at }
771
-
772
- transaction do
773
- approve!
774
- update_columns(changes)
775
- end
776
-
777
- # Manually publish update event since update_columns bypasses callbacks
778
- publish_event('order.approved')
790
+ # Approves the order and records the approver.
791
+ # Delegates to {Spree::Orders::Approve} service.
792
+ #
793
+ # @param user [Spree.user_class, nil] the user who approved the order
794
+ # @return [Spree::ServiceModule::Result]
795
+ def approved_by(user = nil)
796
+ Spree.order_approve_service.call(order: self, approver: user)
779
797
  end
780
798
 
781
799
  def approved?
@@ -806,11 +824,12 @@ module Spree
806
824
  publish_event('order.updated')
807
825
  end
808
826
 
827
+ # Approves the order without recording an approver.
828
+ # Delegates to {Spree::Orders::Approve} service.
829
+ #
830
+ # @return [Spree::ServiceModule::Result]
809
831
  def approve!
810
- update_column(:considered_risky, false)
811
-
812
- # Manually publish update event since update_column bypasses callbacks
813
- publish_event('order.approved')
832
+ Spree.order_approve_service.call(order: self)
814
833
  end
815
834
 
816
835
  def tax_total
@@ -978,6 +997,15 @@ module Spree
978
997
  self.currency ||= store&.default_currency
979
998
  end
980
999
 
1000
+ def currency_must_be_supported_by_store
1001
+ return if currency.blank? || store.blank?
1002
+
1003
+ supported_codes = store.supported_currencies_list.map(&:iso_code)
1004
+ unless supported_codes.include?(currency)
1005
+ errors.add(:currency, Spree.t(:currency_not_supported_by_store))
1006
+ end
1007
+ end
1008
+
981
1009
  def collect_payment_methods
982
1010
  Spree::Deprecation.warn('`Order#collect_payment_methods` is deprecated and will be removed in Spree 5.5. Use `collect_frontend_payment_methods` instead.')
983
1011
 
@@ -2,9 +2,10 @@ require_dependency 'spree/payment/processing'
2
2
 
3
3
  module Spree
4
4
  class Payment < Spree.base_class
5
+ has_prefix_id :py # Stripe: py_
6
+
5
7
  include Spree::Core::NumberGenerator.new(prefix: 'P', letters: true, length: 7)
6
8
  include Spree::NumberIdentifier
7
- include Spree::NumberAsParam
8
9
  include Spree::Metafields
9
10
  include Spree::Metadata
10
11
  if defined?(Spree::Security::Payments)
@@ -35,6 +36,10 @@ module Spree
35
36
  has_many :capture_events, class_name: 'Spree::PaymentCaptureEvent'
36
37
  has_many :refunds, inverse_of: :payment
37
38
 
39
+ has_one :payment_session, class_name: 'Spree::PaymentSession',
40
+ foreign_key: :external_id,
41
+ primary_key: :response_code
42
+
38
43
  validates :payment_method, presence: true
39
44
  validates :source, presence: true, if: :source_required?
40
45
  validate :payment_method_available_for_order, on: :create
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class PaymentCaptureEvent < Spree.base_class
3
+ has_prefix_id :pce
4
+
3
5
  belongs_to :payment, class_name: 'Spree::Payment'
4
6
 
5
7
  def display_amount
@@ -1,9 +1,11 @@
1
1
  module Spree
2
2
  class PaymentMethod < Spree.base_class
3
+ has_prefix_id :pm # Stripe: pm_
4
+
3
5
  acts_as_paranoid
4
6
  acts_as_list
5
7
 
6
- include Spree::MultiStoreResource
8
+ include Spree::StoreScopedResource
7
9
  include Spree::Metafields
8
10
  include Spree::Metadata
9
11
  include Spree::DisplayOn
@@ -26,6 +28,8 @@ module Spree
26
28
  has_many :payments, class_name: 'Spree::Payment', inverse_of: :payment_method, dependent: :nullify
27
29
  has_many :credit_cards, class_name: 'Spree::CreditCard', dependent: :destroy # CCs are soft deleted
28
30
 
31
+ has_many :payment_sessions, class_name: 'Spree::PaymentSession', dependent: :destroy
32
+ has_many :payment_setup_sessions, class_name: 'Spree::PaymentSetupSession', dependent: :destroy
29
33
  has_many :gateway_customers, class_name: 'Spree::GatewayCustomer', dependent: :destroy
30
34
 
31
35
  def self.providers
@@ -45,6 +49,56 @@ module Spree
45
49
  raise ::NotImplementedError, 'You must implement payment_source_class method for this gateway.'
46
50
  end
47
51
 
52
+ # The class used for payment sessions with this payment method.
53
+ # Override in gateway subclasses to provide a provider-specific session class
54
+ # that inherits from Spree::PaymentSession (STI).
55
+ # nil means the payment method doesn't support payment sessions.
56
+ def payment_session_class
57
+ nil
58
+ end
59
+
60
+ # Creates a payment session via the provider.
61
+ # Override in gateway subclasses to implement provider-specific session creation.
62
+ def create_payment_session(order:, amount: nil, external_data: {})
63
+ raise ::NotImplementedError, 'You must implement create_payment_session method for this gateway.'
64
+ end
65
+
66
+ # Updates an existing payment session via the provider.
67
+ # Override in gateway subclasses to implement provider-specific session updates.
68
+ def update_payment_session(payment_session:, amount: nil, external_data: {})
69
+ raise ::NotImplementedError, 'You must implement update_payment_session method for this gateway.'
70
+ end
71
+
72
+ # Completes a payment session via the provider.
73
+ # Override in gateway subclasses to implement provider-specific session completion.
74
+ def complete_payment_session(payment_session:, params: {})
75
+ raise ::NotImplementedError, 'You must implement complete_payment_session method for this gateway.'
76
+ end
77
+
78
+ # Whether this payment method supports setup sessions (saving payment methods for future use).
79
+ # Override in gateway subclasses that support tokenization without a payment.
80
+ def setup_session_supported?
81
+ false
82
+ end
83
+
84
+ # The class used for payment setup sessions with this payment method.
85
+ # Override in gateway subclasses to provide a provider-specific session class.
86
+ def payment_setup_session_class
87
+ nil
88
+ end
89
+
90
+ # Creates a payment setup session via the provider for saving a payment method.
91
+ # Override in gateway subclasses to implement provider-specific setup session creation.
92
+ def create_payment_setup_session(customer:, external_data: {})
93
+ raise ::NotImplementedError, "#{self.class.name} does not implement #create_payment_setup_session"
94
+ end
95
+
96
+ # Completes a payment setup session via the provider.
97
+ # Override in gateway subclasses to implement provider-specific setup session completion.
98
+ def complete_payment_setup_session(setup_session:, params: {})
99
+ raise ::NotImplementedError, "#{self.class.name} does not implement #complete_payment_setup_session"
100
+ end
101
+
48
102
  def method_type
49
103
  type.demodulize.downcase
50
104
  end
@@ -73,6 +127,10 @@ module Spree
73
127
  true
74
128
  end
75
129
 
130
+ def session_required?
131
+ false
132
+ end
133
+
76
134
  def show_in_admin?
77
135
  true
78
136
  end
@@ -0,0 +1,109 @@
1
+ module Spree
2
+ class PaymentSession < Spree.base_class
3
+ has_prefix_id :ps
4
+
5
+ acts_as_paranoid
6
+
7
+ include Spree::Metafields
8
+
9
+ self.event_prefix = 'payment_session'
10
+
11
+ publishes_lifecycle_events
12
+
13
+ belongs_to :order, class_name: 'Spree::Order'
14
+ belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
15
+ belongs_to :customer, class_name: Spree.user_class.to_s, optional: true
16
+
17
+ has_one :payment, class_name: 'Spree::Payment',
18
+ foreign_key: :response_code,
19
+ primary_key: :external_id
20
+
21
+ validates :order, :payment_method, :external_id, :status, :currency, presence: true
22
+ validates :external_id, uniqueness: { scope: [:order_id, :payment_method_id] }
23
+ validates :amount, presence: true, numericality: { greater_than: 0 }
24
+
25
+ state_machine :status, initial: :pending do
26
+ state :pending
27
+ state :processing
28
+ state :completed
29
+ state :failed
30
+ state :canceled
31
+ state :expired
32
+
33
+ event :process do
34
+ transition pending: :processing
35
+ end
36
+
37
+ event :complete do
38
+ transition [:pending, :processing] => :completed
39
+ end
40
+
41
+ event :fail do
42
+ transition [:pending, :processing] => :failed
43
+ end
44
+
45
+ event :cancel do
46
+ transition [:pending, :processing] => :canceled
47
+ end
48
+
49
+ event :expire do
50
+ transition [:pending, :processing] => :expired
51
+ end
52
+
53
+ after_transition to: :processing, do: :publish_processing_event
54
+ after_transition to: :completed, do: :publish_completed_event
55
+ after_transition to: :failed, do: :publish_failed_event
56
+ after_transition to: :canceled, do: :publish_canceled_event
57
+ after_transition to: :expired, do: :publish_expired_event
58
+ end
59
+
60
+ scope :not_expired, -> { where('expires_at IS NULL OR expires_at > ?', Time.current) }
61
+ scope :active, -> { not_expired.where(status: %w[pending processing]) }
62
+
63
+ before_validation :set_defaults_from_order, on: :create
64
+
65
+ delegate :store, to: :order
66
+
67
+ def amount_in_cents
68
+ money.cents
69
+ end
70
+
71
+ def money
72
+ @money ||= Spree::Money.new(amount, currency: currency)
73
+ end
74
+
75
+ def expired?
76
+ expires_at.present? && expires_at <= Time.current
77
+ end
78
+
79
+ private
80
+
81
+ def publish_processing_event
82
+ publish_event('payment_session.processing')
83
+ end
84
+
85
+ def publish_completed_event
86
+ publish_event('payment_session.completed')
87
+ end
88
+
89
+ def publish_failed_event
90
+ publish_event('payment_session.failed')
91
+ end
92
+
93
+ def publish_canceled_event
94
+ publish_event('payment_session.canceled')
95
+ end
96
+
97
+ def publish_expired_event
98
+ publish_event('payment_session.expired')
99
+ end
100
+
101
+ def set_defaults_from_order
102
+ return unless order
103
+
104
+ self.amount ||= order.total_minus_store_credits if amount.blank? || amount.zero?
105
+ self.currency ||= order.currency
106
+ self.customer ||= order.user
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,4 @@
1
+ module Spree
2
+ class PaymentSessions::Bogus < PaymentSession
3
+ end
4
+ end
@@ -0,0 +1,79 @@
1
+ module Spree
2
+ class PaymentSetupSession < Spree.base_class
3
+ has_prefix_id :pss
4
+
5
+ acts_as_paranoid
6
+
7
+ include Spree::Metafields
8
+
9
+ self.event_prefix = 'payment_setup_session'
10
+
11
+ publishes_lifecycle_events
12
+
13
+ belongs_to :customer, class_name: Spree.user_class.to_s, optional: true
14
+ belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
15
+ belongs_to :payment_source, polymorphic: true, optional: true
16
+
17
+ validates :payment_method, :status, presence: true
18
+ validates :external_id, uniqueness: { scope: :payment_method_id }, allow_nil: true
19
+
20
+ state_machine :status, initial: :pending do
21
+ state :pending
22
+ state :processing
23
+ state :completed
24
+ state :failed
25
+ state :canceled
26
+ state :expired
27
+
28
+ event :process do
29
+ transition pending: :processing
30
+ end
31
+
32
+ event :complete do
33
+ transition [:pending, :processing] => :completed
34
+ end
35
+
36
+ event :fail do
37
+ transition [:pending, :processing] => :failed
38
+ end
39
+
40
+ event :cancel do
41
+ transition [:pending, :processing] => :canceled
42
+ end
43
+
44
+ event :expire do
45
+ transition [:pending, :processing] => :expired
46
+ end
47
+
48
+ after_transition to: :processing, do: :publish_processing_event
49
+ after_transition to: :completed, do: :publish_completed_event
50
+ after_transition to: :failed, do: :publish_failed_event
51
+ after_transition to: :canceled, do: :publish_canceled_event
52
+ after_transition to: :expired, do: :publish_expired_event
53
+ end
54
+
55
+ scope :active, -> { where(status: %w[pending processing]) }
56
+
57
+ private
58
+
59
+ def publish_processing_event
60
+ publish_event('payment_setup_session.processing')
61
+ end
62
+
63
+ def publish_completed_event
64
+ publish_event('payment_setup_session.completed')
65
+ end
66
+
67
+ def publish_failed_event
68
+ publish_event('payment_setup_session.failed')
69
+ end
70
+
71
+ def publish_canceled_event
72
+ publish_event('payment_setup_session.canceled')
73
+ end
74
+
75
+ def publish_expired_event
76
+ publish_event('payment_setup_session.expired')
77
+ end
78
+ end
79
+ end
@@ -1,6 +1,8 @@
1
1
  # This model is used to store payment sources for non-credit card payments, eg wallet, account, etc.
2
2
  module Spree
3
3
  class PaymentSource < Spree.base_class
4
+ has_prefix_id :ps
5
+
4
6
  include Spree::Metafields
5
7
  include Spree::Metadata
6
8
  include Spree::PaymentSourceConcern
@@ -39,6 +39,17 @@ module Spree
39
39
  !order.completed? && (order.user == user || order.token && token == order.token)
40
40
  end
41
41
 
42
+ # Line item management
43
+ can :create, Spree::LineItem do |line_item, token|
44
+ line_item.order.user == user || line_item.order.token && token == line_item.order.token
45
+ end
46
+ can :update, Spree::LineItem do |line_item, token|
47
+ !line_item.order.completed? && (line_item.order.user == user || line_item.order.token && token == line_item.order.token)
48
+ end
49
+ can :destroy, Spree::LineItem do |line_item, token|
50
+ !line_item.order.completed? && (line_item.order.user == user || line_item.order.token && token == line_item.order.token)
51
+ end
52
+
42
53
  # User account management - available to all users (including guests for their own record)
43
54
  can :create, Spree.user_class
44
55
  can [:show, :update, :destroy], Spree.user_class, id: user.id
@@ -49,6 +60,9 @@ module Spree
49
60
  # Credit card management
50
61
  can [:read, :destroy], Spree::CreditCard, user_id: user.id
51
62
 
63
+ # Gift card management - users can view their own gift cards
64
+ can :read, Spree::GiftCard, user_id: user.id
65
+
52
66
  # Wishlist management
53
67
  can :manage, Spree::Wishlist, user_id: user.id
54
68
  can :show, Spree::Wishlist do |wishlist|
@@ -60,6 +74,11 @@ module Spree
60
74
 
61
75
  # Invitation acceptance
62
76
  can :accept, Spree::Invitation, invitee_id: [user.id, nil], invitee_type: user.class.name, status: 'pending'
77
+
78
+ # Digital downloads - token-based access
79
+ can :show, Spree::DigitalLink do |digital_link, token|
80
+ digital_link.token == token
81
+ end
63
82
  end
64
83
  end
65
84
  end
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class Policy < Spree.base_class
3
+ has_prefix_id :pol
4
+
3
5
  extend FriendlyId
4
6
  include Spree::TranslatableResource
5
7
 
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class Post < Spree.base_class
3
+ has_prefix_id :post
4
+
3
5
  include Spree::SingleStoreResource
4
6
  include Spree::Metafields
5
7
  extend FriendlyId
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class PostCategory < Spree.base_class
3
+ has_prefix_id :pcat
4
+
3
5
  include Spree::SingleStoreResource
4
6
  include Spree::Metafields
5
7
  extend FriendlyId
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class Price < Spree.base_class
3
+ has_prefix_id :price
4
+
3
5
  include Spree::VatPriceCalculation
4
6
 
5
7
  publishes_lifecycle_events
@@ -38,13 +40,13 @@ module Spree
38
40
  scope :discounted, -> { where('compare_at_amount > amount') }
39
41
  scope :base_prices, -> { where(price_list_id: nil) }
40
42
  scope :for_price_list, ->(price_list) { where(price_list_id: price_list) }
41
- scope :for_products, ->(products, currency = nil) do
43
+ scope :for_products, lambda { |products, currency = nil|
42
44
  currency ||= Spree::Store.default.default_currency
43
45
 
44
46
  with_currency(currency).joins(:variant).where(
45
47
  Spree::Variant.table_name => { product_id: products }
46
48
  )
47
- end
49
+ }
48
50
 
49
51
  extend DisplayMoney
50
52
  money_methods :amount, :price, :compare_at_amount
@@ -64,6 +66,12 @@ module Spree
64
66
  self[:amount] = amount.blank? ? nil : Spree::LocalizedNumber.parse(amount)
65
67
  end
66
68
 
69
+ # Returns the amount in cents
70
+ # @return [Integer]
71
+ def amount_in_cents
72
+ display_amount&.amount_in_cents
73
+ end
74
+
67
75
  def compare_at_money
68
76
  Spree::Money.new(compare_at_amount || 0, currency: currency)
69
77
  end
@@ -74,6 +82,22 @@ module Spree
74
82
  self[:compare_at_amount] = calculated_value
75
83
  end
76
84
 
85
+ # Returns the compare at amount for display
86
+ # @return [Spree::Money, nil]
87
+ def display_compare_at_amount
88
+ return nil if compare_at_amount.nil?
89
+
90
+ Spree::Money.new(compare_at_amount, currency: currency)
91
+ end
92
+
93
+ # Returns the compare at amount in cents
94
+ # @return [Integer, nil]
95
+ def compare_at_amount_in_cents
96
+ return nil if compare_at_amount.nil?
97
+
98
+ display_compare_at_amount.amount_in_cents
99
+ end
100
+
77
101
  alias_attribute :price, :amount
78
102
  alias_method :price=, :amount=
79
103
  alias_attribute :compare_at_price, :compare_at_amount
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class PriceList < Spree.base_class
3
+ has_prefix_id :pl
4
+
3
5
  acts_as_paranoid
4
6
  acts_as_list scope: :store_id
5
7
 
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class PriceRule < Spree.base_class
3
+ has_prefix_id :prule
4
+
3
5
  belongs_to :price_list, class_name: 'Spree::PriceList', touch: true
4
6
 
5
7
  validates :type, :price_list,presence: true