spree_core 5.4.0.beta6 → 5.4.0.beta8

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/base_helper.rb +5 -7
  3. data/app/jobs/spree/payments/handle_webhook_job.rb +22 -0
  4. data/app/mailers/spree/base_mailer.rb +1 -1
  5. data/app/models/concerns/spree/user_methods.rb +16 -0
  6. data/app/models/spree/allowed_origin.rb +44 -0
  7. data/app/models/spree/asset.rb +121 -3
  8. data/app/models/spree/cart_promotion.rb +7 -0
  9. data/app/models/spree/checkout/default_requirements.rb +51 -0
  10. data/app/models/spree/checkout/registry.rb +112 -0
  11. data/app/models/spree/checkout/requirement.rb +49 -0
  12. data/app/models/spree/checkout/requirements.rb +56 -0
  13. data/app/models/spree/checkout/step.rb +52 -0
  14. data/app/models/spree/image/configuration/active_storage.rb +2 -14
  15. data/app/models/spree/image.rb +2 -78
  16. data/app/models/spree/legacy_user.rb +1 -0
  17. data/app/models/spree/line_item.rb +3 -3
  18. data/app/models/spree/order/checkout.rb +18 -0
  19. data/app/models/spree/order.rb +3 -0
  20. data/app/models/spree/order_promotion.rb +2 -0
  21. data/app/models/spree/payment_method.rb +34 -0
  22. data/app/models/spree/payment_session.rb +18 -0
  23. data/app/models/spree/product.rb +45 -34
  24. data/app/models/spree/shipment.rb +1 -0
  25. data/app/models/spree/store.rb +32 -0
  26. data/app/models/spree/variant.rb +21 -12
  27. data/app/services/spree/cart/create.rb +3 -30
  28. data/app/services/spree/carts/complete.rb +46 -0
  29. data/app/services/spree/carts/create.rb +32 -0
  30. data/app/services/spree/carts/update.rb +115 -0
  31. data/app/services/spree/{cart → carts}/upsert_items.rb +19 -23
  32. data/app/services/spree/payments/handle_webhook.rb +58 -0
  33. data/app/services/spree/seeds/all.rb +1 -0
  34. data/app/services/spree/seeds/allowed_origins.rb +14 -0
  35. data/app/views/spree/shared/_mailer_logo.html.erb +1 -1
  36. data/config/locales/en.yml +23 -2
  37. data/db/migrate/20260315000000_create_spree_allowed_origins.rb +14 -0
  38. data/db/migrate/20260315100000_add_product_media_support.rb +21 -0
  39. data/lib/spree/core/configuration.rb +3 -0
  40. data/lib/spree/core/dependencies.rb +4 -2
  41. data/lib/spree/core/version.rb +1 -1
  42. data/lib/spree/core.rb +1 -0
  43. data/lib/spree/permitted_attributes.rb +5 -1
  44. data/lib/spree/testing_support/factories/allowed_origin_factory.rb +8 -0
  45. data/lib/spree/testing_support/factories/asset_factory.rb +6 -9
  46. data/lib/spree/testing_support/factories/image_factory.rb +3 -1
  47. data/lib/spree/testing_support/factories/order_factory.rb +3 -0
  48. data/lib/tasks/images.rake +11 -11
  49. data/lib/tasks/products.rake +4 -2
  50. metadata +21 -6
  51. data/app/services/spree/orders/update.rb +0 -121
@@ -1,82 +1,6 @@
1
+ # Backward compatibility — all logic now lives in Spree::Asset.
2
+ # This class will be removed in Spree 6.0.
1
3
  module Spree
2
4
  class Image < Asset
