spree_core 5.5.3 → 5.6.0.rc1
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/helpers/spree/base_helper.rb +12 -2
- data/app/helpers/spree/currency_helper.rb +1 -4
- data/app/helpers/spree/locale_helper.rb +18 -13
- data/app/helpers/spree/localized_names_helper.rb +25 -0
- data/app/mailers/spree/admin_user_mailer.rb +71 -0
- data/app/mailers/spree/base_mailer.rb +73 -11
- data/app/mailers/spree/export_mailer.rb +7 -7
- data/app/mailers/spree/import_mailer.rb +19 -0
- data/app/mailers/spree/invitation_mailer.rb +16 -11
- data/app/mailers/spree/report_mailer.rb +6 -6
- data/app/mailers/spree/webhook_mailer.rb +7 -6
- data/app/models/concerns/spree/admin_user_methods.rb +44 -0
- data/app/models/concerns/spree/legacy_multi_store_support.rb +48 -0
- data/app/models/concerns/spree/presentation_translatable.rb +39 -0
- data/app/models/concerns/spree/product_scopes.rb +21 -3
- data/app/models/concerns/spree/single_store_resource.rb +5 -0
- data/app/models/concerns/spree/store_scoped_resource.rb +10 -0
- data/app/models/concerns/spree/stores/markets.rb +9 -1
- data/app/models/concerns/spree/stores/setup.rb +37 -23
- data/app/models/concerns/spree/translatable_resource.rb +95 -5
- data/app/models/concerns/spree/user_methods.rb +27 -2
- data/app/models/spree/ability.rb +5 -4
- data/app/models/spree/allowed_origin.rb +31 -0
- data/app/models/spree/category.rb +12 -2
- data/app/models/spree/channel/gating.rb +67 -0
- data/app/models/spree/channel.rb +1 -0
- data/app/models/spree/classification.rb +12 -0
- data/app/models/spree/country.rb +22 -1
- data/app/models/spree/credit_card.rb +29 -0
- data/app/models/spree/currency.rb +47 -0
- data/app/models/spree/current.rb +12 -1
- data/app/models/spree/customer_group.rb +1 -0
- data/app/models/spree/export.rb +19 -0
- data/app/models/spree/exports/coupon_codes.rb +1 -2
- data/app/models/spree/import.rb +84 -1
- data/app/models/spree/import_row.rb +7 -1
- data/app/models/spree/imports/product_translations.rb +6 -0
- data/app/models/spree/locale.rb +104 -0
- data/app/models/spree/option_type.rb +11 -13
- data/app/models/spree/option_value.rb +5 -17
- data/app/models/spree/order.rb +19 -2
- data/app/models/spree/payment_method.rb +6 -5
- data/app/models/spree/policy.rb +2 -1
- data/app/models/spree/product.rb +23 -2
- data/app/models/spree/promotion/rules/channel.rb +37 -0
- data/app/models/spree/promotion/rules/market.rb +37 -0
- data/app/models/spree/promotion.rb +5 -3
- data/app/models/spree/promotion_handler/promotion_duplicator.rb +1 -1
- data/app/models/spree/role_user.rb +4 -0
- data/app/models/spree/search_provider/meilisearch.rb +22 -9
- data/app/models/spree/setup_task.rb +12 -0
- data/app/models/spree/setup_tasks.rb +86 -0
- data/app/models/spree/shipment.rb +38 -1
- data/app/models/spree/shipment_handler.rb +1 -1
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/quantifier.rb +21 -1
- data/app/models/spree/stock_item.rb +2 -1
- data/app/models/spree/stock_location.rb +4 -1
- data/app/models/spree/store.rb +45 -9
- data/app/models/spree/store_payment_method.rb +5 -1
- data/app/models/spree/store_promotion.rb +4 -0
- data/app/models/spree/taxon.rb +112 -9
- data/app/models/spree/taxonomy.rb +1 -1
- data/app/models/spree/translations/batch.rb +104 -0
- data/app/models/spree/variant.rb +29 -2
- data/app/presenters/spree/search_provider/product_presenter.rb +3 -0
- data/app/services/spree/carts/complete.rb +3 -0
- data/app/services/spree/fulfillments/create.rb +259 -0
- data/app/services/spree/locales/set_fallback_locale_for_store.rb +7 -1
- data/app/services/spree/locales.rb +52 -0
- data/app/services/spree/products/prepare_nested_attributes.rb +40 -12
- data/app/services/spree/shipments/update.rb +1 -1
- data/app/services/spree/stock_reservations/reserve.rb +4 -1
- data/app/services/spree/taxons/add_products.rb +3 -0
- data/app/services/spree/taxons/remove_products.rb +3 -0
- data/app/subscribers/spree/admin_user_email_subscriber.rb +29 -0
- data/app/subscribers/spree/import_email_subscriber.rb +26 -0
- data/app/views/layouts/spree/base_mailer.html.erb +14 -17
- data/app/views/spree/admin_user_mailer/confirmation_email.html.erb +7 -0
- data/app/views/spree/admin_user_mailer/password_reset_email.html.erb +8 -0
- data/app/views/spree/export_mailer/export_done.html.erb +6 -14
- data/app/views/spree/import_mailer/import_done.html.erb +12 -0
- data/app/views/spree/invitation_mailer/invitation_accepted.html.erb +4 -12
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +11 -24
- data/app/views/spree/report_mailer/report_done.html.erb +6 -14
- data/app/views/spree/shared/_base_mailer_footer.html.erb +15 -8
- data/app/views/spree/shared/_base_mailer_header.html.erb +7 -5
- data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +211 -273
- data/app/views/spree/shared/_mailer_button.html.erb +10 -0
- data/app/views/spree/shared/_mailer_hero.html.erb +12 -0
- data/app/views/spree/shared/_purchased_items_styles.html.erb +112 -0
- data/app/views/spree/webhook_mailer/endpoint_disabled.html.erb +39 -14
- data/config/initializers/spree_store_setup_tasks.rb +23 -0
- data/config/locales/en.yml +42 -992
- data/db/migrate/20260613000001_add_store_id_to_spree_role_users.rb +10 -0
- data/db/migrate/20260626000001_add_store_id_to_spree_taxons.rb +11 -0
- data/db/migrate/20260627000001_add_products_count_to_spree_taxons.rb +6 -0
- data/db/migrate/20260628000001_add_store_id_to_spree_promotions.rb +10 -0
- data/db/migrate/20260628000002_add_store_id_to_spree_payment_methods.rb +11 -0
- data/db/migrate/20260630000001_add_preorder_fields_to_spree_variants.rb +7 -0
- data/db/migrate/20260707000001_add_fingerprint_to_spree_credit_cards.rb +39 -0
- data/db/migrate/20260710000001_add_import_id_status_index_to_spree_import_rows.rb +7 -0
- data/db/migrate/20260711000001_add_preferences_to_spree_exports.rb +7 -0
- data/db/sample_data/promotions.rb +1 -1
- data/lib/generators/spree/authentication/dummy/templates/create_spree_admin_users.rb.tt +0 -1
- data/lib/mobility/plugins/store_based_fallbacks.rb +7 -5
- data/lib/spree/core/configuration.rb +1 -0
- data/lib/spree/core/controller_helpers/locale.rb +4 -1
- data/lib/spree/core/controller_helpers/strong_parameters.rb +1 -0
- data/lib/spree/core/dependencies.rb +3 -0
- data/lib/spree/core/engine.rb +24 -0
- data/lib/spree/core/preferences/preferable.rb +6 -1
- data/lib/spree/core/previews/admin_user_preview.rb +22 -0
- data/lib/spree/core/previews/export_preview.rb +37 -0
- data/lib/spree/core/previews/invitation_preview.rb +47 -0
- data/lib/spree/core/previews/preview_data.rb +52 -0
- data/lib/spree/core/previews/report_preview.rb +40 -0
- data/lib/spree/core/previews/webhook_preview.rb +34 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +22 -1
- data/lib/spree/permitted_attributes.rb +5 -4
- data/lib/spree/testing_support/factories/export_factory.rb +4 -0
- data/lib/spree/testing_support/factories/payment_method_factory.rb +1 -9
- data/lib/spree/testing_support/factories/promotion_factory.rb +1 -9
- data/lib/spree/testing_support/factories/promotion_rule_factory.rb +24 -0
- data/lib/spree/translations.rb +113 -0
- data/lib/spree/upgrades/5_5_to_5_6/manifest.yml +59 -0
- data/lib/tasks/role_users.rake +21 -0
- data/lib/tasks/single_store_associations.rake +65 -0
- data/lib/tasks/taxons.rake +62 -1
- metadata +47 -5
- data/app/models/spree/product/legacy_multi_store_support.rb +0 -40
data/app/models/spree/export.rb
CHANGED
|
@@ -43,6 +43,15 @@ module Spree
|
|
|
43
43
|
#
|
|
44
44
|
self.whitelisted_ransackable_attributes = %w[number type format vendor_id]
|
|
45
45
|
|
|
46
|
+
#
|
|
47
|
+
# Preferences
|
|
48
|
+
#
|
|
49
|
+
# Absolute URL of the admin view for this export, captured at create by
|
|
50
|
+
# the caller (legacy admin passes its export page; see the import
|
|
51
|
+
# counterpart for the dashboard mechanics) — the export-done email links
|
|
52
|
+
# back to it. No URL, no button.
|
|
53
|
+
preference :results_url, :string, default: nil
|
|
54
|
+
|
|
46
55
|
#
|
|
47
56
|
# Attachments
|
|
48
57
|
#
|
|
@@ -205,6 +214,16 @@ module Spree
|
|
|
205
214
|
Rails.root.join('tmp', export_file_name)
|
|
206
215
|
end
|
|
207
216
|
|
|
217
|
+
# Public API name for the +results_url+ preference (read/write symmetry).
|
|
218
|
+
# String preferences round-trip nil as "" — normalize blank to nil.
|
|
219
|
+
def results_url
|
|
220
|
+
preferred_results_url.presence
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def results_url=(value)
|
|
224
|
+
self.preferred_results_url = value
|
|
225
|
+
end
|
|
226
|
+
|
|
208
227
|
def send_export_done_email
|
|
209
228
|
return if user.blank? # App-created exports (secret API key) have no user to email.
|
|
210
229
|
|
|
@@ -14,8 +14,7 @@ module Spree
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def scope
|
|
17
|
-
model_class.
|
|
18
|
-
.accessible_by(current_ability)
|
|
17
|
+
model_class.where(promotion: store.promotions).accessible_by(current_ability, :show)
|
|
19
18
|
end
|
|
20
19
|
end
|
|
21
20
|
end
|
data/app/models/spree/import.rb
CHANGED
|
@@ -34,7 +34,12 @@ module Spree
|
|
|
34
34
|
#
|
|
35
35
|
# Ransack configuration
|
|
36
36
|
#
|
|
37
|
-
self.whitelisted_ransackable_attributes = %w[number type]
|
|
37
|
+
self.whitelisted_ransackable_attributes = %w[number type status]
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
# Scopes
|
|
41
|
+
#
|
|
42
|
+
scope :for_store, ->(store) { where(owner: store) }
|
|
38
43
|
|
|
39
44
|
#
|
|
40
45
|
# Attachments
|
|
@@ -70,12 +75,23 @@ module Spree
|
|
|
70
75
|
event :fail do
|
|
71
76
|
transition to: :failed
|
|
72
77
|
end
|
|
78
|
+
|
|
79
|
+
# Re-processes rows that failed: the dispatcher targets pending_and_failed
|
|
80
|
+
# rows, so retry is just a transition + re-dispatch.
|
|
81
|
+
event :retry_failed_rows do
|
|
82
|
+
transition from: :completed, to: :processing, if: ->(import) { import.rows.failed.exists? }
|
|
83
|
+
end
|
|
84
|
+
after_transition on: :retry_failed_rows, do: :process_rows_async
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
#
|
|
76
88
|
# Preferences
|
|
77
89
|
#
|
|
78
90
|
preference :delimiter, :string, default: ','
|
|
91
|
+
# Absolute URL of the dashboard's imports view, captured at create and
|
|
92
|
+
# validated against the store's allowed origins — the import-done email
|
|
93
|
+
# links back to it. Blank for legacy-admin or app-created imports.
|
|
94
|
+
preference :results_url, :string, default: nil
|
|
79
95
|
|
|
80
96
|
# Returns true if the import has more rows than the large import threshold.
|
|
81
97
|
# Large imports skip per-row UI broadcasts and use bulk processing.
|
|
@@ -110,6 +126,59 @@ module Spree
|
|
|
110
126
|
status == 'completed'
|
|
111
127
|
end
|
|
112
128
|
|
|
129
|
+
# Imports are deletable only while no processing jobs can be in flight —
|
|
130
|
+
# CreateRowsJob/ProcessGroupJob re-load the record mid-run.
|
|
131
|
+
# @return [Boolean]
|
|
132
|
+
def can_be_deleted?
|
|
133
|
+
%w[pending mapping completed failed].include?(status)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Per-status row counts for API status payloads, memoized so one request
|
|
137
|
+
# issues a single grouped COUNT instead of one per status.
|
|
138
|
+
# @return [Hash{String => Integer}]
|
|
139
|
+
def rows_status_counts
|
|
140
|
+
@rows_status_counts ||= rows.group(:status).count
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Public API name for the +results_url+ preference (read/write symmetry).
|
|
144
|
+
# String preferences round-trip nil as "" — normalize blank to nil.
|
|
145
|
+
def results_url
|
|
146
|
+
preferred_results_url.presence
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def results_url=(value)
|
|
150
|
+
self.preferred_results_url = value
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# URL of the wizard/results view for this import — the caller-provided
|
|
154
|
+
# +results_url+ with the wizard's `?import=` param appended (a no-op for
|
|
155
|
+
# the legacy admin's show URL, whose path already identifies the import).
|
|
156
|
+
# Nil when the preference is absent; the mailer then renders no link.
|
|
157
|
+
# @return [String, nil]
|
|
158
|
+
def results_page_url
|
|
159
|
+
return if preferred_results_url.blank?
|
|
160
|
+
|
|
161
|
+
uri = URI.parse(preferred_results_url)
|
|
162
|
+
params = URI.decode_www_form(uri.query.to_s)
|
|
163
|
+
params << ['import', prefixed_id]
|
|
164
|
+
uri.query = URI.encode_www_form(params)
|
|
165
|
+
uri.to_s
|
|
166
|
+
rescue URI::InvalidURIError
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# First data row as { header => value }, reading a single line of the
|
|
171
|
+
# attached CSV — used by the mapping UI to show example values.
|
|
172
|
+
# @return [Hash]
|
|
173
|
+
def sample_row
|
|
174
|
+
return {} if attachment_file_content.blank?
|
|
175
|
+
|
|
176
|
+
row = ::CSV.foreach(StringIO.new(attachment_file_content), headers: true, col_sep: preferred_delimiter).first
|
|
177
|
+
row&.to_h || {}
|
|
178
|
+
rescue ::CSV::MalformedCSVError, EncodingError
|
|
179
|
+
{}
|
|
180
|
+
end
|
|
181
|
+
|
|
113
182
|
# Returns the model class for the import
|
|
114
183
|
# @return [Class]
|
|
115
184
|
def model_class
|
|
@@ -277,6 +346,20 @@ module Spree
|
|
|
277
346
|
available_types.find { |type| type.model_class.to_s == model.to_s }
|
|
278
347
|
end
|
|
279
348
|
|
|
349
|
+
# Admin API scope family gating this import type — an import is a bulk
|
|
350
|
+
# write, so an API key needs `write_<required_scope>` to create and
|
|
351
|
+
# manage it. Derived from the class name (Spree::Imports::Customers =>
|
|
352
|
+
# :customers); override in subclasses gated by a different scope.
|
|
353
|
+
# Returns nil on the base class, so unmapped types are only accessible
|
|
354
|
+
# to write_all keys.
|
|
355
|
+
#
|
|
356
|
+
# @return [Symbol, nil]
|
|
357
|
+
def required_scope
|
|
358
|
+
return nil if self == Spree::Import
|
|
359
|
+
|
|
360
|
+
to_s.demodulize.underscore.to_sym
|
|
361
|
+
end
|
|
362
|
+
|
|
280
363
|
# eg. Spree::Imports::Orders => Spree::Order
|
|
281
364
|
def model_class
|
|
282
365
|
return Spree.user_class if to_s == 'Spree::Imports::Customers'
|
|
@@ -18,6 +18,11 @@ module Spree
|
|
|
18
18
|
validates :import, :data, presence: true
|
|
19
19
|
validates :row_number, uniqueness: { scope: :import_id }, numericality: { only_integer: true, greater_than: 0 }, presence: true
|
|
20
20
|
|
|
21
|
+
#
|
|
22
|
+
# Ransack configuration
|
|
23
|
+
#
|
|
24
|
+
self.whitelisted_ransackable_attributes = %w[status row_number]
|
|
25
|
+
|
|
21
26
|
#
|
|
22
27
|
# State machine
|
|
23
28
|
#
|
|
@@ -92,6 +97,7 @@ module Spree
|
|
|
92
97
|
def process!(mappings: nil, schema_fields: nil)
|
|
93
98
|
start_processing!
|
|
94
99
|
self.item = import.row_processor_class.new(self, mappings: mappings, schema_fields: schema_fields).process!
|
|
100
|
+
self.validation_errors = nil # clear a stale error when a retried row succeeds
|
|
95
101
|
complete!
|
|
96
102
|
rescue StandardError => e
|
|
97
103
|
Rails.error.report(e, handled: true, context: { import_row_id: id }, source: 'spree.core')
|
|
@@ -105,7 +111,7 @@ module Spree
|
|
|
105
111
|
update_columns(status: 'processing', updated_at: Time.current)
|
|
106
112
|
processor = import.row_processor_class.new(self, mappings: mappings, schema_fields: schema_fields)
|
|
107
113
|
self.item = processor.process!
|
|
108
|
-
update_columns(status: 'completed', item_type: item.class.name, item_id: item.id, updated_at: Time.current)
|
|
114
|
+
update_columns(status: 'completed', item_type: item.class.name, item_id: item.id, validation_errors: nil, updated_at: Time.current)
|
|
109
115
|
rescue StandardError => e
|
|
110
116
|
Rails.error.report(e, handled: true, context: { import_row_id: id }, source: 'spree.core')
|
|
111
117
|
update_columns(status: 'failed', validation_errors: e.message, updated_at: Time.current)
|
|
@@ -16,6 +16,12 @@ module Spree
|
|
|
16
16
|
def self.model_class
|
|
17
17
|
Spree::Product
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
# Translation imports write products, so they share the products scope
|
|
21
|
+
# (mirrors Spree::Exports::ProductTranslations.required_scope).
|
|
22
|
+
def self.required_scope
|
|
23
|
+
:products
|
|
24
|
+
end
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Virtual model for a supported locale. Wraps a locale +code+ and exposes its
|
|
3
|
+
# display name, layout direction, and whether it is a store's default — the
|
|
4
|
+
# single home for that logic (RTL list, name resolution) instead of being
|
|
5
|
+
# restated in serializers, helpers, and the admin RTL module.
|
|
6
|
+
class Locale
|
|
7
|
+
include ActiveModel::Model
|
|
8
|
+
include Comparable
|
|
9
|
+
|
|
10
|
+
# ISO 639-1 language codes that use right-to-left scripts.
|
|
11
|
+
RTL_LANGUAGE_CODES = %w[ar he fa ur yi].freeze
|
|
12
|
+
|
|
13
|
+
# @!attribute code
|
|
14
|
+
# @return [String] the locale code, e.g. "en", "pt-BR"
|
|
15
|
+
attr_accessor :code
|
|
16
|
+
|
|
17
|
+
# Display name, e.g. "English", "Deutsch". Falls back to the code for an
|
|
18
|
+
# unknown locale.
|
|
19
|
+
# @return [String]
|
|
20
|
+
def name
|
|
21
|
+
code = self.code.to_s
|
|
22
|
+
|
|
23
|
+
if I18n.exists?('spree.i18n.this_file_language', locale: code, fallback: false)
|
|
24
|
+
return normalize_name(Spree.t('i18n.this_file_language', locale: code))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if defined?(SpreeI18n::Locale) && (name = SpreeI18n::Locale.local_language_name(code))
|
|
28
|
+
return normalize_name(name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return 'English' if code == 'en'
|
|
32
|
+
|
|
33
|
+
code
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Select label, e.g. "EN — English".
|
|
37
|
+
# @return [String]
|
|
38
|
+
def label
|
|
39
|
+
upper = code.to_s.upcase
|
|
40
|
+
return upper if name.blank? || name.casecmp?(code.to_s)
|
|
41
|
+
|
|
42
|
+
"#{upper} — #{name}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Spree::Store] the current store
|
|
46
|
+
def store
|
|
47
|
+
@store ||= Spree::Store.current
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Boolean] whether this is the store's default (source) locale
|
|
51
|
+
def default?
|
|
52
|
+
store.present? && code.to_s == store.default_locale.to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Boolean] whether the locale uses a right-to-left script
|
|
56
|
+
def rtl?
|
|
57
|
+
RTL_LANGUAGE_CODES.include?(language_code)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [String] "rtl" or "ltr"
|
|
61
|
+
def direction
|
|
62
|
+
rtl? ? 'rtl' : 'ltr'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The base ISO 639-1 language code, dropping any region (e.g. "pt-BR" → "pt").
|
|
66
|
+
# @return [String]
|
|
67
|
+
def language_code
|
|
68
|
+
code.to_s.downcase.tr('_', '-').split('-').first
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_s
|
|
72
|
+
code.to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Compare/equality by code so a Locale slots into string-keyed collections
|
|
76
|
+
# (e.g. `locale == "en"`, `[locale].sort`).
|
|
77
|
+
def <=>(other)
|
|
78
|
+
to_s <=> other.to_s
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def eql?(other)
|
|
82
|
+
other.is_a?(Spree::Locale) && code.to_s == other.code.to_s
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def hash
|
|
86
|
+
code.to_s.hash
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Strip a trailing " (CODE)" suffix from Spree I18n locale labels. Uses
|
|
92
|
+
# plain string ops rather than a regex to avoid polynomial backtracking on
|
|
93
|
+
# adversarial input (ReDoS).
|
|
94
|
+
def normalize_name(name)
|
|
95
|
+
name = name.to_s.rstrip
|
|
96
|
+
return name unless name.end_with?(')')
|
|
97
|
+
|
|
98
|
+
open_paren = name.rindex('(')
|
|
99
|
+
return name unless open_paren
|
|
100
|
+
|
|
101
|
+
name[0...open_paren].rstrip
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -9,10 +9,16 @@ module Spree
|
|
|
9
9
|
include Spree::UniqueName
|
|
10
10
|
include Spree::Metafields
|
|
11
11
|
include Spree::Metadata
|
|
12
|
-
|
|
12
|
+
# presentation column, exposed publicly as `label` + translated via Mobility.
|
|
13
|
+
include Spree::PresentationTranslatable
|
|
13
14
|
|
|
14
|
-
TRANSLATABLE_FIELDS =
|
|
15
|
-
|
|
15
|
+
TRANSLATABLE_FIELDS = Spree::PresentationTranslatable::TRANSLATABLE_FIELDS
|
|
16
|
+
|
|
17
|
+
# Option values are translated alongside their type — the translations
|
|
18
|
+
# endpoint nests their matrices so the editor fetches both in one read.
|
|
19
|
+
def self.translatable_children
|
|
20
|
+
:option_values
|
|
21
|
+
end
|
|
16
22
|
|
|
17
23
|
self::Translation.class_eval do
|
|
18
24
|
normalizes :presentation, with: ->(value) { value&.to_s&.squish&.presence }
|
|
@@ -41,16 +47,8 @@ module Spree
|
|
|
41
47
|
has_many :option_type_prototypes, class_name: 'Spree::OptionTypePrototype'
|
|
42
48
|
has_many :prototypes, through: :option_type_prototypes, class_name: 'Spree::Prototype'
|
|
43
49
|
|
|
44
|
-
# 5.5 API naming bridge (
|
|
45
|
-
#
|
|
46
|
-
# so we define explicit delegating methods instead.
|
|
47
|
-
def label(*args)
|
|
48
|
-
presentation(*args)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def label=(value)
|
|
52
|
-
self.presentation = value
|
|
53
|
-
end
|
|
50
|
+
# 5.5 API naming bridge (`label` → `presentation`) lives in
|
|
51
|
+
# Spree::PresentationTranslatable.
|
|
54
52
|
|
|
55
53
|
#
|
|
56
54
|
# Validations
|
|
@@ -5,14 +5,10 @@ module Spree
|
|
|
5
5
|
include Spree::ParameterizableName
|
|
6
6
|
include Spree::Metafields
|
|
7
7
|
include Spree::Metadata
|
|
8
|
-
|
|
8
|
+
# presentation column, exposed publicly as `label` + translated via Mobility.
|
|
9
|
+
include Spree::PresentationTranslatable
|
|
9
10
|
|
|
10
|
-
TRANSLATABLE_FIELDS =
|
|
11
|
-
translates(*TRANSLATABLE_FIELDS, column_fallback: !Spree.always_use_translations?)
|
|
12
|
-
|
|
13
|
-
self::Translation.class_eval do
|
|
14
|
-
normalizes :presentation, with: ->(value) { value&.to_s&.squish&.presence }
|
|
15
|
-
end
|
|
11
|
+
TRANSLATABLE_FIELDS = Spree::PresentationTranslatable::TRANSLATABLE_FIELDS
|
|
16
12
|
|
|
17
13
|
#
|
|
18
14
|
# Magic methods
|
|
@@ -33,16 +29,8 @@ module Spree
|
|
|
33
29
|
has_many :variants, through: :option_value_variants, class_name: 'Spree::Variant'
|
|
34
30
|
has_many :products, through: :variants, class_name: 'Spree::Product'
|
|
35
31
|
|
|
36
|
-
# 5.5 API naming bridge (
|
|
37
|
-
#
|
|
38
|
-
# so we define explicit delegating methods instead.
|
|
39
|
-
def label(*args)
|
|
40
|
-
presentation(*args)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def label=(value)
|
|
44
|
-
self.presentation = value
|
|
45
|
-
end
|
|
32
|
+
# 5.5 API naming bridge (`label` → `presentation`) lives in
|
|
33
|
+
# Spree::PresentationTranslatable.
|
|
46
34
|
|
|
47
35
|
#
|
|
48
36
|
# Validations
|
data/app/models/spree/order.rb
CHANGED
|
@@ -224,7 +224,6 @@ module Spree
|
|
|
224
224
|
accepts_nested_attributes_for :shipments
|
|
225
225
|
|
|
226
226
|
# Needs to happen before save_permalink is called
|
|
227
|
-
before_validation :ensure_store_presence
|
|
228
227
|
before_validation :ensure_market_presence
|
|
229
228
|
before_validation :ensure_channel_presence
|
|
230
229
|
before_validation :ensure_currency_presence
|
|
@@ -500,7 +499,8 @@ module Spree
|
|
|
500
499
|
end
|
|
501
500
|
|
|
502
501
|
def ensure_store_presence
|
|
503
|
-
|
|
502
|
+
Spree::Deprecation.warn('Spree::Order#ensure_store_presence is deprecated and will be removed in Spree 6.0. ensure_store instead.')
|
|
503
|
+
ensure_store
|
|
504
504
|
end
|
|
505
505
|
|
|
506
506
|
def ensure_market_presence
|
|
@@ -513,6 +513,23 @@ module Spree
|
|
|
513
513
|
self.channel = store&.default_channel
|
|
514
514
|
end
|
|
515
515
|
|
|
516
|
+
# @return [Boolean] true when this order has no registered user and its
|
|
517
|
+
# channel forbids guest checkout (see Spree::Channel::Gating). Enforced by
|
|
518
|
+
# the checkout completion service and the v3 Store API so every completion
|
|
519
|
+
# path (controller, payment webhook) is covered.
|
|
520
|
+
#
|
|
521
|
+
# A +prices_hidden+ channel also disallows guest completion regardless of
|
|
522
|
+
# the +guest_checkout+ flag — prices are withheld from guests, and a buyer
|
|
523
|
+
# who cannot see prices cannot meaningfully place an order. This dissolves
|
|
524
|
+
# the otherwise contradictory "prices hidden but guests may buy" config.
|
|
525
|
+
def guest_checkout_disallowed?
|
|
526
|
+
return false if user_id.present?
|
|
527
|
+
return false if channel.blank?
|
|
528
|
+
return true if channel.storefront_prices_hidden?
|
|
529
|
+
|
|
530
|
+
!channel.resolved_guest_checkout
|
|
531
|
+
end
|
|
532
|
+
|
|
516
533
|
def allow_cancel?
|
|
517
534
|
return false if !completed? || canceled?
|
|
518
535
|
|
|
@@ -5,13 +5,15 @@ module Spree
|
|
|
5
5
|
acts_as_paranoid
|
|
6
6
|
acts_as_list
|
|
7
7
|
|
|
8
|
-
include Spree::
|
|
8
|
+
include Spree::SingleStoreResource
|
|
9
9
|
include Spree::Metafields
|
|
10
10
|
include Spree::Metadata
|
|
11
11
|
include Spree::DisplayOn
|
|
12
12
|
if defined?(Spree::Security::PaymentMethods)
|
|
13
13
|
include Spree::Security::PaymentMethods
|
|
14
14
|
end
|
|
15
|
+
# Multi-store sharing moved to the spree_multi_store extension in 5.6.
|
|
16
|
+
include Spree::LegacyMultiStoreSupport unless defined?(SpreeMultiStore)
|
|
15
17
|
|
|
16
18
|
scope :active, -> { where(active: true).order(position: :asc) }
|
|
17
19
|
scope :available, -> { active.where(display_on: [:front_end, :back_end, :both]) }
|
|
@@ -20,10 +22,10 @@ module Spree
|
|
|
20
22
|
after_initialize :set_name, if: :new_record?
|
|
21
23
|
|
|
22
24
|
validates :name, presence: true
|
|
25
|
+
validates :store, presence: true, unless: -> { Spree::Config[:disable_store_presence_validation] }
|
|
23
26
|
normalizes :name, with: ->(value) { value&.to_s&.squish&.presence }
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
has_many :stores, class_name: 'Spree::Store', through: :store_payment_methods
|
|
28
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
27
29
|
|
|
28
30
|
has_many :payments, class_name: 'Spree::Payment', inverse_of: :payment_method, dependent: :nullify
|
|
29
31
|
has_many :credit_cards, class_name: 'Spree::CreditCard', dependent: :destroy # CCs are soft deleted
|
|
@@ -101,7 +103,6 @@ module Spree
|
|
|
101
103
|
# Returns the webhook URL for this payment method.
|
|
102
104
|
# @return [String, nil]
|
|
103
105
|
def webhook_url
|
|
104
|
-
store = stores.first
|
|
105
106
|
return nil unless store
|
|
106
107
|
|
|
107
108
|
"#{store.url_or_custom_domain}/api/v3/webhooks/payments/#{prefixed_id}"
|
|
@@ -200,7 +201,7 @@ module Spree
|
|
|
200
201
|
def available_for_store?(store)
|
|
201
202
|
return true if store.blank?
|
|
202
203
|
|
|
203
|
-
|
|
204
|
+
store_id == store.id
|
|
204
205
|
end
|
|
205
206
|
|
|
206
207
|
def public_preferences
|
data/app/models/spree/policy.rb
CHANGED
|
@@ -21,7 +21,8 @@ module Spree
|
|
|
21
21
|
# Translations
|
|
22
22
|
#
|
|
23
23
|
TRANSLATABLE_FIELDS = %i[name body].freeze
|
|
24
|
-
|
|
24
|
+
RICH_TEXT_TRANSLATABLE_FIELDS = %i[body].freeze
|
|
25
|
+
translates(*TRANSLATABLE_FIELDS, column_fallback: Spree.mobility_column_fallback)
|
|
25
26
|
|
|
26
27
|
#
|
|
27
28
|
# ActionText
|
data/app/models/spree/product.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Spree
|
|
|
34
34
|
include Spree::Product::Webhooks
|
|
35
35
|
include Spree::Product::Slugs
|
|
36
36
|
include Spree::Product::Channels
|
|
37
|
-
include Spree::
|
|
37
|
+
include Spree::LegacyMultiStoreSupport unless defined?(SpreeMultiStore)
|
|
38
38
|
include Spree::SearchIndexable
|
|
39
39
|
if defined?(Spree::VendorConcern)
|
|
40
40
|
include Spree::VendorConcern
|
|
@@ -56,7 +56,8 @@ module Spree
|
|
|
56
56
|
}.freeze
|
|
57
57
|
|
|
58
58
|
TRANSLATABLE_FIELDS = %i[name description slug meta_description meta_title].freeze
|
|
59
|
-
|
|
59
|
+
RICH_TEXT_TRANSLATABLE_FIELDS = %i[description].freeze
|
|
60
|
+
translates(*TRANSLATABLE_FIELDS, column_fallback: Spree.mobility_column_fallback)
|
|
60
61
|
|
|
61
62
|
self::Translation.class_eval do
|
|
62
63
|
normalizes :name, :meta_title, with: ->(value) { value&.to_s&.squish&.presence }
|
|
@@ -522,6 +523,26 @@ module Spree
|
|
|
522
523
|
active? && !deleted? && (available_on.nil? || available_on <= Time.current)
|
|
523
524
|
end
|
|
524
525
|
|
|
526
|
+
# True when the product is currently offered as a pre-order: at least one
|
|
527
|
+
# variant is sold before it is in stock (see {Variant#preorder?}).
|
|
528
|
+
# Independent of publishing — a pre-order product is live in the catalog,
|
|
529
|
+
# just flagged "buy now, ships later".
|
|
530
|
+
# @return [Boolean]
|
|
531
|
+
def preorder?
|
|
532
|
+
default_variant.preorder? || variants.any?(&:preorder?)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# The latest "ships by" date across the product's pre-order variants, so a
|
|
536
|
+
# storefront can show a single "ships by" promise for the product. Nil when
|
|
537
|
+
# the product is not a pre-order. Scans the same sellable set {#preorder?}
|
|
538
|
+
# consults (the master only counts when it is the sole, purchasable
|
|
539
|
+
# variant), reading preloaded associations to avoid an N+1.
|
|
540
|
+
# @return [ActiveSupport::TimeWithZone, nil]
|
|
541
|
+
def preorder_ships_at
|
|
542
|
+
candidates = has_variants? ? variants : [master]
|
|
543
|
+
candidates.select(&:preorder?).filter_map(&:preorder_ships_at).max
|
|
544
|
+
end
|
|
545
|
+
|
|
525
546
|
def discontinue!
|
|
526
547
|
self.discontinue_on = Time.current
|
|
527
548
|
self.status = 'archived'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# A rule to limit a promotion based on the order's channel.
|
|
2
|
+
module Spree
|
|
3
|
+
class Promotion
|
|
4
|
+
module Rules
|
|
5
|
+
class Channel < PromotionRule
|
|
6
|
+
# Stored as raw IDs. Accepts prefixed IDs (`ch_…`) from API
|
|
7
|
+
# callers and decodes them on write so eligibility checks compare
|
|
8
|
+
# against the order's raw `channel_id` directly. Scope confines the
|
|
9
|
+
# existence check to the promotion's store so cross-store channel
|
|
10
|
+
# IDs can't sneak in.
|
|
11
|
+
preference :channel_ids, :array, default: [],
|
|
12
|
+
parse_on_set: normalize_id_preference(
|
|
13
|
+
klass: Spree::Channel,
|
|
14
|
+
scope: ->(rule) { rule.promotion&.store&.channels || Spree::Channel.none }
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def applicable?(promotable)
|
|
18
|
+
promotable.is_a?(Spree::Order)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def channels
|
|
22
|
+
return Spree::Channel.none if preferred_channel_ids.blank?
|
|
23
|
+
|
|
24
|
+
promotion.store.channels.where(id: preferred_channel_ids)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def eligible?(order, _options = {})
|
|
28
|
+
return false if preferred_channel_ids.empty?
|
|
29
|
+
return true if preferred_channel_ids.map(&:to_s).include?(order.channel_id.to_s)
|
|
30
|
+
|
|
31
|
+
eligibility_errors.add(:base, eligibility_error_message(:no_matching_channel))
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# A rule to limit a promotion based on the order's market.
|
|
2
|
+
module Spree
|
|
3
|
+
class Promotion
|
|
4
|
+
module Rules
|
|
5
|
+
class Market < PromotionRule
|
|
6
|
+
# Stored as raw IDs. Accepts prefixed IDs (`mkt_…`) from API
|
|
7
|
+
# callers and decodes them on write so eligibility checks compare
|
|
8
|
+
# against the order's raw `market_id` directly. Scope confines the
|
|
9
|
+
# existence check to the promotion's store so cross-store market
|
|
10
|
+
# IDs can't sneak in.
|
|
11
|
+
preference :market_ids, :array, default: [],
|
|
12
|
+
parse_on_set: normalize_id_preference(
|
|
13
|
+
klass: Spree::Market,
|
|
14
|
+
scope: ->(rule) { rule.promotion&.store&.markets || Spree::Market.none }
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def applicable?(promotable)
|
|
18
|
+
promotable.is_a?(Spree::Order)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def markets
|
|
22
|
+
return Spree::Market.none if preferred_market_ids.blank?
|
|
23
|
+
|
|
24
|
+
promotion.store.markets.where(id: preferred_market_ids)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def eligible?(order, _options = {})
|
|
28
|
+
return false if preferred_market_ids.empty?
|
|
29
|
+
return true if preferred_market_ids.map(&:to_s).include?(order.market_id.to_s)
|
|
30
|
+
|
|
31
|
+
eligibility_errors.add(:base, eligibility_error_message(:no_matching_market))
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -2,12 +2,14 @@ module Spree
|
|
|
2
2
|
class Promotion < Spree.base_class
|
|
3
3
|
has_prefix_id :promo # Spree-specific: promotion
|
|
4
4
|
|
|
5
|
-
include Spree::
|
|
5
|
+
include Spree::SingleStoreResource
|
|
6
6
|
include Spree::Metafields
|
|
7
7
|
include Spree::Metadata
|
|
8
8
|
if defined?(Spree::Security::Promotions)
|
|
9
9
|
include Spree::Security::Promotions
|
|
10
10
|
end
|
|
11
|
+
# Multi-store sharing moved to the spree_multi_store extension in 5.6.
|
|
12
|
+
include Spree::LegacyMultiStoreSupport unless defined?(SpreeMultiStore)
|
|
11
13
|
|
|
12
14
|
publishes_lifecycle_events
|
|
13
15
|
|
|
@@ -37,8 +39,7 @@ module Spree
|
|
|
37
39
|
has_many :coupon_codes, -> { order(created_at: :asc) }, dependent: :destroy, class_name: 'Spree::CouponCode'
|
|
38
40
|
has_many :order_promotions, class_name: 'Spree::OrderPromotion'
|
|
39
41
|
has_many :orders, through: :order_promotions, class_name: 'Spree::Order'
|
|
40
|
-
|
|
41
|
-
has_many :stores, class_name: 'Spree::Store', through: :store_promotions
|
|
42
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
42
43
|
|
|
43
44
|
after_save :apply_pending_rules_and_actions, if: :pending_rules_or_actions?
|
|
44
45
|
|
|
@@ -60,6 +61,7 @@ module Spree
|
|
|
60
61
|
#
|
|
61
62
|
validates_associated :rules
|
|
62
63
|
validates :name, presence: true
|
|
64
|
+
validates :store, presence: true, unless: -> { Spree::Config[:disable_store_presence_validation] }
|
|
63
65
|
validates :usage_limit, numericality: { greater_than: 0, allow_nil: true }
|
|
64
66
|
validates :description, length: { maximum: 255 }, allow_blank: true
|
|
65
67
|
validate :expires_at_must_be_later_than_starts_at, if: -> { starts_at && expires_at }
|
|
@@ -11,7 +11,7 @@ module Spree
|
|
|
11
11
|
@new_promotion.path = "#{@promotion.path}_#{@random_string}"
|
|
12
12
|
@new_promotion.name = "New #{@promotion.name}"
|
|
13
13
|
@new_promotion.code = "#{@promotion.code}_#{@random_string}"
|
|
14
|
-
@new_promotion.
|
|
14
|
+
@new_promotion.store = @promotion.store
|
|
15
15
|
|
|
16
16
|
ActiveRecord::Base.transaction do
|
|
17
17
|
@new_promotion.save
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
class RoleUser < Spree.base_class
|
|
3
|
+
include Spree::SingleStoreResource
|
|
4
|
+
|
|
3
5
|
#
|
|
4
6
|
# Associations
|
|
5
7
|
#
|
|
6
8
|
belongs_to :role, class_name: 'Spree::Role', foreign_key: :role_id
|
|
7
9
|
belongs_to :user, polymorphic: true
|
|
8
10
|
belongs_to :resource, polymorphic: true
|
|
11
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
9
12
|
belongs_to :invitation, class_name: 'Spree::Invitation', optional: true, inverse_of: :role_user
|
|
10
13
|
|
|
11
14
|
#
|
|
@@ -14,6 +17,7 @@ module Spree
|
|
|
14
17
|
validates :role, presence: true
|
|
15
18
|
validates :user, presence: true
|
|
16
19
|
validates :resource, presence: true
|
|
20
|
+
validates :store, presence: true
|
|
17
21
|
validates :role_id, uniqueness: { scope: [:user_id, :resource_id, :user_type, :resource_type] }
|
|
18
22
|
|
|
19
23
|
#
|