spree_core 5.4.1 → 5.4.3
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.
- checksums.yaml +4 -4
- data/app/jobs/spree/events/subscriber_job.rb +1 -1
- data/app/jobs/spree/imports/process_group_job.rb +45 -0
- data/app/jobs/spree/imports/process_rows_job.rb +51 -5
- data/app/models/concerns/spree/payment_source_concern.rb +21 -0
- data/app/models/concerns/spree/publishable.rb +1 -1
- data/app/models/spree/address.rb +1 -1
- data/app/models/spree/asset.rb +9 -9
- data/app/models/spree/event.rb +6 -6
- data/app/models/spree/export.rb +2 -2
- data/app/models/spree/exports/product_translations.rb +1 -1
- data/app/models/spree/gateway/bogus.rb +16 -7
- data/app/models/spree/import.rb +15 -0
- data/app/models/spree/import_row.rb +14 -2
- data/app/models/spree/imports/product_translations.rb +4 -0
- data/app/models/spree/imports/products.rb +5 -0
- data/app/models/spree/line_item.rb +1 -1
- data/app/models/spree/market.rb +25 -0
- data/app/models/spree/order_inventory.rb +24 -2
- data/app/models/spree/payment.rb +1 -0
- data/app/models/spree/payment_session.rb +5 -0
- data/app/models/spree/payment_setup_sessions/bogus.rb +4 -0
- data/app/models/spree/product.rb +5 -10
- data/app/models/spree/search_provider/base.rb +13 -1
- data/app/models/spree/search_provider/database.rb +44 -29
- data/app/models/spree/search_provider/filters_result.rb +5 -0
- data/app/models/spree/search_provider/meilisearch.rb +99 -82
- data/app/models/spree/search_provider/search_result.rb +1 -1
- data/app/models/spree/shipment.rb +10 -4
- data/app/models/spree/subscriber.rb +12 -12
- data/app/presenters/spree/csv/formula_sanitizer.rb +28 -0
- data/app/services/spree/credit_cards/destroy.rb +10 -26
- data/app/services/spree/gift_cards/apply.rb +5 -1
- data/app/services/spree/imports/row_processors/base.rb +19 -2
- data/app/services/spree/imports/row_processors/product_translation.rb +1 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +1 -1
- data/app/services/spree/payments/handle_webhook.rb +8 -9
- data/app/subscribers/spree/event_log_subscriber.rb +10 -8
- data/config/initializers/carmen.rb +23 -0
- data/config/locales/en.yml +8 -1
- data/db/migrate/20260424000001_add_unique_index_to_spree_payments_response_code.rb +51 -0
- data/db/migrate/20260424100000_add_processing_groups_to_spree_imports.rb +6 -0
- data/db/migrate/20260504103113_add_type_to_spree_payment_setup_sessions.rb +6 -0
- data/db/sample_data/orders.rb +1 -1
- data/lib/generators/spree/dummy/dummy_generator.rb +1 -1
- data/lib/spree/core/configuration.rb +3 -0
- data/lib/spree/core/engine.rb +3 -6
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/events/adapters/active_support_notifications.rb +1 -1
- data/lib/spree/events/adapters/base.rb +3 -3
- data/lib/spree/events/registry.rb +1 -1
- data/lib/spree/events.rb +7 -1
- data/lib/spree/testing_support/factories/payment_factory.rb +1 -1
- metadata +16 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8ff8a33cfaa4b9554bb3658995a2ed99d1715cb2365d3da012fce5dd2bbd518
|
|
4
|
+
data.tar.gz: fd19b449aa211be4cbec760db28e573064f9112d8c0922bf78517e51c0fc5002
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a392b8f08c1732f3b661a8758d37d63ac70e2aa7cf39d01dff5521c0e3267b1ee87ed190ea2cdddf914c7bf83462ebb6c7809736b5243610ab6d5e55c411300
|
|
7
|
+
data.tar.gz: 841e2707d2535a628a1cc2c743d594276a1cda0a969dd243ed8d82329f53f5d96bda3433517e40d0e261ef79159a7faae9cb57ebb1c5975b6bd2ea00f17d7641
|
|
@@ -10,7 +10,7 @@ module Spree
|
|
|
10
10
|
# @example Direct usage (typically called by the adapter)
|
|
11
11
|
# Spree::Events::SubscriberJob.perform_later(
|
|
12
12
|
# 'MySubscriber',
|
|
13
|
-
# { name: 'order.
|
|
13
|
+
# { name: 'order.completed', payload: {...}, ... }
|
|
14
14
|
# )
|
|
15
15
|
#
|
|
16
16
|
class SubscriberJob < Spree::BaseJob
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Imports
|
|
3
|
+
class ProcessGroupJob < Spree::BaseJob
|
|
4
|
+
queue_as Spree.queues.imports
|
|
5
|
+
|
|
6
|
+
def perform(import_id, row_ids)
|
|
7
|
+
import = Spree::Import.find(import_id)
|
|
8
|
+
Spree::Current.store = import.store
|
|
9
|
+
|
|
10
|
+
mappings = import.mappings.mapped.to_a
|
|
11
|
+
schema_fields = import.schema_fields
|
|
12
|
+
large = import.large_import?
|
|
13
|
+
rows = import.rows.where(id: row_ids).order(:row_number)
|
|
14
|
+
|
|
15
|
+
if large
|
|
16
|
+
Spree::Events.disable do
|
|
17
|
+
rows.each { |row| row.bulk_process!(mappings: mappings, schema_fields: schema_fields) }
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
rows.each do |row|
|
|
21
|
+
row.process!(mappings: mappings, schema_fields: schema_fields)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
check_import_completion(import, large)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def check_import_completion(import, large)
|
|
31
|
+
Spree::Import.where(id: import.id).update_all(
|
|
32
|
+
'completed_groups_count = COALESCE(completed_groups_count, 0) + 1'
|
|
33
|
+
)
|
|
34
|
+
import.reload
|
|
35
|
+
|
|
36
|
+
if import.completed_groups_count >= import.processing_groups_count
|
|
37
|
+
# Guard against concurrent workers both reaching this point
|
|
38
|
+
import.complete! if import.status == 'processing'
|
|
39
|
+
elsif large && (import.completed_groups_count % 10).zero?
|
|
40
|
+
import.publish_event('import.progress')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -3,16 +3,62 @@ module Spree
|
|
|
3
3
|
class ProcessRowsJob < Spree::BaseJob
|
|
4
4
|
queue_as Spree.queues.imports
|
|
5
5
|
|
|
6
|
+
BATCH_SIZE = 100
|
|
7
|
+
|
|
6
8
|
def perform(import_id)
|
|
7
9
|
import = Spree::Import.find(import_id)
|
|
10
|
+
dispatch_groups(import)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def dispatch_groups(import)
|
|
16
|
+
group_field = import.group_column
|
|
17
|
+
group_mapping = group_field && import.mappings.mapped.find_by(schema_field: group_field)
|
|
18
|
+
file_column = group_mapping&.file_column
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
if file_column
|
|
21
|
+
dispatch_grouped(import, file_column)
|
|
22
|
+
else
|
|
23
|
+
dispatch_batched(import)
|
|
12
24
|
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dispatch_grouped(import, file_column)
|
|
28
|
+
groups = Hash.new { |h, k| h[k] = [] }
|
|
29
|
+
|
|
30
|
+
import.rows.pending_and_failed.order(:row_number).pluck(:id, :data).each do |id, data|
|
|
31
|
+
parsed = JSON.parse(data)
|
|
32
|
+
key = parsed[file_column].to_s.strip.downcase.presence || '__ungrouped__'
|
|
33
|
+
groups[key] << id
|
|
34
|
+
rescue JSON::ParserError
|
|
35
|
+
groups['__ungrouped__'] << id
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Set count before enqueuing so workers can't complete prematurely
|
|
39
|
+
import.update_columns(
|
|
40
|
+
processing_groups_count: groups.size,
|
|
41
|
+
completed_groups_count: 0,
|
|
42
|
+
updated_at: Time.current
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
groups.each_value { |row_ids| ProcessGroupJob.perform_later(import.id, row_ids) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def dispatch_batched(import)
|
|
49
|
+
# Count first, then enqueue — prevents premature completion
|
|
50
|
+
row_id_batches = import.rows.pending_and_failed.order(:row_number)
|
|
51
|
+
.pluck(:id)
|
|
52
|
+
.each_slice(BATCH_SIZE)
|
|
53
|
+
.to_a
|
|
54
|
+
|
|
55
|
+
import.update_columns(
|
|
56
|
+
processing_groups_count: row_id_batches.size,
|
|
57
|
+
completed_groups_count: 0,
|
|
58
|
+
updated_at: Time.current
|
|
59
|
+
)
|
|
13
60
|
|
|
14
|
-
|
|
15
|
-
import.complete!
|
|
61
|
+
row_id_batches.each { |row_ids| ProcessGroupJob.perform_later(import.id, row_ids) }
|
|
16
62
|
end
|
|
17
63
|
end
|
|
18
64
|
end
|
|
@@ -2,6 +2,10 @@ module Spree
|
|
|
2
2
|
module PaymentSourceConcern
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
|
+
included do
|
|
6
|
+
before_destroy :cleanup_payments_on_incomplete_orders
|
|
7
|
+
end
|
|
8
|
+
|
|
5
9
|
# Available actions for the payment source.
|
|
6
10
|
# @return [Array<String>]
|
|
7
11
|
def actions
|
|
@@ -35,5 +39,22 @@ module Spree
|
|
|
35
39
|
def has_payment_profile?
|
|
36
40
|
gateway_customer_profile_id.present? || gateway_payment_profile_id.present?
|
|
37
41
|
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Cleans up payments on incomplete orders before the source is destroyed.
|
|
46
|
+
# Invalidates checkout-state payments and voids non-checkout payments.
|
|
47
|
+
# Skips payments whose payment_method was already nullified (e.g. when
|
|
48
|
+
# the payment method itself is being destroyed).
|
|
49
|
+
def cleanup_payments_on_incomplete_orders
|
|
50
|
+
incomplete_payments = Spree::Payment.valid
|
|
51
|
+
.where(source: self)
|
|
52
|
+
.where.not(payment_method_id: nil)
|
|
53
|
+
.joins(:order)
|
|
54
|
+
.merge(Spree::Order.incomplete)
|
|
55
|
+
|
|
56
|
+
incomplete_payments.checkout.each(&:invalidate!)
|
|
57
|
+
incomplete_payments.where.not(state: :checkout).each(&:void!)
|
|
58
|
+
end
|
|
38
59
|
end
|
|
39
60
|
end
|
|
@@ -122,7 +122,7 @@ module Spree
|
|
|
122
122
|
|
|
123
123
|
# Publish an event with this model's data
|
|
124
124
|
#
|
|
125
|
-
# @param event_name [String] The event name (e.g., 'order.
|
|
125
|
+
# @param event_name [String] The event name (e.g., 'order.completed')
|
|
126
126
|
# @param payload [Hash, nil] Custom payload (defaults to event_payload)
|
|
127
127
|
# @param metadata [Hash] Additional metadata
|
|
128
128
|
# @return [Spree::Event] The published event
|
data/app/models/spree/address.rb
CHANGED
|
@@ -79,7 +79,7 @@ module Spree
|
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
delegate :name, :iso3, :iso, :iso_name, to: :country, prefix: true
|
|
82
|
+
delegate :name, :iso3, :iso, :iso_name, to: :country, prefix: true, allow_nil: true
|
|
83
83
|
delegate :abbr, to: :state, prefix: true, allow_nil: true
|
|
84
84
|
|
|
85
85
|
alias_attribute :postal_code, :zipcode
|
data/app/models/spree/asset.rb
CHANGED
|
@@ -153,31 +153,31 @@ module Spree
|
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
def increment_viewable_media_count
|
|
156
|
-
case
|
|
157
|
-
when
|
|
156
|
+
case viewable
|
|
157
|
+
when Spree::Variant
|
|
158
158
|
Spree::Variant.increment_counter(:media_count, viewable_id)
|
|
159
159
|
Spree::Product.increment_counter(:media_count, viewable.product_id)
|
|
160
|
-
when
|
|
160
|
+
when Spree::Product
|
|
161
161
|
Spree::Product.increment_counter(:media_count, viewable_id)
|
|
162
162
|
end
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def decrement_viewable_media_count
|
|
166
|
-
case
|
|
167
|
-
when
|
|
166
|
+
case viewable
|
|
167
|
+
when Spree::Variant
|
|
168
168
|
Spree::Variant.decrement_counter(:media_count, viewable_id)
|
|
169
169
|
Spree::Product.decrement_counter(:media_count, viewable.product_id)
|
|
170
|
-
when
|
|
170
|
+
when Spree::Product
|
|
171
171
|
Spree::Product.decrement_counter(:media_count, viewable_id)
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|
|
175
175
|
def update_viewable_thumbnail
|
|
176
|
-
case
|
|
177
|
-
when
|
|
176
|
+
case viewable
|
|
177
|
+
when Spree::Variant
|
|
178
178
|
viewable.update_thumbnail!
|
|
179
179
|
viewable.product.update_thumbnail!
|
|
180
|
-
when
|
|
180
|
+
when Spree::Product
|
|
181
181
|
viewable.update_thumbnail!
|
|
182
182
|
end
|
|
183
183
|
end
|
data/app/models/spree/event.rb
CHANGED
|
@@ -6,20 +6,20 @@ module Spree
|
|
|
6
6
|
# Events are immutable objects that carry information about something
|
|
7
7
|
# that happened in the system. They contain:
|
|
8
8
|
# - An id (UUID)
|
|
9
|
-
# - A name (e.g., 'order.
|
|
9
|
+
# - A name (e.g., 'order.completed', 'product.created')
|
|
10
10
|
# - A store_id (the store where the event originated)
|
|
11
11
|
# - A payload (serialized data about the event)
|
|
12
12
|
# - Metadata (contextual information like spree_version)
|
|
13
13
|
#
|
|
14
14
|
# @example Creating an event
|
|
15
15
|
# event = Spree::Event.new(
|
|
16
|
-
# name: 'order.
|
|
16
|
+
# name: 'order.completed',
|
|
17
17
|
# payload: order.serializable_hash
|
|
18
18
|
# )
|
|
19
19
|
#
|
|
20
20
|
# @example Accessing event data
|
|
21
21
|
# event.id # => "550e8400-e29b-41d4-a716-446655440000"
|
|
22
|
-
# event.name # => 'order.
|
|
22
|
+
# event.name # => 'order.completed'
|
|
23
23
|
# event.store_id # => 1
|
|
24
24
|
# event.payload # => { 'id' => 1, 'number' => 'R123456' }
|
|
25
25
|
# event.created_at # => 2024-01-15 10:30:00 UTC
|
|
@@ -61,19 +61,19 @@ module Spree
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Returns the resource type from the event name
|
|
64
|
-
# @return [String] The resource type (e.g., 'order' from 'order.
|
|
64
|
+
# @return [String] The resource type (e.g., 'order' from 'order.completed')
|
|
65
65
|
def resource_type
|
|
66
66
|
@resource_type ||= name.to_s.split('.').first
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# Returns the action from the event name
|
|
70
|
-
# @return [String] The action (e.g., '
|
|
70
|
+
# @return [String] The action (e.g., 'completed' from 'order.completed')
|
|
71
71
|
def action
|
|
72
72
|
@action ||= name.to_s.split('.').drop(1).join('.')
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
# Checks if the event matches a pattern
|
|
76
|
-
# Supports wildcards: 'order.*' matches 'order.
|
|
76
|
+
# Supports wildcards: 'order.*' matches 'order.completed', 'order.canceled'
|
|
77
77
|
# @param pattern [String] The pattern to match against
|
|
78
78
|
# @return [Boolean]
|
|
79
79
|
def matches?(pattern)
|
data/app/models/spree/export.rb
CHANGED
|
@@ -83,10 +83,10 @@ module Spree
|
|
|
83
83
|
batch.each do |record|
|
|
84
84
|
if multi_line_csv?
|
|
85
85
|
record.to_csv(store).each do |line|
|
|
86
|
-
csv << line
|
|
86
|
+
csv << Spree::CSV::FormulaSanitizer.row(line)
|
|
87
87
|
end
|
|
88
88
|
else
|
|
89
|
-
csv << record.to_csv(store)
|
|
89
|
+
csv << Spree::CSV::FormulaSanitizer.row(record.to_csv(store))
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
end
|
|
@@ -32,7 +32,7 @@ module Spree
|
|
|
32
32
|
def authorize(_money, credit_card, _options = {})
|
|
33
33
|
profile_id = credit_card.gateway_customer_profile_id
|
|
34
34
|
if VALID_CCS.include?(credit_card.number) || (profile_id&.starts_with?('BGS-'))
|
|
35
|
-
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization:
|
|
35
|
+
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization: generate_authorization, avs_result: { code: 'D' })
|
|
36
36
|
else
|
|
37
37
|
Spree::PaymentResponse.new(false, 'Bogus Gateway: Forced failure', { message: 'Bogus Gateway: Forced failure' }, test: true)
|
|
38
38
|
end
|
|
@@ -41,18 +41,18 @@ module Spree
|
|
|
41
41
|
def purchase(_money, credit_card, _options = {})
|
|
42
42
|
profile_id = credit_card.gateway_customer_profile_id
|
|
43
43
|
if VALID_CCS.include?(credit_card.number) || (profile_id&.starts_with?('BGS-'))
|
|
44
|
-
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization:
|
|
44
|
+
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization: generate_authorization, avs_result: { code: 'M' })
|
|
45
45
|
else
|
|
46
46
|
Spree::PaymentResponse.new(false, 'Bogus Gateway: Forced failure', message: 'Bogus Gateway: Forced failure', test: true)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def credit(_money, _credit_card, _response_code, _options = {})
|
|
51
|
-
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization:
|
|
51
|
+
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization: generate_authorization)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def capture(_money, authorization, _gateway_options)
|
|
55
|
-
if authorization
|
|
55
|
+
if authorization&.start_with?('BGS-')
|
|
56
56
|
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true)
|
|
57
57
|
else
|
|
58
58
|
Spree::PaymentResponse.new(false, 'Bogus Gateway: Forced failure', error: 'Bogus Gateway: Forced failure', test: true)
|
|
@@ -60,11 +60,11 @@ module Spree
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def void(_response_code, _credit_card, _options = {})
|
|
63
|
-
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization:
|
|
63
|
+
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization: "void-#{generate_authorization}")
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def cancel(_response_code, _payment = nil)
|
|
67
|
-
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization:
|
|
67
|
+
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization: generate_authorization)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def test?
|
|
@@ -120,9 +120,14 @@ module Spree
|
|
|
120
120
|
true
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
def payment_setup_session_class
|
|
124
|
+
PaymentSetupSessions::Bogus
|
|
125
|
+
end
|
|
126
|
+
|
|
123
127
|
def create_payment_setup_session(customer:, external_data: {})
|
|
124
|
-
|
|
128
|
+
payment_setup_session_class.create(
|
|
125
129
|
customer: customer,
|
|
130
|
+
payment_method: self,
|
|
126
131
|
status: 'pending',
|
|
127
132
|
external_id: "bogus_seti_#{SecureRandom.hex(12)}",
|
|
128
133
|
external_client_secret: "bogus_seti_secret_#{SecureRandom.hex(8)}",
|
|
@@ -150,6 +155,10 @@ module Spree
|
|
|
150
155
|
|
|
151
156
|
private
|
|
152
157
|
|
|
158
|
+
def generate_authorization
|
|
159
|
+
"BGS-#{SecureRandom.hex(6)}"
|
|
160
|
+
end
|
|
161
|
+
|
|
153
162
|
def generate_profile_id(success)
|
|
154
163
|
record = true
|
|
155
164
|
prefix = success ? 'BGS' : 'FAIL'
|
data/app/models/spree/import.rb
CHANGED
|
@@ -76,6 +76,21 @@ module Spree
|
|
|
76
76
|
#
|
|
77
77
|
preference :delimiter, :string, default: ','
|
|
78
78
|
|
|
79
|
+
# Returns true if the import has more rows than the large import threshold.
|
|
80
|
+
# Large imports skip per-row UI broadcasts and use bulk processing.
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def large_import?
|
|
83
|
+
rows_count >= Spree::Config[:large_import_threshold]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns the schema field name used to group rows for parallel processing.
|
|
87
|
+
# Rows sharing the same value in this field are processed together in one job.
|
|
88
|
+
# Returns nil for imports where rows are independent (default — batched in chunks).
|
|
89
|
+
# @return [String, nil]
|
|
90
|
+
def group_column
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
79
94
|
# Returns true if the import is in mapping state
|
|
80
95
|
# @return [Boolean]
|
|
81
96
|
def mapping?
|
|
@@ -77,9 +77,9 @@ module Spree
|
|
|
77
77
|
data_json[mapping.file_column]
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
def process!
|
|
80
|
+
def process!(mappings: nil, schema_fields: nil)
|
|
81
81
|
start_processing!
|
|
82
|
-
self.item = import.row_processor_class.new(self).process!
|
|
82
|
+
self.item = import.row_processor_class.new(self, mappings: mappings, schema_fields: schema_fields).process!
|
|
83
83
|
complete!
|
|
84
84
|
rescue StandardError => e
|
|
85
85
|
Rails.error.report(e, handled: true, context: { import_row_id: id }, source: 'spree.core')
|
|
@@ -87,6 +87,18 @@ module Spree
|
|
|
87
87
|
fail!
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
# Bulk processing mode for large imports.
|
|
91
|
+
# Uses update_columns to skip callbacks, validations, and event publishing.
|
|
92
|
+
def bulk_process!(mappings:, schema_fields:)
|
|
93
|
+
update_columns(status: 'processing', updated_at: Time.current)
|
|
94
|
+
processor = import.row_processor_class.new(self, mappings: mappings, schema_fields: schema_fields)
|
|
95
|
+
self.item = processor.process!
|
|
96
|
+
update_columns(status: 'completed', item_type: item.class.name, item_id: item.id, updated_at: Time.current)
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
Rails.error.report(e, handled: true, context: { import_row_id: id }, source: 'spree.core')
|
|
99
|
+
update_columns(status: 'failed', validation_errors: e.message, updated_at: Time.current)
|
|
100
|
+
end
|
|
101
|
+
|
|
90
102
|
def publish_import_row_completed_event
|
|
91
103
|
publish_event('import_row.completed')
|
|
92
104
|
end
|
|
@@ -293,7 +293,7 @@ module Spree
|
|
|
293
293
|
end
|
|
294
294
|
|
|
295
295
|
def verify_order_inventory_before_destroy
|
|
296
|
-
Spree::OrderInventory.new(order, self).verify(target_shipment)
|
|
296
|
+
Spree::OrderInventory.new(order, self).verify(target_shipment, removing: true)
|
|
297
297
|
end
|
|
298
298
|
|
|
299
299
|
def update_adjustments
|
data/app/models/spree/market.rb
CHANGED
|
@@ -28,6 +28,7 @@ module Spree
|
|
|
28
28
|
# Callbacks
|
|
29
29
|
#
|
|
30
30
|
before_save :ensure_single_default
|
|
31
|
+
before_destroy :ensure_can_be_deleted
|
|
31
32
|
|
|
32
33
|
#
|
|
33
34
|
# Scopes
|
|
@@ -80,12 +81,36 @@ module Spree
|
|
|
80
81
|
@supported_locales_list ||= (supported_locales.to_s.split(',').map(&:strip) << default_locale).compact.uniq.sort
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
# Returns true when the market is safe to delete. A market cannot be deleted
|
|
85
|
+
# if it is the default market or the only market in the store, since
|
|
86
|
+
# Spree::Current.currency would have no fallback.
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def can_be_deleted?
|
|
90
|
+
!default? && !last_in_store?
|
|
91
|
+
end
|
|
92
|
+
|
|
83
93
|
private
|
|
84
94
|
|
|
95
|
+
def last_in_store?
|
|
96
|
+
!self.class.where(store_id: store_id).where.not(id: id).exists?
|
|
97
|
+
end
|
|
98
|
+
|
|
85
99
|
def ensure_single_default
|
|
86
100
|
return unless default? && default_changed?
|
|
87
101
|
|
|
88
102
|
self.class.where(store_id: store_id, default: true).where.not(id: id).update_all(default: false)
|
|
89
103
|
end
|
|
104
|
+
|
|
105
|
+
def ensure_can_be_deleted
|
|
106
|
+
return if can_be_deleted?
|
|
107
|
+
|
|
108
|
+
if default?
|
|
109
|
+
errors.add(:base, :cannot_destroy_default_market)
|
|
110
|
+
else
|
|
111
|
+
errors.add(:base, :cannot_destroy_last_market)
|
|
112
|
+
end
|
|
113
|
+
throw(:abort)
|
|
114
|
+
end
|
|
90
115
|
end
|
|
91
116
|
end
|
|
@@ -17,13 +17,23 @@ module Spree
|
|
|
17
17
|
# In case shipment is passed the stock location should only unstock or
|
|
18
18
|
# restock items if the order is completed. That is so because stock items
|
|
19
19
|
# are always unstocked when the order is completed through +shipment.finalize+
|
|
20
|
-
def verify(shipment = nil, is_updated: false)
|
|
20
|
+
def verify(shipment = nil, is_updated: false, removing: false)
|
|
21
21
|
return unless order.completed? || shipment.present?
|
|
22
22
|
|
|
23
23
|
units_count = inventory_units.reload.sum(&:quantity)
|
|
24
24
|
line_item_changed = is_updated ? !line_item.saved_changes? : !line_item.changed?
|
|
25
25
|
|
|
26
|
-
if
|
|
26
|
+
if removing
|
|
27
|
+
# When the line item is being destroyed, only remove existing inventory.
|
|
28
|
+
# Adding here would create units that the LineItem `dependent: :destroy`
|
|
29
|
+
# cascade can't see (set_up_inventory writes through shipment.inventory_units,
|
|
30
|
+
# leaving line_item.inventory_units stale), producing an orphaned unit.
|
|
31
|
+
#
|
|
32
|
+
# Bypass `remove` because it routes through `set_quantity_to_remove` which
|
|
33
|
+
# assumes a quantity-change scenario; here we want to drain everything tied
|
|
34
|
+
# to this line item regardless of `line_item.quantity`.
|
|
35
|
+
remove_all_units(units_count, shipment) if units_count.positive?
|
|
36
|
+
elsif units_count < line_item.quantity
|
|
27
37
|
quantity = line_item.quantity - units_count
|
|
28
38
|
|
|
29
39
|
shipment ||= determine_target_shipment
|
|
@@ -49,6 +59,18 @@ module Spree
|
|
|
49
59
|
end
|
|
50
60
|
end
|
|
51
61
|
|
|
62
|
+
def remove_all_units(quantity, target_shipment = nil)
|
|
63
|
+
if target_shipment.present?
|
|
64
|
+
remove_from_shipment(target_shipment, quantity)
|
|
65
|
+
else
|
|
66
|
+
order.shipments.each do |shipment|
|
|
67
|
+
break if quantity.zero?
|
|
68
|
+
|
|
69
|
+
quantity -= remove_from_shipment(shipment, quantity)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
52
74
|
def set_quantity_to_remove(units_count)
|
|
53
75
|
if (units_count - line_item.quantity).zero?
|
|
54
76
|
line_item.quantity
|
data/app/models/spree/payment.rb
CHANGED
|
@@ -42,6 +42,7 @@ module Spree
|
|
|
42
42
|
|
|
43
43
|
validates :payment_method, presence: true
|
|
44
44
|
validates :source, presence: true, if: :source_required?
|
|
45
|
+
validates :response_code, uniqueness: { scope: [:order_id, :payment_method_id] }, allow_nil: true
|
|
45
46
|
validate :payment_method_available_for_order, on: :create
|
|
46
47
|
|
|
47
48
|
before_validation :validate_source
|
data/app/models/spree/product.rb
CHANGED
|
@@ -249,16 +249,11 @@ module Spree
|
|
|
249
249
|
|
|
250
250
|
products_to_auto_match_ids = store.products.not_deleted.not_archived.where(id: product_ids).ids
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
auto_match_taxons_jobs = products_to_auto_match_ids.map do |product_id|
|
|
255
|
-
Spree::Products::AutoMatchTaxonsJob.new(product_id)
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
ActiveJob.perform_all_later(auto_match_taxons_jobs)
|
|
259
|
-
else
|
|
260
|
-
products_to_auto_match_ids.each { |product_id| Spree::Products::AutoMatchTaxonsJob.perform_later(product_id) }
|
|
252
|
+
auto_match_taxons_jobs = products_to_auto_match_ids.map do |product_id|
|
|
253
|
+
Spree::Products::AutoMatchTaxonsJob.new(product_id).tap { |job| job.scheduled_at = 30.seconds.from_now }
|
|
261
254
|
end
|
|
255
|
+
|
|
256
|
+
ActiveJob.perform_all_later(auto_match_taxons_jobs)
|
|
262
257
|
end
|
|
263
258
|
|
|
264
259
|
# Can't use short form block syntax due to https://github.com/Netflix/fast_jsonapi/issues/259
|
|
@@ -582,7 +577,7 @@ module Spree
|
|
|
582
577
|
store = stores.find_by(default: true) || stores.first
|
|
583
578
|
return if store.nil? || store.taxons.automatic.none?
|
|
584
579
|
|
|
585
|
-
Spree::Products::AutoMatchTaxonsJob.perform_later(id)
|
|
580
|
+
Spree::Products::AutoMatchTaxonsJob.set(wait: 30.seconds).perform_later(id)
|
|
586
581
|
end
|
|
587
582
|
|
|
588
583
|
def to_csv(store = nil)
|
|
@@ -13,7 +13,7 @@ module Spree
|
|
|
13
13
|
@store = store
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
# Search
|
|
16
|
+
# Search and paginate products. Does NOT compute filter facets — use #filters for that.
|
|
17
17
|
#
|
|
18
18
|
# @param scope [ActiveRecord::Relation] base scope (store-scoped, visibility-filtered, authorized)
|
|
19
19
|
# @param query [String, nil] text search query
|
|
@@ -26,6 +26,18 @@ module Spree
|
|
|
26
26
|
raise NotImplementedError
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Compute filter facets, sort options, and total count for the given scope.
|
|
30
|
+
# Called by the dedicated filters endpoint — kept separate from search_and_filter
|
|
31
|
+
# to avoid expensive facet queries on every product listing.
|
|
32
|
+
#
|
|
33
|
+
# @param scope [ActiveRecord::Relation] base scope
|
|
34
|
+
# @param query [String, nil] text search query
|
|
35
|
+
# @param filters [Hash] structured filters
|
|
36
|
+
# @return [FiltersResult]
|
|
37
|
+
def filters(scope:, query: nil, filters: {})
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
end
|
|
40
|
+
|
|
29
41
|
# Index a product — called after product save. No-op for database provider.
|
|
30
42
|
#
|
|
31
43
|
# @param product [Spree::Product] the product to index
|