3
- include Spree::Image::Configuration::ActiveStorage # legacy to be removed in Spree 6
4
- include Rails.application.routes.url_helpers
5
- include Spree::ImageMethods # legacy, will be removed in Spree 6
6
-
7
- validates :attachment, attached: true, content_type: Rails.application.config.active_storage.web_image_content_types
8
-
9
- after_commit :touch_product_variants, if: :should_touch_product_variants?, on: :update
10
- after_commit :update_variant_thumbnail, on: [:create, :destroy]
11
- after_commit :update_variant_thumbnail_on_reorder, on: :update, if: :saved_change_to_position?
12
- after_commit :update_variant_thumbnail_on_viewable_change, on: :update, if: :saved_change_to_viewable_id?
13
-
14
- after_create :increment_viewable_image_count
15
- after_destroy :decrement_viewable_image_count
16
-
17
- # In Rails 5.x class constants are being undefined/redefined during the code reloading process
18
- # in a rails development environment, after which the actual ruby objects stored in those class constants
19
- # are no longer equal (subclass == self) what causes error ActiveRecord::SubclassNotFound
20
- # Invalid single-table inheritance type: Spree::Image is not a subclass of Spree::Image.
21
- # The line below prevents the error.
22
- self.inheritance_column = nil
23
-
24
- # @deprecated
25
- def styles
26
- Spree::Deprecation.warn("Image#styles is deprecated and will be removed in Spree 6.0. Please use active storage variants with cdn_image_url")
27
-
28
- self.class.styles.map do |_, size|
29
- width, height = size.chop.split('x').map(&:to_i)
30
-
31
- {
32
- url: generate_url(size: size),
33
- size: size,
34
- width: width,
35
- height: height
36
- }
37
- end
38
- end
39
-
40
- private
41
-
42
- def touch_product_variants
43
- viewable.product.variants.touch_all
44
- end
45
-
46
- def should_touch_product_variants?
47
- viewable.is_a?(Spree::Variant) &&
48
- viewable.is_master? &&
49
- viewable.product.has_variants? &&
50
- saved_change_to_position?
51
- end
52
-
53
- def increment_viewable_image_count
54
- return unless viewable.is_a?(Spree::Variant)
55
-
56
- Spree::Variant.increment_counter(:image_count, viewable_id)
57
- Spree::Product.increment_counter(:total_image_count, viewable.product_id)
58
- end
59
-
60
- def decrement_viewable_image_count
61
- return unless viewable.is_a?(Spree::Variant)
62
-
63
- Spree::Variant.decrement_counter(:image_count, viewable_id)
64
- Spree::Product.decrement_counter(:total_image_count, viewable.product_id)
65
- end
66
-
67
- def update_variant_thumbnail
68
- return unless viewable.is_a?(Spree::Variant)
69
-
70
- viewable.update_thumbnail!
71
- viewable.product.update_thumbnail!
72
- end
73
-
74
- def update_variant_thumbnail_on_reorder
75
- update_variant_thumbnail
76
- end
77
-
78
- def update_variant_thumbnail_on_viewable_change
79
- update_variant_thumbnail
80
- end
81
5
  end
82
6
  end
@@ -12,6 +12,7 @@ module Spree
12
12
  attr_accessor :password, :password_confirmation
13
13
 
14
14
  validates :email, presence: true, uniqueness: { case_sensitive: false }
15
+ validates :password, confirmation: true, if: :password
15
16
 
16
17
  before_save :encrypt_password, if: :password
17
18
 
@@ -55,10 +55,10 @@ module Spree
55
55
  delegate :name, :description, :brand, :category, to: :product
56
56
 
57
57
  # Returns the thumbnail image for this line item
58
- # Prefers variant thumbnail, falls back to product thumbnail
59
- # @return [Spree::Image, nil]
58
+ # Prefers variant primary media, falls back to product primary media
59
+ # @return [Spree::Asset, nil]
60
60
  def thumbnail
61
- variant.thumbnail || product.thumbnail
61
+ variant.primary_media || product.primary_media
62
62
  end
63
63
  delegate :tax_zone, to: :order
64
64
  delegate :digital?, :can_supply?, to: :variant
@@ -227,6 +227,24 @@ module Spree
227
227
  self.checkout_steps.index(step).to_i
228
228
  end
229
229
 
230
+ # Customer-facing checkout step derived from the internal state machine state.
231
+ # Maps +'cart'+ to +'address'+ since cart is not a user-facing checkout step.
232
+ #
233
+ # @return [String] current checkout step name (e.g. +"address"+, +"delivery"+, +"payment"+)
234
+ def current_checkout_step
235
+ state == 'cart' ? 'address' : state
236
+ end
237
+
238
+ # Checkout steps that have already been completed, i.e. all steps before
239
+ # {#current_checkout_step}. Does not include +'complete'+.
240
+ #
241
+ # @return [Array<String>] completed step names in order
242
+ def completed_checkout_steps
243
+ steps = checkout_steps.reject { |s| s == 'complete' }
244
+ idx = steps.index(current_checkout_step) || 0
245
+ steps.first(idx)
246
+ end
247
+
230
248
  def can_go_to_state?(state)
