spree_core 5.5.2 → 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/jobs/spree/imports/process_group_job.rb +20 -9
- data/app/jobs/spree/imports/process_rows_job.rb +12 -4
- 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 +93 -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/imports/row_processors/base.rb +9 -0
- data/app/services/spree/imports/row_processors/product_variant.rb +37 -19
- 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 +6 -5
- 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
|
@@ -123,12 +123,16 @@ module Spree
|
|
|
123
123
|
|
|
124
124
|
def prepare_shipping_category
|
|
125
125
|
shipping_category_name = attributes['shipping_category'].strip
|
|
126
|
-
|
|
126
|
+
cached_lookup(:shipping_category, shipping_category_name) do
|
|
127
|
+
Spree::ShippingCategory.find_by(name: shipping_category_name)
|
|
128
|
+
end
|
|
127
129
|
end
|
|
128
130
|
|
|
129
131
|
def prepare_tax_category
|
|
130
132
|
tax_category_name = attributes['tax_category'].strip
|
|
131
|
-
|
|
133
|
+
cached_lookup(:tax_category, tax_category_name) do
|
|
134
|
+
Spree::TaxCategory.find_by(name: tax_category_name)
|
|
135
|
+
end
|
|
132
136
|
end
|
|
133
137
|
|
|
134
138
|
def prepare_option_value_variants
|
|
@@ -171,27 +175,39 @@ module Spree
|
|
|
171
175
|
# surfaces via the DB unique index (RecordNotUnique) or the AR uniqueness
|
|
172
176
|
# validator (RecordInvalid with a :taken error on the relevant attribute).
|
|
173
177
|
def find_or_create_option_type!(presentation)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
cached_lookup(:option_type, presentation) do
|
|
179
|
+
begin
|
|
180
|
+
Spree::OptionType.search_by_name(presentation).first || Spree::OptionType.create!(presentation: presentation)
|
|
181
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
182
|
+
raise unless uniqueness_conflict?(e, :name)
|
|
177
183
|
|
|
178
|
-
|
|
184
|
+
Spree::OptionType.search_by_name(presentation).first!
|
|
185
|
+
end
|
|
186
|
+
end
|
|
179
187
|
end
|
|
180
188
|
|
|
181
189
|
def find_or_create_option_value!(option_type, presentation)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
cached_lookup(:option_value, option_type.id, presentation) do
|
|
191
|
+
begin
|
|
192
|
+
option_type.option_values.search_by_name(presentation).first || option_type.option_values.create!(presentation: presentation)
|
|
193
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
194
|
+
raise unless uniqueness_conflict?(e, :name)
|
|
185
195
|
|
|
186
|
-
|
|
196
|
+
option_type.option_values.search_by_name(presentation).first!
|
|
197
|
+
end
|
|
198
|
+
end
|
|
187
199
|
end
|
|
188
200
|
|
|
189
201
|
def find_or_create_product_option_type!(option_type)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
202
|
+
cached_lookup(:product_option_type, product.id, option_type.id) do
|
|
203
|
+
begin
|
|
204
|
+
Spree::ProductOptionType.find_or_create_by!(product: product, option_type: option_type)
|
|
205
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
206
|
+
raise unless uniqueness_conflict?(e, :product_id)
|
|
193
207
|
|
|
194
|
-
|
|
208
|
+
Spree::ProductOptionType.find_by!(product: product, option_type: option_type)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
195
211
|
end
|
|
196
212
|
|
|
197
213
|
# RecordNotUnique is always a uniqueness conflict; RecordInvalid only when the
|
|
@@ -245,11 +261,13 @@ module Spree
|
|
|
245
261
|
namespace, key = product.extract_namespace_and_key(full_key)
|
|
246
262
|
|
|
247
263
|
# Find or initialize metafield definition
|
|
248
|
-
metafield_definition =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
264
|
+
metafield_definition = cached_lookup(:metafield_definition, namespace, key, product.class.name) do
|
|
265
|
+
Spree::MetafieldDefinition.find_by(
|
|
266
|
+
namespace: namespace,
|
|
267
|
+
key: key,
|
|
268
|
+
resource_type: product.class.name
|
|
269
|
+
)
|
|
270
|
+
end
|
|
253
271
|
|
|
254
272
|
next unless metafield_definition
|
|
255
273
|
|
|
@@ -9,7 +9,13 @@ module Spree
|
|
|
9
9
|
object[locale] = [store_default_locale]
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# Pass the store default as the terminal default so EVERY locale —
|
|
13
|
+
# including regional variants not in `supported_locales_list` (e.g.
|
|
14
|
+
# `pt-BR` when only `pt` is configured) — ultimately falls back to it.
|
|
15
|
+
# The store default resolves to the populated DB column via Mobility's
|
|
16
|
+
# `column_fallback`, so translated reads never return nil for a locale
|
|
17
|
+
# that lacks a translation row.
|
|
18
|
+
fallbacks_instance = I18n::Locale::Fallbacks.new(store_default_locale, fallbacks)
|
|
13
19
|
|
|
14
20
|
Mobility.store_based_fallbacks = fallbacks_instance
|
|
15
21
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Locales
|
|
3
|
+
# Canonical set of locale codes a merchant may translate **data** into
|
|
4
|
+
# (Mobility-backed content such as product names, descriptions, taxons,
|
|
5
|
+
# option values, etc.).
|
|
6
|
+
#
|
|
7
|
+
# This is deliberately **independent of the admin/storefront UI translation
|
|
8
|
+
# set** (`Spree.available_locales`, which reflects which `spree_i18n` UI
|
|
9
|
+
# bundles happen to be installed). In a headless setup the storefront ships
|
|
10
|
+
# its own UI translations, so the languages a merchant can store content in
|
|
11
|
+
# must not be constrained by which admin chrome translations exist.
|
|
12
|
+
#
|
|
13
|
+
# Codes follow BCP-47 casing (lowercase language, uppercase region —
|
|
14
|
+
# `pt-BR`, `zh-CN`) so the browser's `Intl.DisplayNames` resolves a clean
|
|
15
|
+
# localized label for each. The list is the full ISO 639-1 base-language set
|
|
16
|
+
# plus the regional variants that matter for commerce (British vs. US
|
|
17
|
+
# English, Brazilian vs. European Portuguese, Simplified vs. Traditional
|
|
18
|
+
# Chinese, etc.).
|
|
19
|
+
#
|
|
20
|
+
# The underlying translation tables accept any locale string, so this list
|
|
21
|
+
# governs only what the locale pickers offer — it is not a hard storage
|
|
22
|
+
# constraint.
|
|
23
|
+
ALL = %w[
|
|
24
|
+
aa ab ae af ak am an ar as av ay az
|
|
25
|
+
ba be bg bh bi bm bn bo br bs
|
|
26
|
+
ca ce ch co cr cs cu cv cy
|
|
27
|
+
da de de-AT de-CH dv dz
|
|
28
|
+
ee el en en-AU en-CA en-GB en-IN en-NZ eo es es-419 es-MX et eu
|
|
29
|
+
fa ff fi fj fo fr fr-CA fy
|
|
30
|
+
ga gd gl gn gu gv
|
|
31
|
+
ha he hi ho hr ht hu hy hz
|
|
32
|
+
ia id ie ig ii ik io is it iu
|
|
33
|
+
ja jv
|
|
34
|
+
ka kg ki kj kk kl km kn ko kr ks ku kv kw ky
|
|
35
|
+
la lb lg li ln lo lt lu lv
|
|
36
|
+
mg mh mi mk ml mn mr ms mt my
|
|
37
|
+
na nb nd ne ng nl nn no nr nv ny
|
|
38
|
+
oc oj om or os
|
|
39
|
+
pa pi pl ps pt pt-BR pt-PT
|
|
40
|
+
qu
|
|
41
|
+
rm rn ro ru rw
|
|
42
|
+
sa sc sd se sg si sk sl sm sn so sq sr ss st su sv sw
|
|
43
|
+
ta te tg th ti tk tl tn to tr ts tt tw ty
|
|
44
|
+
ug uk ur uz
|
|
45
|
+
ve vi vo
|
|
46
|
+
wa wo
|
|
47
|
+
xh
|
|
48
|
+
yi yo
|
|
49
|
+
za zh-CN zh-HK zh-TW zu
|
|
50
|
+
].freeze
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -7,6 +7,12 @@ module Spree
|
|
|
7
7
|
# from other stores are preserved. This prevents accidental data loss when a store
|
|
8
8
|
# admin updates product categories in their store without affecting other stores.
|
|
9
9
|
#
|
|
10
|
+
# Variant removal is opt-in: only variants explicitly listed in the
|
|
11
|
+
# `removed_variant_ids` param are marked for destruction (or collected for
|
|
12
|
+
# discontinuation when they have completed orders). Variants merely absent from
|
|
13
|
+
# `variants_attributes` are left untouched, so a partially rendered or broken
|
|
14
|
+
# form can never silently mass-delete variants.
|
|
15
|
+
#
|
|
10
16
|
# @example
|
|
11
17
|
# service = Spree::Products::PrepareNestedAttributes.new(
|
|
12
18
|
# product,
|
|
@@ -28,10 +34,13 @@ module Spree
|
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
def call
|
|
37
|
+
extract_removed_variant_ids
|
|
38
|
+
|
|
31
39
|
if params[:variants_attributes]
|
|
32
40
|
params[:variants_attributes].each do |key, variant_params|
|
|
33
41
|
existing_variant = variant_params[:id].presence && @product.variants.find_by(id: variant_params[:id])
|
|
34
|
-
|
|
42
|
+
# a re-submitted variant always wins over a removal request
|
|
43
|
+
variants_to_remove.delete(variant_params[:id].to_s) if variant_params[:id].present?
|
|
35
44
|
|
|
36
45
|
variant_params.delete(:price) # remove legacy price param
|
|
37
46
|
|
|
@@ -72,17 +81,20 @@ module Spree
|
|
|
72
81
|
# ensure the product is owned by a store
|
|
73
82
|
params[:store_id] = store.id if params[:store_id].blank? && product.store_id.blank?
|
|
74
83
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
# The variants matrix was emptied: no variant rows re-submitted, removals sent instead.
|
|
85
|
+
# Option types are detached only when the removal list covers every variant, so a
|
|
86
|
+
# partial (possibly broken) submission never turns the product into a simple one.
|
|
87
|
+
# Variants kept alive by discontinuation still count as removed from the matrix,
|
|
88
|
+
# so the detachment can't hinge on any of them yielding a `_destroy` row.
|
|
89
|
+
if params[:variants_attributes].blank? && variants_to_remove.any? && can_remove_variants?
|
|
90
|
+
attributes = removed_variants_attributes
|
|
79
91
|
|
|
80
|
-
|
|
81
|
-
variant_ids_to_destroy.each_with_index do |variant_id, index|
|
|
82
|
-
params[:variants_attributes][index.to_s] = { id: variant_id, _destroy: '1' }
|
|
83
|
-
end
|
|
92
|
+
params[:option_type_ids] = [] if removing_all_variants? && !params.key?(:option_type_ids)
|
|
84
93
|
|
|
85
|
-
|
|
94
|
+
if attributes.any?
|
|
95
|
+
params[:variants_attributes] = attributes
|
|
96
|
+
params[:variants_attributes].permit!
|
|
97
|
+
end
|
|
86
98
|
end
|
|
87
99
|
|
|
88
100
|
params
|
|
@@ -116,8 +128,24 @@ module Spree
|
|
|
116
128
|
@product_option_types_to_remove ||= product.product_option_type_ids
|
|
117
129
|
end
|
|
118
130
|
|
|
131
|
+
# Pulls `removed_variant_ids` out of the params so it never reaches Product#update.
|
|
132
|
+
# Must run before any `variants_to_remove` access.
|
|
133
|
+
def extract_removed_variant_ids
|
|
134
|
+
@removed_variant_ids = Array(params.delete(:removed_variant_ids)).map(&:to_s)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Only variants the client explicitly asked to remove, and only ones that
|
|
138
|
+
# actually belong to this product.
|
|
119
139
|
def variants_to_remove
|
|
120
|
-
@variants_to_remove ||=
|
|
140
|
+
@variants_to_remove ||= (@removed_variant_ids || []).uniq & all_variant_ids
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def all_variant_ids
|
|
144
|
+
@all_variant_ids ||= product.variant_ids.map(&:to_s)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def removing_all_variants?
|
|
148
|
+
(all_variant_ids - variants_to_remove).empty?
|
|
121
149
|
end
|
|
122
150
|
|
|
123
151
|
def can_update_prices?
|
|
@@ -142,7 +170,7 @@ module Spree
|
|
|
142
170
|
populate_variants_to_discontinue
|
|
143
171
|
|
|
144
172
|
attributes = {}
|
|
145
|
-
last_index = params[:variants_attributes].keys
|
|
173
|
+
last_index = params[:variants_attributes].presence&.keys&.map(&:to_i)&.max || -1
|
|
146
174
|
variant_ids_to_destroy.each_with_index do |variant_id, index|
|
|
147
175
|
attributes[(last_index + 1 + index).to_s] = { id: variant_id, _destroy: '1' }
|
|
148
176
|
end
|
|
@@ -8,7 +8,7 @@ module Spree
|
|
|
8
8
|
ActiveRecord::Base.transaction do
|
|
9
9
|
return failure(shipment) unless shipment.update(shipment_attributes)
|
|
10
10
|
|
|
11
|
-
if shipment_attributes.key?(:selected_shipping_rate_id)
|
|
11
|
+
if shipment_attributes.key?(:selected_shipping_rate_id) || shipment_attributes.key?(:selected_delivery_rate_id)
|
|
12
12
|
order = shipment.order
|
|
13
13
|
|
|
14
14
|
# Changing the selected Shipping Rate won't update the cost (for now)
|
|
@@ -65,7 +65,10 @@ module Spree
|
|
|
65
65
|
next unless variant&.should_track_inventory?
|
|
66
66
|
|
|
67
67
|
stock_item = select_stock_item(variant)
|
|
68
|
-
|
|
68
|
+
# Backorderable and pre-order variants oversell past count_on_hand,
|
|
69
|
+
# so a reservation capped at on-hand stock would wrongly reject them;
|
|
70
|
+
# their cap is enforced by Stock::Quantifier#can_supply? instead.
|
|
71
|
+
next if stock_item.nil? || stock_item.backorderable? || variant.preorder?
|
|
69
72
|
|
|
70
73
|
[line_item, stock_item]
|
|
71
74
|
end
|
|
@@ -33,6 +33,9 @@ module Spree
|
|
|
33
33
|
product_ids = products.pluck(:id)
|
|
34
34
|
taxon_ids.each { |id| Spree::Taxon.reset_counters(id, :classifications) }
|
|
35
35
|
product_ids.each { |id| Spree::Product.reset_counters(id, :classifications) }
|
|
36
|
+
# Recompute the descendant-inclusive products_count for the taxons and
|
|
37
|
+
# their ancestors (bulk insert skips Classification callbacks).
|
|
38
|
+
Spree::Taxon.recalculate_products_count(taxon_ids)
|
|
36
39
|
|
|
37
40
|
# clear cache & index products
|
|
38
41
|
Spree::Product.where(id: product_ids).touch_all
|
|
@@ -48,6 +48,9 @@ module Spree
|
|
|
48
48
|
# update counter caches
|
|
49
49
|
taxon_ids.each { |id| Spree::Taxon.reset_counters(id, :classifications) }
|
|
50
50
|
product_ids.each { |id| Spree::Product.reset_counters(id, :classifications) }
|
|
51
|
+
# Recompute the descendant-inclusive products_count for the taxons and
|
|
52
|
+
# their ancestors (delete_all skips Classification callbacks).
|
|
53
|
+
Spree::Taxon.recalculate_products_count(taxon_ids)
|
|
51
54
|
|
|
52
55
|
# clear cache & index products
|
|
53
56
|
Spree::Product.where(id: product_ids).touch_all
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
class AdminUserEmailSubscriber < Spree::Subscriber
|
|
5
|
+
subscribes_to 'admin_user.password_reset_requested'
|
|
6
|
+
|
|
7
|
+
def handle(event)
|
|
8
|
+
user = Spree.admin_user_class.find_by(email: event.payload['email'])
|
|
9
|
+
return unless user
|
|
10
|
+
|
|
11
|
+
store = find_store(event)
|
|
12
|
+
|
|
13
|
+
AdminUserMailer.password_reset_email(
|
|
14
|
+
user,
|
|
15
|
+
event.payload['reset_token'],
|
|
16
|
+
store,
|
|
17
|
+
redirect_url: event.payload['redirect_url']
|
|
18
|
+
).deliver_later
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def find_store(event)
|
|
24
|
+
Spree::Store.find_by_prefix_id(event.payload['store_id']) ||
|
|
25
|
+
Spree::Current.store ||
|
|
26
|
+
Spree::Store.default
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
# Emails the import's owner when processing finishes — including after a
|
|
5
|
+
# failed-rows retry pass, which publishes `import.completed` again.
|
|
6
|
+
# App-created imports (API key without an admin creator) have no recipient
|
|
7
|
+
# and send nothing.
|
|
8
|
+
#
|
|
9
|
+
# We use async: false because this subscriber only enqueues the mail job.
|
|
10
|
+
class ImportEmailSubscriber < Spree::Subscriber
|
|
11
|
+
subscribes_to 'import.completed', async: false
|
|
12
|
+
|
|
13
|
+
on 'import.completed', :send_import_done_email
|
|
14
|
+
|
|
15
|
+
def send_import_done_email(event)
|
|
16
|
+
import_id = event.payload['id']
|
|
17
|
+
return unless import_id
|
|
18
|
+
|
|
19
|
+
import = Spree::Import.find_by_prefix_id(import_id)
|
|
20
|
+
return unless import
|
|
21
|
+
return if import.user&.email.blank?
|
|
22
|
+
|
|
23
|
+
Spree::ImportMailer.import_done(import).deliver_later
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -1,46 +1,43 @@
|
|
|
1
1
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
2
|
-
<html>
|
|
2
|
+
<html lang="<%= I18n.locale %>">
|
|
3
3
|
<head>
|
|
4
4
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
5
5
|
<meta name="x-apple-disable-message-reformatting"/>
|
|
6
6
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
|
7
|
+
<meta name="color-scheme" content="light"/>
|
|
8
|
+
<meta name="supported-color-schemes" content="light"/>
|
|
7
9
|
|
|
8
10
|
<%= render partial: 'spree/shared/base_mailer_stylesheets' %>
|
|
11
|
+
<%# Per-email styles — templates add their own via `content_for :head` %>
|
|
12
|
+
<%= yield :head %>
|
|
9
13
|
</head>
|
|
10
|
-
<body>
|
|
11
|
-
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
14
|
+
<body class="email-bg" style="margin: 0; padding: 0; -webkit-text-size-adjust: none;" bgcolor="#FFFFFF">
|
|
15
|
+
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation" bgcolor="#FFFFFF">
|
|
12
16
|
<tr>
|
|
13
17
|
<td align="center">
|
|
14
18
|
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
15
|
-
<%= render partial: 'spree/shared/base_mailer_header' %>
|
|
16
19
|
<!-- Email Body -->
|
|
17
20
|
<tr>
|
|
18
|
-
<td class="email-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<tbody>
|
|
21
|
+
<td class="email-card" bgcolor="#F6F6F6">
|
|
22
|
+
<%= render partial: 'spree/shared/base_mailer_header' %>
|
|
23
|
+
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
22
24
|
<tr>
|
|
23
|
-
<td class="content-cell">
|
|
24
|
-
|
|
25
|
-
<%= yield %>
|
|
26
|
-
</div>
|
|
25
|
+
<td class="content-cell f-fallback" align="left">
|
|
26
|
+
<%= yield %>
|
|
27
27
|
</td>
|
|
28
28
|
</tr>
|
|
29
|
-
</tbody>
|
|
30
29
|
</table>
|
|
31
30
|
</td>
|
|
32
31
|
</tr>
|
|
32
|
+
<!-- Email Footer -->
|
|
33
33
|
<tr>
|
|
34
|
-
<td>
|
|
34
|
+
<td class="email-footer" align="center">
|
|
35
35
|
<%= render partial: 'spree/shared/base_mailer_footer' %>
|
|
36
36
|
</td>
|
|
37
37
|
</tr>
|
|
38
38
|
</table>
|
|
39
39
|
</td>
|
|
40
40
|
</tr>
|
|
41
|
-
<tr>
|
|
42
|
-
<td height="60px"> </td>
|
|
43
|
-
</tr>
|
|
44
41
|
</table>
|
|
45
42
|
</body>
|
|
46
43
|
</html>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%= render layout: 'spree/shared/mailer_hero' do %>
|
|
2
|
+
<h1><%= Spree.t('admin_user_mailer.confirmation_email.action') %></h1>
|
|
3
|
+
<p class="greeting"><%= Spree.t('admin_user_mailer.confirmation_email.greeting', name: @user.first_name.presence || @user.email) %></p>
|
|
4
|
+
<p><%= Spree.t('admin_user_mailer.confirmation_email.instructions', store_name: @current_store.name) %></p>
|
|
5
|
+
<%= render 'spree/shared/mailer_button', url: @confirmation_url, label: Spree.t('admin_user_mailer.confirmation_email.action') %>
|
|
6
|
+
<p class="sub"><%= Spree.t('admin_user_mailer.confirmation_email.ignore_notice') %></p>
|
|
7
|
+
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%= render layout: 'spree/shared/mailer_hero' do %>
|
|
2
|
+
<h1><%= Spree.t('admin_user_mailer.password_reset_email.action') %></h1>
|
|
3
|
+
<p class="greeting"><%= Spree.t('admin_user_mailer.password_reset_email.greeting', name: @user.first_name.presence || @user.email) %></p>
|
|
4
|
+
<p><%= Spree.t('admin_user_mailer.password_reset_email.instructions', store_name: @current_store.name) %></p>
|
|
5
|
+
<%= render 'spree/shared/mailer_button', url: @reset_url, label: Spree.t('admin_user_mailer.password_reset_email.action') %>
|
|
6
|
+
<p class="sub"><%= Spree.t('admin_user_mailer.password_reset_email.expiry_notice') %></p>
|
|
7
|
+
<p class="sub"><%= Spree.t('admin_user_mailer.password_reset_email.ignore_notice') %></p>
|
|
8
|
+
<% end %>
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<% if spree.respond_to?(:admin_export_url) %>
|
|
8
|
-
<p>
|
|
9
|
-
<%= link_to @export.attachment.filename, spree.admin_export_url(@export, host: @current_store.formatted_url) %>
|
|
10
|
-
</p>
|
|
1
|
+
<%= render layout: 'spree/shared/mailer_hero' do %>
|
|
2
|
+
<h1><%= Spree.t('export_mailer.greeting', user_name: @export.user&.first_name) %></h1>
|
|
3
|
+
<p><%= Spree.t('export_mailer.export_done.message') %></p>
|
|
4
|
+
<% if @export.results_url.present? %>
|
|
5
|
+
<%= render 'spree/shared/mailer_button', url: @export.results_url, label: @export.attachment.filename %>
|
|
6
|
+
<% end %>
|
|
11
7
|
<% end %>
|
|
12
|
-
<p>
|
|
13
|
-
<%= Spree.t('export_mailer.thanks') %><br />
|
|
14
|
-
<%= "#{current_store.name}" %>
|
|
15
|
-
</p>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<%= render layout: 'spree/shared/mailer_hero' do %>
|
|
2
|
+
<h1><%= Spree.t('import_mailer.greeting', user_name: @import.user&.first_name) %></h1>
|
|
3
|
+
<p><%= Spree.t('import_mailer.import_done.message', completed_count: @import.rows_status_counts['completed'] || 0) %></p>
|
|
4
|
+
<% failed_count = @import.rows_status_counts['failed'] || 0 %>
|
|
5
|
+
<% if failed_count > 0 %>
|
|
6
|
+
<p><strong><%= Spree.t('import_mailer.import_done.failed_message', failed_count: failed_count) %></strong></p>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% results_url = @import.results_page_url %>
|
|
9
|
+
<% if results_url.present? %>
|
|
10
|
+
<%= render 'spree/shared/mailer_button', url: results_url, label: Spree.t('import_mailer.import_done.view_results') %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<%= Spree.t('invitation_mailer.invitation_accepted.body', invitee_name: @invitation.invitee&.name, resource_name: @invitation.resource.name) %>
|
|
6
|
-
</p>
|
|
7
|
-
|
|
8
|
-
<p>
|
|
9
|
-
<%= Spree.t('invitation_mailer.thanks') %><br />
|
|
10
|
-
<%= current_store.name %>
|
|
11
|
-
</p>
|
|
12
|
-
|
|
1
|
+
<%= render layout: 'spree/shared/mailer_hero' do %>
|
|
2
|
+
<h1><%= Spree.t('invitation_mailer.greeting', user_name: @invitation.inviter.first_name) %></h1>
|
|
3
|
+
<p><%= Spree.t('invitation_mailer.invitation_accepted.body', invitee_name: @invitation.invitee&.name, resource_name: @invitation.resource.name) %></p>
|
|
4
|
+
<% end %>
|
|
@@ -1,25 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<p>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<p>
|
|
13
|
-
<%= link_to Spree.t(:accept), spree.admin_invitation_url(@invitation, token: @invitation.token, host: @invitation.store.formatted_url) %>
|
|
14
|
-
</p>
|
|
15
|
-
<% else %>
|
|
16
|
-
<p>
|
|
17
|
-
<%= link_to Spree.t(:accept), main_app.admin_invitation_acceptance_url(@invitation) %>
|
|
18
|
-
</p>
|
|
1
|
+
<%= render layout: 'spree/shared/mailer_hero' do %>
|
|
2
|
+
<h1><%= Spree.t('invitation_mailer.invitation_email.subject', resource_name: @invitation.resource.name) %></h1>
|
|
3
|
+
<p class="greeting"><%= Spree.t('invitation_mailer.greeting', user_name: @invitation.invitee&.first_name || @invitation.email) %></p>
|
|
4
|
+
<p><%= Spree.t('invitation_mailer.invitation_email.body', inviter_name: @invitation.inviter.name, resource_name: @invitation.resource.name) %></p>
|
|
5
|
+
<p><%= Spree.t('invitation_mailer.invitation_email.link_description') %></p>
|
|
6
|
+
<% accept_url = if spree.respond_to?(:admin_invitation_url)
|
|
7
|
+
spree.admin_invitation_url(@invitation, token: @invitation.token, host: @invitation.store.formatted_url)
|
|
8
|
+
else
|
|
9
|
+
main_app.admin_invitation_acceptance_url(@invitation)
|
|
10
|
+
end %>
|
|
11
|
+
<%= render 'spree/shared/mailer_button', url: accept_url, label: Spree.t(:accept) %>
|
|
19
12
|
<% end %>
|
|
20
|
-
|
|
21
|
-
<p>
|
|
22
|
-
<%= Spree.t('invitation_mailer.thanks') %><br />
|
|
23
|
-
<%= current_store.name %>
|
|
24
|
-
</p>
|
|
25
|
-
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<% if spree.respond_to?(:admin_report_url) %>
|
|
8
|
-
<p>
|
|
9
|
-
<%= link_to @report.attachment.filename, spree.admin_report_url(@report, host: @current_store.formatted_url) %>
|
|
10
|
-
</p>
|
|
1
|
+
<%= render layout: 'spree/shared/mailer_hero' do %>
|
|
2
|
+
<h1><%= Spree.t('report_mailer.greeting', user_name: @report.user&.first_name) %></h1>
|
|
3
|
+
<p><%= Spree.t('report_mailer.report_done.message') %></p>
|
|
4
|
+
<% if spree.respond_to?(:admin_report_url) %>
|
|
5
|
+
<%= render 'spree/shared/mailer_button', url: spree.admin_report_url(@report, host: @current_store.formatted_url), label: @report.attachment.filename %>
|
|
6
|
+
<% end %>
|
|
11
7
|
<% end %>
|
|
12
|
-
<p>
|
|
13
|
-
<%= Spree.t('report_mailer.thanks') %><br />
|
|
14
|
-
<%= "#{current_store.name}" %>
|
|
15
|
-
</p>
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
<!-- You can override this template to design your own footer.
|
|
2
|
-
<table
|
|
1
|
+
<!-- You can override this template to design your own footer. -->
|
|
2
|
+
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
3
3
|
<tr>
|
|
4
|
-
<td
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
<td align="center">
|
|
5
|
+
<div class="footer_content f-fallback">
|
|
6
|
+
<p class="footer_brand">
|
|
7
|
+
<%= link_to current_store.name, current_store.storefront_url %>
|
|
8
|
+
</p>
|
|
9
|
+
<% if current_store.address.present? %>
|
|
10
|
+
<p class="footer_meta"><%= current_store.address %></p>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% if current_store.customer_support_email.present? %>
|
|
13
|
+
<p class="footer_meta"><%= mail_to current_store.customer_support_email %></p>
|
|
14
|
+
<% end %>
|
|
15
|
+
<p class="footer_note">© <%= Time.current.year %> <%= current_store.name %></p>
|
|
16
|
+
</div>
|
|
9
17
|
</td>
|
|
10
18
|
</tr>
|
|
11
19
|
</table>
|
|
12
|
-
-->
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<!-- You can override this template to design your own header. -->
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
</
|
|
2
|
+
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
3
|
+
<tr>
|
|
4
|
+
<td class="email-masthead" align="center">
|
|
5
|
+
<%= render 'spree/shared/mailer_logo', logo: current_store.mailer_logo.attached? ? current_store.mailer_logo : current_store.logo, height: 32 %>
|
|
6
|
+
</td>
|
|
7
|
+
</tr>
|
|
8
|
+
</table>
|