spree_core 6.0.0 → 6.0.2
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/README.md +2 -2
- data/app/jobs/spree/seller_transfers/execute_pending_due_job.rb +7 -5
- data/app/jobs/spree/seller_transfers/execute_pending_job.rb +37 -17
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade/store_regional_settings.rb +63 -0
- data/app/models/concerns/spree/admin_user_methods.rb +23 -0
- data/app/models/concerns/spree/translatable_resource.rb +19 -0
- data/app/models/concerns/spree/user_roles.rb +9 -4
- data/app/models/spree/invitation.rb +11 -2
- data/app/models/spree/order.rb +3 -1
- data/app/models/spree/order_promotion.rb +3 -1
- data/app/models/spree/order_routing/strategy/rules.rb +8 -48
- data/app/models/spree/payment.rb +16 -1
- data/app/models/spree/payment_method/store_credit.rb +3 -1
- data/app/models/spree/payment_method.rb +14 -1
- data/app/models/spree/payment_source.rb +3 -1
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +1 -1
- data/app/models/spree/promotion.rb +10 -1
- data/app/models/spree/promotion_handler/cart.rb +7 -1
- data/app/models/spree/promotion_handler/coupon.rb +1 -29
- data/app/models/spree/refresh_token.rb +26 -0
- data/app/models/spree/seller_transfer.rb +51 -0
- data/app/models/spree/stock/coordinator.rb +37 -17
- data/app/models/spree/stock/packer.rb +19 -9
- data/app/models/spree/stock/prioritizer.rb +30 -15
- data/app/models/spree/store.rb +35 -2
- data/app/models/spree/variant.rb +1 -1
- data/app/services/spree/carts/update.rb +2 -1
- data/app/services/spree/promotions/detach_from_owner.rb +72 -0
- data/app/services/spree/stock_reservations/reserve.rb +78 -26
- data/app/workflows/spree/carts/recalculate.rb +5 -1
- data/app/workflows/spree/seller_transfers/reverse.rb +5 -44
- data/db/migrate/20260809150001_promote_shipping_categories_to_delivery_profiles.rb +15 -6
- data/db/migrate/20261029100001_add_lockout_columns_to_spree_admin_users.rb +8 -0
- data/db/migrate/20261030100001_scope_payment_sources_profile_id_uniqueness_to_payment_method.rb +38 -0
- data/db/migrate/20261031000001_add_last_active_at_to_spree_admin_users.rb +7 -0
- data/db/migrate/20261031100001_delete_orphaned_spree_option_value_variants.rb +10 -0
- data/lib/generators/spree/dummy/dummy_generator.rb +0 -1
- data/lib/generators/spree/dummy/templates/rails/database.yml +6 -9
- data/lib/generators/spree/dummy/templates/rails/test.rb +9 -5
- data/lib/generators/spree/dummy_model/templates/migration.rb.tt +1 -1
- data/lib/spree/checkout/default_requirements.rb +33 -5
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +1 -3
- data/lib/spree/testing_support/common_rake.rb +3 -2
- data/lib/spree/testing_support/lifecycle_events.rb +1 -3
- data/lib/spree/time_zones.rb +34 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +15 -1
- data/lib/tasks/delivery_migration.rake +17 -1
- data/lib/tasks/remove_orphan_order_promotions.rake +20 -0
- metadata +15 -78
- data/lib/generators/spree/authentication/dummy/dummy_generator.rb +0 -54
- data/lib/generators/spree/authentication/dummy/templates/authentication_helpers.rb.tt +0 -52
- data/lib/generators/spree/authentication/dummy/templates/create_spree_admin_users.rb.tt +0 -33
|
@@ -202,34 +202,15 @@ module Spree
|
|
|
202
202
|
kind: 'refund_reversal',
|
|
203
203
|
provider: @earning.provider,
|
|
204
204
|
status: 'pending'
|
|
205
|
-
}.merge(
|
|
205
|
+
}.merge(@earning.reversal_settlement(bounded))
|
|
206
206
|
)
|
|
207
207
|
end
|
|
208
208
|
end
|
|
209
209
|
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
# never join the batch that pays the earning it cancels — the seller would
|
|
213
|
-
# be paid in full for goods that came back, and the row would sit in a
|
|
214
|
-
# currency their account cannot pay.
|
|
215
|
-
#
|
|
216
|
-
# Prorated from the earning's own settled figure rather than converted at
|
|
217
|
-
# today's rate, so the money comes back at the rate it went out at.
|
|
218
|
-
def settlement_of(bounded)
|
|
219
|
-
return {} if @earning.settled_amount.blank? || @earning.amount.zero?
|
|
220
|
-
|
|
221
|
-
share = @earning.settled_amount * (bounded / @earning.amount)
|
|
222
|
-
|
|
223
|
-
{
|
|
224
|
-
settled_amount: -Spree::Money::Rounding.quantize(
|
|
225
|
-
share, Spree::Money::Rounding.precision(@earning.settlement_currency)
|
|
226
|
-
),
|
|
227
|
-
settled_currency: @earning.settled_currency
|
|
228
|
-
}
|
|
229
|
-
end
|
|
230
|
-
|
|
210
|
+
# Asked of the provider that made the earning — see
|
|
211
|
+
# {Spree::SellerTransfer#payout_provider_instance}.
|
|
231
212
|
def execute_reversal
|
|
232
|
-
|
|
213
|
+
@earning.payout_provider_instance.reverse!(reversal)
|
|
233
214
|
rescue Spree::Core::AmbiguousGatewayError => e
|
|
234
215
|
# Whether the clawback happened is the provider's to say. Recorded as
|
|
235
216
|
# such rather than as a refusal, so an operator reconciling knows which
|
|
@@ -238,31 +219,11 @@ module Spree
|
|
|
238
219
|
Rails.error.report(e, handled: true, context: { seller_transfer_id: reversal.id }, source: 'spree.core')
|
|
239
220
|
failure(reversal, e.message)
|
|
240
221
|
rescue StandardError => e
|
|
222
|
+
# Left retryable: `ExecutePendingJob` asks the provider again.
|
|
241
223
|
reversal.update!(status: 'processing')
|
|
242
224
|
Rails.error.report(e, handled: true, context: { seller_transfer_id: reversal.id }, source: 'spree.core')
|
|
243
225
|
failure(reversal, e.message)
|
|
244
226
|
end
|
|
245
|
-
|
|
246
|
-
# The provider that made the earning, not whichever one the store uses
|
|
247
|
-
# now. A marketplace that changes provider still has money sitting with
|
|
248
|
-
# the old one, and asking the new one to reverse a transfer it never
|
|
249
|
-
# made leaves the original standing — the seller keeps a refunded sale.
|
|
250
|
-
def provider
|
|
251
|
-
@provider ||= begin
|
|
252
|
-
configured = Spree.payout_providers.find { |candidate| candidate.to_s == @earning.provider }
|
|
253
|
-
|
|
254
|
-
# No silent fallback to whatever the store uses now. A provider that
|
|
255
|
-
# is no longer installed still holds the transfer this reverses, and
|
|
256
|
-
# handing the job to a different one would mark the row reversed
|
|
257
|
-
# while the money stayed where it was. An operator has to know.
|
|
258
|
-
if configured.nil?
|
|
259
|
-
raise Spree::Core::GatewayError,
|
|
260
|
-
"Payout provider #{@earning.provider} is not registered, so its transfer cannot be reversed"
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
configured.new
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
227
|
end
|
|
267
228
|
end
|
|
268
229
|
end
|
|
@@ -52,6 +52,7 @@ class PromoteShippingCategoriesToDeliveryProfiles < ActiveRecord::Migration[8.1]
|
|
|
52
52
|
remove_column :spree_product_types, :fulfillment_types
|
|
53
53
|
|
|
54
54
|
backfill_store_defaults
|
|
55
|
+
copy_first_zone_onto_methods
|
|
55
56
|
|
|
56
57
|
drop_table :spree_delivery_method_zones
|
|
57
58
|
end
|
|
@@ -141,13 +142,21 @@ class PromoteShippingCategoriesToDeliveryProfiles < ActiveRecord::Migration[8.1]
|
|
|
141
142
|
# never carried a category, so they all belong to the default profile.
|
|
142
143
|
MigrationProduct.where(store_id: store.id, delivery_profile_id: nil).update_all(delivery_profile_id: profile.id)
|
|
143
144
|
|
|
144
|
-
MigrationMethod.where(store_id: store.id).
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
MigrationMethod.where(store_id: store.id).update_all(delivery_profile_id: profile.id, delivery_origin_group_id: origin_group.id)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Runs for every method, including 5.x rows whose store_id is still nil: the
|
|
150
|
+
# link table is dropped right after, so this is the last chance to keep it.
|
|
151
|
+
def copy_first_zone_onto_methods
|
|
152
|
+
zone_ids_by_method = MigrationMethodZone.order(:id).pluck(:delivery_method_id, :delivery_zone_id).
|
|
153
|
+
group_by(&:first).transform_values { |pairs| pairs.map(&:last) }
|
|
154
|
+
|
|
155
|
+
zone_ids_by_method.each do |method_id, zone_ids|
|
|
156
|
+
if zone_ids.size > 1
|
|
157
|
+
say "Delivery method #{method_id} was linked to #{zone_ids.size} zones; keeping the first — recreate the method for the other zones if both were intended."
|
|
150
158
|
end
|
|
159
|
+
MigrationMethod.where(id: method_id).update_all(delivery_zone_id: zone_ids.first)
|
|
151
160
|
end
|
|
152
161
|
end
|
|
153
162
|
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Spree::AccountLockout reads these columns. Installs upgraded from a Devise
|
|
2
|
+
# admin model without :lockable reach 6.0 with neither of them.
|
|
3
|
+
class AddLockoutColumnsToSpreeAdminUsers < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
add_column :spree_admin_users, :failed_attempts, :integer unless column_exists?(:spree_admin_users, :failed_attempts)
|
|
6
|
+
add_column :spree_admin_users, :locked_at, :datetime unless column_exists?(:spree_admin_users, :locked_at)
|
|
7
|
+
end
|
|
8
|
+
end
|
data/db/migrate/20261030100001_scope_payment_sources_profile_id_uniqueness_to_payment_method.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class ScopePaymentSourcesProfileIdUniquenessToPaymentMethod < ActiveRecord::Migration[8.1]
|
|
2
|
+
LEGACY_INDEX_NAME = 'index_payment_sources_on_type_and_gateway_payment_profile_id'.freeze
|
|
3
|
+
INDEX_NAME = 'index_payment_sources_on_type_profile_id_and_payment_method'.freeze
|
|
4
|
+
|
|
5
|
+
def up
|
|
6
|
+
remove_index :spree_payment_sources, name: LEGACY_INDEX_NAME
|
|
7
|
+
add_index :spree_payment_sources, [:type, :gateway_payment_profile_id, :payment_method_id],
|
|
8
|
+
unique: true, name: INDEX_NAME
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# The legacy index cannot come back once a buyer holds the same gateway profile under two
|
|
12
|
+
# payment methods, which is what `up` exists to allow. Checked before anything is dropped so a
|
|
13
|
+
# refusal leaves both indexes intact on MySQL, which has no transactional DDL.
|
|
14
|
+
def down
|
|
15
|
+
if cross_payment_method_duplicates?
|
|
16
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
17
|
+
'Payment sources share a gateway_payment_profile_id across payment methods, which ' \
|
|
18
|
+
'the globally unique index cannot represent. Reconcile those rows before rolling back.'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
remove_index :spree_payment_sources, name: INDEX_NAME
|
|
22
|
+
add_index :spree_payment_sources, [:type, :gateway_payment_profile_id],
|
|
23
|
+
unique: true, name: LEGACY_INDEX_NAME
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# NULLs are distinct in a unique index, so only fully populated pairs could ever have collided.
|
|
29
|
+
def cross_payment_method_duplicates?
|
|
30
|
+
connection.select_value(<<~SQL).present?
|
|
31
|
+
SELECT 1 FROM spree_payment_sources
|
|
32
|
+
WHERE type IS NOT NULL AND gateway_payment_profile_id IS NOT NULL
|
|
33
|
+
GROUP BY type, gateway_payment_profile_id
|
|
34
|
+
HAVING COUNT(*) > 1
|
|
35
|
+
LIMIT 1
|
|
36
|
+
SQL
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Written whenever a session starts or refreshes (see Spree::RefreshToken), so
|
|
2
|
+
# it outlives the sessions themselves, which signing out deletes.
|
|
3
|
+
class AddLastActiveAtToSpreeAdminUsers < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
add_column :spree_admin_users, :last_active_at, :datetime
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class DeleteOrphanedSpreeOptionValueVariants < ActiveRecord::Migration[8.1]
|
|
2
|
+
# Replacing a variant's option_value_variants used to nullify the old join rows instead of
|
|
3
|
+
# deleting them. Rows with no variant belong to nothing and block new variants from reusing
|
|
4
|
+
# their option values through the uniqueness validation scoped to a NULL variant_id.
|
|
5
|
+
def up
|
|
6
|
+
execute 'DELETE FROM spree_option_value_variants WHERE variant_id IS NULL'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def down; end
|
|
10
|
+
end
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
<% if agent_number = ENV['TC_AGENT_NUMBER']
|
|
2
|
-
database_prefix = agent_number + '_'
|
|
3
|
-
end %>
|
|
4
1
|
<% if options[:lib_name]
|
|
5
2
|
lib_name = options[:lib_name].gsub('/', '_')
|
|
6
3
|
end %>
|
|
@@ -37,13 +34,13 @@ mysql: &mysql
|
|
|
37
34
|
|
|
38
35
|
development:
|
|
39
36
|
<<: *mysql
|
|
40
|
-
database: <%=
|
|
37
|
+
database: <%= lib_name %>_spree_development
|
|
41
38
|
test:
|
|
42
39
|
<<: *mysql
|
|
43
|
-
database: <%=
|
|
40
|
+
database: <%= lib_name %>_spree_test<%%= ENV['TEST_ENV_NUMBER'] %>
|
|
44
41
|
production:
|
|
45
42
|
<<: *mysql
|
|
46
|
-
database: <%=
|
|
43
|
+
database: <%= lib_name %>_spree_production
|
|
47
44
|
<% when 'postgres' %>
|
|
48
45
|
postgres: &postgres
|
|
49
46
|
adapter: postgresql
|
|
@@ -61,13 +58,13 @@ postgres: &postgres
|
|
|
61
58
|
|
|
62
59
|
development:
|
|
63
60
|
<<: *postgres
|
|
64
|
-
database: <%=
|
|
61
|
+
database: <%= lib_name %>_spree_development
|
|
65
62
|
test:
|
|
66
63
|
<<: *postgres
|
|
67
|
-
database: <%=
|
|
64
|
+
database: <%= lib_name %>_spree_test<%%= ENV['TEST_ENV_NUMBER'] %>
|
|
68
65
|
production:
|
|
69
66
|
<<: *postgres
|
|
70
|
-
database: <%=
|
|
67
|
+
database: <%= lib_name %>_spree_production
|
|
71
68
|
<% else %>
|
|
72
69
|
development:
|
|
73
70
|
adapter: sqlite3
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
Rails.application.configure do
|
|
2
2
|
# Settings specified here will take precedence over those in config/application.rb
|
|
3
3
|
|
|
4
4
|
# The test environment is used exclusively to run your application's
|
|
5
5
|
# test suite. You never need to work with it otherwise. Remember that
|
|
6
6
|
# your test database is "scratch space" for the test suite and is wiped
|
|
7
7
|
# and recreated between test runs. Don't rely on the data there!
|
|
8
|
-
config.
|
|
8
|
+
config.enable_reloading = false
|
|
9
9
|
|
|
10
10
|
# Configure static asset server for tests with Cache-Control for performance
|
|
11
11
|
config.public_file_server.enabled = true
|
|
@@ -17,8 +17,9 @@ Dummy::Application.configure do
|
|
|
17
17
|
|
|
18
18
|
config.eager_load = false
|
|
19
19
|
|
|
20
|
-
# Raise exceptions
|
|
21
|
-
|
|
20
|
+
# Raise unexpected exceptions; render the ones Rails maps to a status, such
|
|
21
|
+
# as a 404 for an unknown route, the way production does.
|
|
22
|
+
config.action_dispatch.show_exceptions = :rescuable
|
|
22
23
|
|
|
23
24
|
# Disable request forgery protection in test environment
|
|
24
25
|
config.action_controller.allow_forgery_protection = false
|
|
@@ -138,7 +139,10 @@ Dummy::Application.configure do
|
|
|
138
139
|
%r{/spree/orders/complete\.rb:\d+:in '[^']*#place_sibling'},
|
|
139
140
|
# Each requested line is priced, stock-checked, saved and taxed as it
|
|
140
141
|
# is written.
|
|
141
|
-
%r{/spree/carts/upsert_items\.rb:\d+:in '[^']*#apply_item'}
|
|
142
|
+
%r{/spree/carts/upsert_items\.rb:\d+:in '[^']*#apply_item'},
|
|
143
|
+
# An import writes each row on its own, inside the request when the
|
|
144
|
+
# import is set to run inline.
|
|
145
|
+
%r{/spree/import_row\.rb:\d+:in '[^']*#process!'}
|
|
142
146
|
]
|
|
143
147
|
|
|
144
148
|
# A purchase's billing and shipping addresses, and a transfer's two
|
|
@@ -10,6 +10,12 @@ module Spree
|
|
|
10
10
|
#
|
|
11
11
|
# @see Requirements
|
|
12
12
|
class DefaultRequirements
|
|
13
|
+
# Everything the discontinuation and stock checks read, so a set of
|
|
14
|
+
# lines is checked at once.
|
|
15
|
+
STOCK_CHECK_ASSOCIATIONS = {
|
|
16
|
+
variant: [{ product: %i[store product_publications] }, { stock_levels: %i[stock_location active_stock_reservations] }]
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
13
19
|
# @param cart [Spree::Cart]
|
|
14
20
|
def initialize(cart)
|
|
15
21
|
@cart = cart
|
|
@@ -33,6 +39,7 @@ module Spree
|
|
|
33
39
|
r << req('address', 'email', I18n.t('spree.checkout_requirements.email_required')) unless @cart.email.present?
|
|
34
40
|
r << req('address', 'ship_address', I18n.t('spree.checkout_requirements.ship_address_required')) if @cart.shipping_address_required? && @cart.ship_address.blank?
|
|
35
41
|
r << req('delivery', 'delivery_method', I18n.t('spree.checkout_requirements.delivery_method_required')) if delivery_step_required? && !delivery_method_selected?
|
|
42
|
+
r.concat(undeliverable_errors)
|
|
36
43
|
r << req('payment', 'payment', I18n.t('spree.checkout_requirements.payment_required')) if payment_required? && !payment_satisfied?
|
|
37
44
|
r << req('address', 'po_number', I18n.t('spree.checkout_requirements.po_number_required')) if po_number_missing?
|
|
38
45
|
r << order_minimum_requirement if below_order_minimum?
|
|
@@ -102,16 +109,37 @@ module Spree
|
|
|
102
109
|
end
|
|
103
110
|
end
|
|
104
111
|
|
|
112
|
+
# A rebuild drops a proposal no method can quote with only a transient
|
|
113
|
+
# warning, so part of a line can be in no fulfillment. Advisory, so the
|
|
114
|
+
# buyer reads it while they can still change the address or the cart.
|
|
115
|
+
def undeliverable_errors
|
|
116
|
+
return [] unless delivery_step_required? && @cart.fulfillments.any?
|
|
117
|
+
|
|
118
|
+
undelivered_line_items.map do |line_item|
|
|
119
|
+
req('delivery', 'line_items', I18n.t('spree.cart_line_item.delivery_unavailable', li_name: line_item.name),
|
|
120
|
+
code: 'delivery_unavailable')
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A line short of stock is packed short too; the stock check already
|
|
125
|
+
# names it. Only lines short of a fulfillment pay for that check, so a
|
|
126
|
+
# cart read stays at one query.
|
|
127
|
+
def undelivered_line_items
|
|
128
|
+
allocated = Spree::FulfillmentItem.where(fulfillment_id: @cart.fulfillments.select(:id))
|
|
129
|
+
.group(:line_item_id).sum(:quantity)
|
|
130
|
+
short = @cart.line_items.select { |line_item| allocated.fetch(line_item.id, 0) < line_item.quantity }
|
|
131
|
+
return [] if short.empty?
|
|
132
|
+
|
|
133
|
+
ActiveRecord::Associations::Preloader.new(records: short, associations: STOCK_CHECK_ASSOCIATIONS).call
|
|
134
|
+
short.reject { |line_item| discontinued?(line_item) || !line_item.sufficient_stock? }
|
|
135
|
+
end
|
|
136
|
+
|
|
105
137
|
def discontinued?(line_item)
|
|
106
138
|
line_item.variant.nil? || line_item.variant.discontinued? || line_item.variant.product.discontinued?
|
|
107
139
|
end
|
|
108
140
|
|
|
109
141
|
def completion_line_items
|
|
110
|
-
|
|
111
|
-
# at once.
|
|
112
|
-
@completion_line_items ||= @cart.line_items.includes(
|
|
113
|
-
variant: [{ product: %i[store product_publications] }, { stock_levels: %i[stock_location active_stock_reservations] }]
|
|
114
|
-
).to_a
|
|
142
|
+
@completion_line_items ||= @cart.line_items.includes(STOCK_CHECK_ASSOCIATIONS).to_a
|
|
115
143
|
end
|
|
116
144
|
|
|
117
145
|
def delivery_step_required?
|
data/lib/spree/core/version.rb
CHANGED
data/lib/spree/core.rb
CHANGED
|
@@ -39,8 +39,6 @@ require 'ransack'
|
|
|
39
39
|
require 'active_storage_validations'
|
|
40
40
|
require 'wannabe_bool'
|
|
41
41
|
require 'geocoder'
|
|
42
|
-
|
|
43
|
-
require 'safely_block'
|
|
44
42
|
require 'ar_lazy_preload'
|
|
45
43
|
require 'sqids'
|
|
46
44
|
|
|
@@ -125,7 +123,6 @@ module Spree
|
|
|
125
123
|
collections: :default,
|
|
126
124
|
stock_location_stock_levels: :default,
|
|
127
125
|
coupon_codes: :default,
|
|
128
|
-
themes: :default,
|
|
129
126
|
addresses: :default,
|
|
130
127
|
gift_cards: :default,
|
|
131
128
|
webhooks: :default,
|
|
@@ -666,6 +663,7 @@ require 'spree/i18n'
|
|
|
666
663
|
require 'spree/iso_data'
|
|
667
664
|
require 'spree/localized_number'
|
|
668
665
|
require 'spree/measurement'
|
|
666
|
+
require 'spree/time_zones'
|
|
669
667
|
require 'spree/translations'
|
|
670
668
|
require 'spree/money'
|
|
671
669
|
require 'spree/service_module'
|
|
@@ -3,11 +3,12 @@ require 'generators/spree/dummy_model/dummy_model_generator'
|
|
|
3
3
|
|
|
4
4
|
desc 'Generates a dummy app for testing'
|
|
5
5
|
namespace :common do
|
|
6
|
+
# :authentication is ignored (test apps use the gem-owned auth models); it stays
|
|
7
|
+
# so extensions passing positional arguments keep working.
|
|
6
8
|
task :test_app, [:authentication, :user_class, :admin_user_class] do |_t, args|
|
|
7
9
|
# Support both Rake::TaskArguments (via invoke) and Hash (via execute)
|
|
8
10
|
# When using execute with a Hash, args IS the hash directly
|
|
9
11
|
defaults = {
|
|
10
|
-
authentication: 'dummy',
|
|
11
12
|
user_class: 'Spree::Customer',
|
|
12
13
|
admin_user_class: 'Spree::AdminUser',
|
|
13
14
|
}
|
|
@@ -45,7 +46,7 @@ namespace :common do
|
|
|
45
46
|
File.exist?(File.expand_path('../../../../spree.gemspec', __dir__))
|
|
46
47
|
bundle_exec = use_root_gemfile ? "bundle exec --gemfile=#{root_gemfile}" : 'bundle exec'
|
|
47
48
|
puts 'Running Spree install generator...'
|
|
48
|
-
system("#{bundle_exec} rails g spree:install --force --auto-accept --migrate=false --seed=false --sample=false --user_class=#{args[:user_class]} --admin_user_class=#{args[:admin_user_class]}
|
|
49
|
+
system("#{bundle_exec} rails g spree:install --force --auto-accept --migrate=false --seed=false --sample=false --user_class=#{args[:user_class]} --admin_user_class=#{args[:admin_user_class]}")
|
|
49
50
|
|
|
50
51
|
unless ENV['NO_MIGRATE']
|
|
51
52
|
puts 'Setting up dummy database...'
|
|
@@ -23,9 +23,7 @@ shared_examples_for 'lifecycle events' do |factory: nil, event_prefix: nil|
|
|
|
23
23
|
|
|
24
24
|
# update_attribute, not update!: Spree::Fulfillment#update! shadows the
|
|
25
25
|
# Active Record method with the legacy state-recalculation API.
|
|
26
|
-
|
|
27
|
-
record.update_attribute(:updated_at, Time.current)
|
|
28
|
-
end
|
|
26
|
+
record.update_attribute(:updated_at, 1.minute.from_now)
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
it 'does not publish updated event on a bare touch' do
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Time zones are stored and exchanged as IANA identifiers (`Europe/Berlin`),
|
|
3
|
+
# the names browsers and every non-Ruby client understand. Rails' display
|
|
4
|
+
# names ("Eastern Time (US & Canada)") are converted to them on the way in.
|
|
5
|
+
module TimeZones
|
|
6
|
+
# The zones clients are offered: the IANA zones Rails gives a display name
|
|
7
|
+
# to, with plain `UTC` standing in for `Etc/UTC`. Any other valid IANA
|
|
8
|
+
# identifier is accepted too. The list comes with the Rails version rather
|
|
9
|
+
# than the machine's time zone database, so generated types match wherever
|
|
10
|
+
# the same Rails version runs.
|
|
11
|
+
SUGGESTED = (ActiveSupport::TimeZone::MAPPING.values - ['Etc/UTC'] + ['UTC']).uniq.sort.freeze
|
|
12
|
+
|
|
13
|
+
# @param identifier [String, nil]
|
|
14
|
+
# @return [Boolean] whether the value is an IANA time zone identifier
|
|
15
|
+
def self.valid?(identifier)
|
|
16
|
+
return false if identifier.blank?
|
|
17
|
+
|
|
18
|
+
TZInfo::Timezone.get(identifier.to_s)
|
|
19
|
+
true
|
|
20
|
+
rescue TZInfo::InvalidTimezoneIdentifier
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param value [String, nil] an IANA identifier or a Rails time zone name
|
|
25
|
+
# @return [String, nil] the IANA identifier, or the value unchanged when it
|
|
26
|
+
# names no zone the installed time zone database knows
|
|
27
|
+
def self.normalize(value)
|
|
28
|
+
return value if value.blank? || valid?(value)
|
|
29
|
+
|
|
30
|
+
identifier = ActiveSupport::TimeZone::MAPPING[value.to_s]
|
|
31
|
+
valid?(identifier) ? identifier : value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -212,7 +212,8 @@ steps:
|
|
|
212
212
|
task: "spree:migrate_shipping_to_delivery"
|
|
213
213
|
notes: |
|
|
214
214
|
Rewrites stored 'Spree::Shipment' class-name strings (state changes,
|
|
215
|
-
stock movements, legacy adjustments)
|
|
215
|
+
stock movements, legacy adjustments) and the calculators' 'Spree::ShippingMethod'
|
|
216
|
+
calculable type (so delivery methods find their calculator), renames the terminal fulfillment
|
|
216
217
|
status 'shipped' -> 'fulfilled', backfills the fulfillment provider on
|
|
217
218
|
delivery methods (DigitalDelivery calculators become the Digital
|
|
218
219
|
provider) and copies the provider-derived type onto existing
|
|
@@ -597,6 +598,19 @@ steps:
|
|
|
597
598
|
is dropped in 6.1. Idempotent — only rows without a capture_method are
|
|
598
599
|
written.
|
|
599
600
|
|
|
601
|
+
- id: store_regional_settings
|
|
602
|
+
name: "Convert store time zones and admin languages to standard codes"
|
|
603
|
+
task_class: "Spree::MaintenanceTasks::Upgrade::StoreRegionalSettings"
|
|
604
|
+
notes: |
|
|
605
|
+
A store's time zone is now always an IANA name such as America/New_York,
|
|
606
|
+
and its admin language a locale code Spree lists, the forms the API
|
|
607
|
+
accepts and browsers understand. Rails time zone names such as
|
|
608
|
+
"Eastern Time (US & Canada)" and locale spellings such as en_US are
|
|
609
|
+
converted. A time zone that names no zone is reset to the store default,
|
|
610
|
+
which is what the store already ran on; an admin language Spree does not
|
|
611
|
+
list is cleared, so staff see their own language. Idempotent — stores
|
|
612
|
+
already on standard codes are skipped.
|
|
613
|
+
|
|
600
614
|
- id: product_types_backfill
|
|
601
615
|
name: "Backfill product type store ownership"
|
|
602
616
|
task: "spree:product_types:backfill"
|
|
@@ -29,6 +29,11 @@ namespace :spree do
|
|
|
29
29
|
say.call "spree_adjustments.adjustable_type: #{count} rows Spree::Shipment → Spree::Fulfillment"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Calculators are polymorphic on the renamed shipping method; without this
|
|
33
|
+
# DeliveryMethod#calculator finds nothing.
|
|
34
|
+
count = Spree::Calculator.where(calculable_type: 'Spree::ShippingMethod').in_batches(of: batch_size).update_all(calculable_type: 'Spree::DeliveryMethod')
|
|
35
|
+
say.call "spree_calculators.calculable_type: #{count} rows Spree::ShippingMethod → Spree::DeliveryMethod"
|
|
36
|
+
|
|
32
37
|
# The name column distinguishes the machine ('shipment' rows become
|
|
33
38
|
# 'fulfillment'); the recorded states keep their history except the
|
|
34
39
|
# renamed terminal value.
|
|
@@ -171,6 +176,11 @@ namespace :spree do
|
|
|
171
176
|
end
|
|
172
177
|
end
|
|
173
178
|
|
|
179
|
+
# 5.x methods have no store until backfill_delivery_and_stock_store_ids
|
|
180
|
+
# runs (later in the upgrade), and a DeliveryZone requires one. Resolve
|
|
181
|
+
# them to the default store, exactly as that backfill will.
|
|
182
|
+
default_store_id = Spree::Store.default&.id
|
|
183
|
+
|
|
174
184
|
legacy_zones.find_each do |zone|
|
|
175
185
|
if Spree::DeliveryZone.exists?(id: zone.id) && !migrated.value?(zone.id)
|
|
176
186
|
puts "SKIP zone #{zone.id} (#{zone.name}): a DeliveryZone with the same id already exists — resolve manually"
|
|
@@ -178,7 +188,7 @@ namespace :spree do
|
|
|
178
188
|
end
|
|
179
189
|
|
|
180
190
|
methods_by_home = Spree::DeliveryMethod.unscoped.where(delivery_zone_id: zone.id).
|
|
181
|
-
group_by { |method| [method.store_id, method.delivery_profile_id] }
|
|
191
|
+
group_by { |method| [method.store_id || default_store_id, method.delivery_profile_id] }
|
|
182
192
|
|
|
183
193
|
methods_by_home.each do |(store_id, profile_id), methods|
|
|
184
194
|
profile_id ||= Spree::DeliveryProfile.where(store_id: store_id, default: true).pick(:id)
|
|
@@ -208,6 +218,12 @@ namespace :spree do
|
|
|
208
218
|
puts "SKIP unreferenced shipping zone #{zone.id} (#{zone.name}) — no delivery method uses it"
|
|
209
219
|
end
|
|
210
220
|
|
|
221
|
+
zoneless = Spree::DeliveryMethod.unscoped.where(delivery_zone_id: nil).pluck(:id, :name)
|
|
222
|
+
if zoneless.any?
|
|
223
|
+
puts "WARNING: #{zoneless.size} delivery methods have no delivery zone and return no rates at checkout. Assign a zone to each in the admin:"
|
|
224
|
+
zoneless.each { |id, name| puts " - ##{id} #{name}" }
|
|
225
|
+
end
|
|
226
|
+
|
|
211
227
|
puts 'migrate_zones_to_delivery_zones done. Tax-scoped zones stay on Spree::Zone for the tax provider migration.'
|
|
212
228
|
end
|
|
213
229
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Deleting a promotion used to leave OrderPromotion rows on open carts pointing
|
|
4
|
+
# at a promotion that no longer exists, which broke loading those carts.
|
|
5
|
+
namespace :spree do
|
|
6
|
+
desc 'Remove order promotions whose promotion no longer exists and recalculate the affected carts'
|
|
7
|
+
task remove_orphan_order_promotions: :environment do
|
|
8
|
+
orphans = Spree::OrderPromotion.where.not(promotion_id: Spree::Promotion.unscoped.select(:id))
|
|
9
|
+
removed = 0
|
|
10
|
+
|
|
11
|
+
orphans.find_each do |order_promotion|
|
|
12
|
+
owner = order_promotion.owner
|
|
13
|
+
order_promotion.destroy
|
|
14
|
+
owner&.recalculate_totals! if owner.is_a?(Spree::Cart)
|
|
15
|
+
removed += 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
puts "remove_orphan_order_promotions done. removed: #{removed}"
|
|
19
|
+
end
|
|
20
|
+
end
|