231
249
  return false unless has_checkout_step?(self.state) && has_checkout_step?(state)
232
250
 
@@ -139,6 +139,7 @@ module Spree
139
139
  inverse_of: :order
140
140
 
141
141
  has_many :order_promotions, class_name: 'Spree::OrderPromotion'
142
+ has_many :cart_promotions, class_name: 'Spree::CartPromotion', foreign_key: :order_id
142
143
  has_many :promotions, through: :order_promotions, class_name: 'Spree::Promotion'
143
144
 
144
145
  has_many :shipments, class_name: 'Spree::Shipment', dependent: :destroy, inverse_of: :order do
@@ -148,6 +149,8 @@ module Spree
148
149
  end
149
150
  has_many :shipment_adjustments, through: :shipments, source: :adjustments
150
151
 
152
+ alias items line_items
153
+
151
154
  accepts_nested_attributes_for :line_items
152
155
  accepts_nested_attributes_for :bill_address
153
156
  accepts_nested_attributes_for :ship_address
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class OrderPromotion < Spree.base_class
3
+ has_prefix_id :oprom
4
+
3
5
  belongs_to :order, class_name: 'Spree::Order'
4
6
  belongs_to :promotion, class_name: 'Spree::Promotion'
5
7
 
@@ -71,10 +71,44 @@ module Spree
71
71
 
72
72
  # Completes a payment session via the provider.
73
73
  # Override in gateway subclasses to implement provider-specific session completion.
74
+ #
75
+ # Responsibilities:
76
+ # - Verify payment status with the provider
77
+ # - Create/update the Spree::Payment record
78
+ # - Patch order data from provider (e.g. wallet billing address)
79
+ # - Transition payment session to completed/failed
80
+ #
81
+ # Must NOT complete the order — that is handled by Carts::Complete
82
+ # (called by the frontend or by the webhook handler).
74
83
  def complete_payment_session(payment_session:, params: {})
75
84
  raise ::NotImplementedError, 'You must implement complete_payment_session method for this gateway.'
76
85
  end
77
86
 
87
+ # Parses an incoming webhook payload from the payment provider.
88
+ # Override in gateway subclasses to implement provider-specific webhook parsing.
89
+ #
90
+ # @param raw_body [String] the raw request body
91
+ # @param headers [Hash] the request headers
92
+ # @return [Hash, nil] normalized result or nil for unsupported events
93
+ # { action: :captured/:authorized/:failed/:canceled,
94
+ # payment_session: <Spree::PaymentSession>,
95
+ # metadata: {} }
96
+ # @raise [Spree::PaymentMethod::WebhookSignatureError] if signature is invalid
97
+ def parse_webhook_event(raw_body, headers)
98
+ raise ::NotImplementedError, 'You must implement parse_webhook_event method for this gateway.'
99
+ end
100
+
101
+ # Returns the webhook URL for this payment method.
102
+ # @return [String, nil]
103
+ def webhook_url
104
+ store = stores.first
105
+ return nil unless store
106
+
107
+ "#{store.url_or_custom_domain}/api/v3/webhooks/payments/#{prefixed_id}"
108
+ end
109
+
110
+ class WebhookSignatureError < StandardError; end
111
+
78
112
  # Whether this payment method supports setup sessions (saving payment methods for future use).
79
113
  # Override in gateway subclasses that support tokenization without a payment.
80
114
  def setup_session_supported?
@@ -76,6 +76,24 @@ module Spree
76
76
  expires_at.present? && expires_at <= Time.current
77
77
  end
78
78
 
79
+ # Creates or finds the Spree::Payment record for this session.
80
+ # Gateway subclasses can override this in their PaymentSession subclass
81
+ # to handle gateway-specific source creation (credit cards, wallets, etc).
82
+ #
83
+ # @param metadata [Hash] gateway-specific metadata
84
+ # @return [Spree::Payment] the payment record
85
+ def find_or_create_payment!(metadata = {})
86
+ return payment if payment.present?
87
+
88
+ order.payments.find_or_create_by!(
89
+ payment_method: payment_method,
90
+ response_code: external_id,
91
+ ) do |p|
92
+ p.amount = amount
93
+ p.skip_source_requirement = true
94
+ end
95
+ end
96
+
79
97
  private
80
98
 
81
99
  def publish_processing_event
@@ -114,10 +114,12 @@ module Spree
114
114
  has_many :orders, through: :line_items
115
115
  has_many :completed_orders, -> { reorder(nil).distinct.complete }, through: :line_items, source: :order
116
116
 
117
+ has_many :media, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: 'Spree::Asset'
118
+
117
119
  has_many :variant_images, -> { order(:position) }, source: :images, through: :variants_including_master
118
120
  has_many :variant_images_without_master, -> { order(:position) }, source: :images, through: :variants
119
121
 
120
- belongs_to :thumbnail, class_name: 'Spree::Image', optional: true
122
+ belongs_to :primary_media, class_name: 'Spree::Asset', optional: true, foreign_key: :primary_media_id
121
123
 
122
124
  has_many :option_value_variants, class_name: 'Spree::OptionValueVariant', through: :variants
123
125
  has_many :option_values, class_name: 'Spree::OptionValue', through: :variants
@@ -335,17 +337,26 @@ module Spree
335
337
  @default_variant_id ||= default_variant.id
336
338
  end
337
339
 
338
- # Returns true if any variant (including master) has images.
339
- # Uses loaded association when available, otherwise falls back to counter cache.
340
+ # Returns the product's media gallery.
341
+ # Uses product-level media if present, otherwise falls back to variant images.
342
+ # @return [ActiveRecord::Relation]
343
+ def gallery_media
344
+ return media if association(:media).loaded? ? media.any? : media.exists?
345
+
346
+ variant_images
347
+ end
348
+
349
+ # Returns true if the product has any media (product-level or variant-level).
350
+ # Uses counter cache for performance.
340
351
  # @return [Boolean]
341
- def has_variant_images?
352
+ def has_media?
342
353
  return variant_images.any? if association(:variant_images).loaded?
343
354
 
344
- total_image_count.positive?
355
+ media_count.positive?
345
356
  end
346
357
 
347
- # Alias for has_variant_images? for consistency with Variant#has_images?
348
- alias has_images? has_variant_images?
358
+ alias has_images? has_media?
359
+ alias has_variant_images? has_media?
349
360
 
350
361
  # Returns the variant that should be used for displaying images.
351
362
  # Priority: master > default_variant > first variant with images
@@ -354,47 +365,47 @@ module Spree
354
365
  @variant_for_images ||= find_variant_for_images
355
366
  end
356
367
 
357
- # Returns default Image for Product.
358
- # Uses cached thumbnail_id which is updated when images are added/removed/reordered.
359
- # @return [Spree::Image, nil]
368
+ # @deprecated Use #primary_media instead.
360
369
  def default_image
361
- thumbnail
370
+ Spree::Deprecation.warn('Spree::Product#default_image is deprecated and will be removed in Spree 6.0. Please use Spree::Product#primary_media instead.')
371
+ primary_media
362
372
  end
363
373
 
364
- # Backward compatibility for Spree 5.2 and earlier.
365
- # @deprecated Use Spree::Product#default_image instead.
374
+ # @deprecated Use #primary_media instead.
366
375
  def featured_image
367
- Spree::Deprecation.warn('Spree::Product#featured_image is deprecated and will be removed in Spree 5.5. Please use Spree::Product#default_image instead.')
376
+ Spree::Deprecation.warn('Spree::Product#featured_image is deprecated and will be removed in Spree 6.0. Please use Spree::Product#primary_media instead.')
377
+ primary_media
378
+ end
368
379
 
369
- default_image
380
+ # @deprecated Use #primary_media instead.
381
+ def primary_image
382
+ Spree::Deprecation.warn('Spree::Product#primary_image is deprecated and will be removed in Spree 6.0. Please use Spree::Product#primary_media instead.')
383
+ primary_media
370
384
  end
371
385
 
372
- # Returns secondary Image for Product (for hover effects).
373
- # @return [Spree::Image, nil]
386
+ # Returns secondary media for Product (for hover effects).
387
+ # @return [Spree::Asset, nil]
374
388
  def secondary_image
375
389
  variant_for_images&.secondary_image
376
390
  end
377
391
 
378
- # Alias for default_image for consistency.
379
- alias primary_image default_image
380
-
381
- # Returns the image count from the variant used for displaying images.
382
- # @return [Integer]
392
+ # @deprecated Use media_count instead
383
393
  def image_count
384
- variant_for_images&.image_count || 0
394
+ media_count
385
395
  end
386
396
 
387
- # Updates the thumbnail_id to the first image from variant_images.
388
- # Called when images are added, removed, or reordered on any variant.
397
+ # Updates primary_media_id to the first media item.
398
+ # Checks product-level media first, then falls back to variant images.
399
+ # Called when media is added, removed, or reordered.
389
400
  def update_thumbnail!
390
- first_image = variant_images.order(:position).first
391
- update_column(:thumbnail_id, first_image&.id)
401
+ first_media = media.order(:position).first || variant_images.order(:position).first
402
+ update_column(:primary_media_id, first_media&.id)
392
403
  end
393
404
 
394
- # Finds first variant with images using preloaded data when available.
405
+ # Finds first variant with media using preloaded data when available.
395
406
  # @return [Spree::Variant, nil]
396
407
  def find_variant_with_images
397
- return variants.find(&:has_images?) if variants.loaded?
408
+ return variants.find(&:has_media?) if variants.loaded?
398
409
 
399
410
  variants.joins(:images).first
400
411
  end
@@ -630,12 +641,12 @@ module Spree
630
641
 
631
642
  private
632
643
 
633
- # Determines which variant should be used for displaying images.
634
- # Priority: master > default_variant > first variant with images
644
+ # Determines which variant should be used for displaying media.
645
+ # Priority: master > default_variant > first variant with media
635
646
  def find_variant_for_images
636
- return master if master.has_images?
637
- return default_variant if has_variants? && default_variant.has_images?
638
- return find_variant_with_images if has_variant_images?
647
+ return master if master.has_media?
648
+ return default_variant if has_variants? && default_variant.has_media?
649
+ return find_variant_with_images if has_media?
639
650
 
640
651
  nil
641
652
  end
@@ -354,6 +354,7 @@ module Spree
354
354
  return if order.completed?
355
355
 
356
356
  update_amounts
357
+ reload # reload to pick up cost set by update_columns in update_amounts
357
358
  order.set_shipments_cost
358
359
  end
359
360
 
@@ -58,6 +58,7 @@ module Spree
58
58
  #
59
59
  # Associations
60
60
  #
61
+ has_many :carts, -> { incomplete }, class_name: 'Spree::Order', inverse_of: :store
61
62
  has_many :checkouts, -> { incomplete }, class_name: 'Spree::Order', inverse_of: :store
62
63
  has_many :orders, class_name: 'Spree::Order'
63
64
  has_many :line_items, through: :orders, class_name: 'Spree::LineItem'
@@ -108,6 +109,7 @@ module Spree
108
109
  has_many :customer_groups, class_name: 'Spree::CustomerGroup', dependent: :destroy, inverse_of: :store
109
110
 
110
111
  has_many :api_keys, class_name: 'Spree::ApiKey', dependent: :destroy
112
+ has_many :allowed_origins, class_name: 'Spree::AllowedOrigin', dependent: :destroy
111
113
 
112
114
  #
113
115
  # Validations
@@ -251,6 +253,36 @@ module Spree
251
253
  formatted_url
252
254
  end
253
255
 
256
+ # Returns the storefront origin URL for use in customer-facing emails and links.
257
+ # Uses the first allowed origin if configured, otherwise falls back to formatted_url.
258
+ #
259
+ # @return [String] e.g. "https://myshop.com"
260
+ def storefront_url
261
+ allowed_origins.order(:created_at).pick(:origin) || formatted_url
262
+ end
263
+
264
+ # Returns true if the given URL's origin matches one of the store's allowed origins.
265
+ # Comparison is port-less: only scheme + host are matched, so storing
266
+ # `http://localhost` will match `http://localhost:3000`, `http://localhost:4000`, etc.
267
+ #
268
+ # @param url [String] the full URL to check
269
+ # @return [Boolean]
270
+ def allowed_origin?(url)
271
+ return false if url.blank?
272
+
273
+ uri = URI.parse(url)
274
+ request_origin = "#{uri.scheme}://#{uri.host}"
275
+
276
+ allowed_origins.pluck(:origin).any? do |stored|
277
+ stored_uri = URI.parse(stored)
278
+ "#{stored_uri.scheme}://#{stored_uri.host}" == request_origin
279
+ rescue URI::InvalidURIError
280
+ false
281
+ end
282
+ rescue URI::InvalidURIError
283
+ false
284
+ end
285
+
254
286
  # Returns the states available for checkout for the store
255
287
  # @param country [Spree::Country] the country to get the states for
256
288
  # @return [Array<Spree::State>]
@@ -48,8 +48,8 @@ module Spree
48
48
  has_many :option_value_variants, class_name: 'Spree::OptionValueVariant'
49
49
  has_many :option_values, through: :option_value_variants, dependent: :destroy, class_name: 'Spree::OptionValue'
50
50
 
51
- has_many :images, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: 'Spree::Image'
52
- belongs_to :thumbnail, class_name: 'Spree::Image', optional: true
51
+ has_many :images, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: 'Spree::Asset'
52
+ belongs_to :primary_media, class_name: 'Spree::Asset', optional: true, foreign_key: :primary_media_id
53
53
 
54
54
  has_many :prices,
55
55
  class_name: 'Spree::Price',
@@ -279,26 +279,35 @@ module Spree
279
279
  is_master? ? name + ' - Master' : name + ' - ' + options_text
280
280
  end
281
281
 
282
- # Returns true if the variant has images.
282
+ # Returns the variant's media gallery.
283
+ # Currently returns direct images. In 6.0 will use variant_media join table.
284
+ # @return [ActiveRecord::Relation]
285
+ def gallery_media
286
+ images
287
+ end
288
+
289
+ # Returns true if the variant has media.
283
290
  # Uses loaded association when available, otherwise falls back to counter cache.
284
291
  # @return [Boolean]
285
- def has_images?
292
+ def has_media?
286
293
  return images.any? if images.loaded?
287
294
 
288
- image_count.positive?
295
+ media_count.positive?
289
296
  end
290
297
 
291
- # Returns default Image for Variant.
292
- # @return [Spree::Image, nil]
298
+ alias has_images? has_media?
299
+
300
+ # @deprecated Use #primary_media instead.
293
301
  def default_image
294
- thumbnail
302
+ Spree::Deprecation.warn('Spree::Variant#default_image is deprecated and will be removed in Spree 6.0. Please use Spree::Variant#primary_media instead.')
303
+ primary_media
295
304
  end
296
305
 
297
- # Updates the thumbnail_id to the first image by position.
298
- # Called when images are added, removed, or reordered.
306
+ # Updates primary_media_id to the first media item by position.
307
+ # Called when media is added, removed, or reordered.
299
308
  def update_thumbnail!
300
- first_image = images.order(:position).first
301
- update_column(:thumbnail_id, first_image&.id)
309
+ first_media = images.order(:position).first
310
+ update_column(:primary_media_id, first_media&.id)
302
311
  end
303
312
 
304
313
  # Returns first Image for Variant.
@@ -3,49 +3,22 @@ module Spree
3
3
  class Create
4
4
  prepend Spree::ServiceModule::Base
5
5
 
6
- # @param user [Spree.user_class, nil] the user to associate with the cart
7
- # @param store [Spree::Store] the store for the cart
8
- # @param currency [String, nil] ISO currency code, defaults to store's default currency
9
- # @param locale [String, nil] locale for the cart (e.g. 'en', 'fr'), defaults to Spree::Current.locale
10
- # @param public_metadata [Hash] public metadata for the order
11
- # @param private_metadata [Hash] private metadata for the order
12
- # @param order_params [Hash] additional order attributes
13
- # @param line_items [Array<Hash>] line items to add, each with :variant_id (prefixed) and :quantity
14
- # @return [Spree::ServiceModule::Result]
15
- def call(user:, store:, currency:, locale: nil, metadata: {}, public_metadata: {}, private_metadata: {}, order_params: {}, line_items: [])
6
+ def call(user:, store:, currency:, public_metadata: {}, private_metadata: {}, order_params: {})
16
7
  order_params ||= {}
17
- line_items ||= []
18
8
 
19
9
  # we cannot create an order without store
20
10
  return failure(:store_is_required) if store.nil?
21
11
 
22
- resolved_metadata = metadata.presence || private_metadata
23
-
24
12
  default_params = {
25
13
  user: user,
26
14
  currency: currency || store.default_currency,
27
- locale: locale || Spree::Current.locale,
28
15
  token: Spree::GenerateToken.new.call(Spree::Order),
29
16
  public_metadata: public_metadata.to_h,
30
- private_metadata: resolved_metadata.to_h
17
+ private_metadata: private_metadata.to_h
31
18
  }
32
19
 
33
- order = nil
34
-
35
- ApplicationRecord.transaction do
36
- order = store.orders.create!(default_params.merge(order_params))
37
-
38
- if line_items.present?
39
- result = Spree.cart_upsert_items_service.call(order: order, line_items: line_items)
40
- raise StandardError, result.error.to_s if result.failure?
41
- end
42
- end
43
-
20
+ order = store.orders.create!(default_params.merge(order_params))
44
21
  success(order)
45
- rescue ActiveRecord::RecordNotFound
46
- raise
47
- rescue StandardError => e
48
- failure(order, e.message)
49
22
  end
50
23
  end
51
24
  end
@@ -0,0 +1,46 @@
1
+ # In Spree 6 this servoice will complete the Spree::Cart, and create a Spree::Order
2
+ # created based on the contents of the cart.
3
+ module Spree
4
+ module Carts
5
+ class Complete
6
+ prepend Spree::ServiceModule::Base
7
+
8
+ # Completes the cart and creates a Spree::Order based on its contents.
9
+ # @return [Spree::Order]
10
+ def call(cart:)
11
+ return success(cart) if cart.completed?
12
+ return failure(cart, 'Order is canceled') if cart.canceled?
13
+
14
+ cart.with_lock do
15
+ process_payments!(cart) if cart.payment_required?
16
+
17
+ return failure(cart, cart.errors.full_messages.to_sentence) if cart.errors.any?
18
+
19
+ advance_to_complete!(cart)
20
+
21
+ if cart.reload.complete?
22
+ success(cart)
23
+ else
24
+ failure(cart, cart.errors.full_messages.to_sentence.presence || 'Could not complete checkout')
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def process_payments!(cart)
32
+ # If payments were already processed by the payment session
33
+ # (e.g. Stripe charged the card during complete_payment_session),
34
+ # skip re-processing. Only process unprocessed (checkout state) payments.
35
+ return if cart.payment_total >= cart.total
36
+ return if cart.payments.valid.any?(&:completed?) && cart.unprocessed_payments.empty?
37
+
38
+ cart.process_payments!
39
+ end
40
+
41
+ def advance_to_complete!(cart)
42
+ cart.next until cart.complete? || cart.errors.present?
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ module Spree
2
+ module Carts
3
+ class Create
4
+ prepend Spree::ServiceModule::Base
5
+
6
+ def call(params: {})
7
+ @params = params.to_h.deep_symbolize_keys
8
+
9
+ store = @params.delete(:store)
10
+ return failure(:store_is_required) if store.nil?
11
+
12
+ cart = store.carts.create!(
13
+ user: @params.delete(:user),
14
+ currency: @params.delete(:currency) || store.default_currency,
15
+ locale: @params.delete(:locale) || Spree::Current.locale
16
+ )
17
+
18
+ # Delegate all attribute/address/item processing to Carts::Update
19
+ if @params.present?
20
+ result = Spree::Carts::Update.call(cart: cart, params: @params)
21
+ return result if result.failure?
22
+ end
23
+
24
+ success(cart.reload)
25
+ rescue ActiveRecord::RecordNotFound
26
+ raise
27
+ rescue StandardError => e
28
+ failure(nil, e.message)
29
+ end
30
+ end
31
+ end
32
+ end