spree_core 6.0.0.beta1 → 6.0.0.beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/jobs/spree/payments/handle_webhook_job.rb +1 -0
- data/app/jobs/spree/sample_data/load_job.rb +18 -0
- data/app/jobs/spree/seller_transfers/execute_pending_job.rb +1 -0
- data/app/mailers/spree/admin_user_mailer.rb +2 -2
- data/app/models/concerns/spree/acted_by.rb +210 -0
- data/app/models/concerns/spree/actor.rb +51 -0
- data/app/models/concerns/spree/admin_user_methods.rb +48 -17
- data/app/models/concerns/spree/customer_methods.rb +17 -1
- data/app/models/concerns/spree/has_number.rb +3 -2
- data/app/models/concerns/spree/prefixed_id.rb +4 -1
- data/app/models/concerns/spree/purchase/payment_processing.rb +1 -1
- data/app/models/concerns/spree/purchase/totals.rb +3 -1
- data/app/models/concerns/spree/ransackable_attributes.rb +38 -6
- data/app/models/concerns/spree/translatable_resource.rb +6 -7
- data/app/models/concerns/spree/webhook_payload_redaction.rb +132 -21
- data/app/models/spree/adjusters/promotion.rb +1 -1
- data/app/models/spree/api_key.rb +5 -3
- data/app/models/spree/authentication/lockout.rb +34 -0
- data/app/models/spree/authentication/strategies/email_password_strategy.rb +4 -12
- data/app/models/spree/authentication/strategies/oidc_strategy.rb +1 -1
- data/app/models/spree/catalog.rb +50 -13
- data/app/models/spree/claim.rb +3 -1
- data/app/models/spree/commission_line.rb +4 -2
- data/app/models/spree/commission_rule.rb +1 -1
- data/app/models/spree/coupon_code.rb +14 -2
- data/app/models/spree/current.rb +56 -13
- data/app/models/spree/exchange.rb +3 -1
- data/app/models/spree/export.rb +8 -5
- data/app/models/spree/exports/customers.rb +2 -2
- data/app/models/spree/fulfillment_group.rb +127 -0
- data/app/models/spree/gift_card.rb +9 -0
- data/app/models/spree/invitation.rb +18 -5
- data/app/models/spree/line_item.rb +1 -1
- data/app/models/spree/number_generators/sequential.rb +2 -2
- data/app/models/spree/option_value.rb +7 -0
- data/app/models/spree/order.rb +89 -18
- data/app/models/spree/order_group.rb +78 -6
- data/app/models/spree/payment_source.rb +1 -1
- data/app/models/spree/payment_split.rb +24 -0
- data/app/models/spree/price.rb +74 -0
- data/app/models/spree/pricing_provider/internal.rb +37 -15
- data/app/models/spree/product.rb +4 -0
- data/app/models/spree/product_option_type.rb +11 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +92 -30
- data/app/models/spree/promotion/rules/category.rb +14 -9
- data/app/models/spree/promotion/rules/product.rb +17 -1
- data/app/models/spree/promotion.rb +1 -1
- data/app/models/spree/promotion_action.rb +12 -1
- data/app/models/spree/promotion_handler/coupon.rb +50 -5
- data/app/models/spree/promotion_rule.rb +14 -0
- data/app/models/spree/refund.rb +2 -1
- data/app/models/spree/return.rb +18 -7
- data/app/models/spree/return_line_item.rb +19 -0
- data/app/models/spree/search_provider/database.rb +10 -2
- data/app/models/spree/seller_payout.rb +3 -1
- data/app/models/spree/seller_transfer.rb +3 -1
- data/app/models/spree/stock_receipt.rb +7 -2
- data/app/models/spree/store.rb +4 -0
- data/app/models/spree/store_credit.rb +4 -0
- data/app/models/spree/translations/batch.rb +4 -2
- data/app/models/spree/user_identity.rb +10 -6
- data/app/models/spree/variant.rb +8 -3
- data/app/models/spree/webhook_delivery.rb +28 -0
- data/app/models/spree/webhook_endpoint.rb +14 -0
- data/app/presenters/spree/csv/order_line_item_presenter.rb +2 -2
- data/app/services/spree/carts/split_by_seller.rb +22 -12
- data/app/services/spree/companies/add_member.rb +8 -2
- data/app/services/spree/imports/row_processors/price_list_price.rb +6 -0
- data/app/services/spree/imports/row_processors/product_translation.rb +1 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +17 -11
- data/app/services/spree/order_groups/allocate_payments.rb +3 -0
- data/app/services/spree/orders/approve.rb +3 -2
- data/app/services/spree/orders/build_fulfillments.rb +6 -0
- data/app/services/spree/orders/update_statuses.rb +8 -2
- data/app/services/spree/prices/bulk_upsert.rb +186 -47
- data/app/services/spree/sample_data/import_builder.rb +1 -1
- data/app/services/spree/sample_data/loader.rb +54 -40
- data/app/services/spree/seeds/all.rb +1 -26
- data/app/services/spree/seeds/allowed_origins.rb +5 -4
- data/app/services/spree/seeds/api_keys.rb +10 -9
- data/app/services/spree/seeds/channels.rb +9 -8
- data/app/services/spree/seeds/commission_rates.rb +21 -20
- data/app/services/spree/seeds/customer_groups.rb +5 -4
- data/app/services/spree/seeds/digital_delivery.rb +7 -8
- data/app/services/spree/seeds/payment_methods.rb +12 -11
- data/app/services/spree/seeds/product_types.rb +7 -8
- data/app/services/spree/seeds/returns_environment.rb +12 -11
- data/app/services/spree/seeds/roles.rb +8 -7
- data/app/services/spree/seeds/saved_reports.rb +19 -20
- data/app/services/spree/seeds/seller_requirements.rb +5 -4
- data/app/services/spree/seeds/store_resources.rb +41 -0
- data/app/services/spree/seeds/store_scoped.rb +15 -0
- data/app/services/spree/seeds/tax_categories.rb +10 -9
- data/app/services/spree/stock_locations/stock_items/create.rb +1 -1
- data/app/services/spree/stock_locations/stock_levels/create.rb +19 -3
- data/app/subscribers/spree/order_status_subscriber.rb +15 -7
- data/app/workflows/spree/carts/add_item.rb +5 -7
- data/app/workflows/spree/carts/complete.rb +20 -64
- data/app/workflows/spree/carts/recalculate_totals.rb +3 -1
- data/app/workflows/spree/claims/create.rb +31 -1
- data/app/workflows/spree/claims/resolve.rb +7 -11
- data/app/workflows/spree/exchanges/create.rb +11 -0
- data/app/workflows/spree/exchanges/fulfill.rb +21 -13
- data/app/workflows/spree/orders/cancel.rb +3 -2
- data/app/workflows/spree/orders/complete.rb +99 -2
- data/app/workflows/spree/price_lists/update.rb +17 -0
- data/app/workflows/spree/refunds/create.rb +1 -1
- data/app/workflows/spree/refunds/order_payments.rb +55 -1
- data/app/workflows/spree/returns/approve.rb +1 -1
- data/app/workflows/spree/returns/create.rb +11 -17
- data/app/workflows/spree/returns/refund.rb +16 -25
- data/app/workflows/spree/returns/returnable_quantity.rb +53 -0
- data/app/workflows/spree/sellers/invite.rb +1 -1
- data/config/locales/en.yml +8 -119
- data/db/migrate/20260916000001_add_email_delivery_flags_to_spree_order_groups.rb +14 -0
- data/db/migrate/20260916000001_order_operation_actors_polymorphic.rb +30 -0
- data/db/migrate/20260922000001_add_refunded_order_to_spree_store_credits.rb +5 -0
- data/db/migrate/20260923000001_change_spree_user_identities_tokens_to_text.rb +10 -0
- data/db/sample_data/channels.rb +1 -1
- data/db/sample_data/collections.rb +1 -1
- data/db/sample_data/custom_field_definitions.rb +1 -1
- data/db/sample_data/digital_products.rb +1 -1
- data/db/sample_data/fulfillment.rb +1 -1
- data/db/sample_data/options.rb +4 -2
- data/db/sample_data/orders.rb +9 -10
- data/db/sample_data/payment_methods.rb +18 -18
- data/db/sample_data/product_type_categories.rb +1 -1
- data/db/sample_data/product_types.rb +2 -2
- data/db/sample_data/promotions.rb +2 -4
- data/db/sample_data/wholesale.rb +1 -1
- data/lib/generators/spree/api_resource/api_resource_generator.rb +121 -6
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +8 -2
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +3 -0
- data/lib/generators/spree/api_resource/templates/permissions.en.yml.tt +6 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +2 -2
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +4 -1
- data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
- data/lib/generators/spree/model/model_generator.rb +40 -1
- data/lib/generators/spree/model/templates/create_table_migration.rb.tt +9 -0
- data/lib/generators/spree/model/templates/model.rb.tt +10 -4
- data/lib/generators/spree/subscriber/subscriber_generator.rb +3 -3
- data/lib/spree/core/configuration.rb +25 -25
- data/lib/spree/core/engine.rb +35 -8
- data/lib/spree/core/preferences/runtime_configuration.rb +76 -2
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +27 -0
- data/lib/spree/iso_data.rb +23 -4
- data/lib/spree/store_scope_guard.rb +16 -1
- data/lib/spree/testing_support/factories/invitation_factory.rb +1 -0
- data/lib/spree/testing_support/factories/order_group_factory.rb +49 -0
- data/lib/spree/testing_support/factories/payment_factory.rb +7 -2
- data/lib/spree/testing_support/factories/user_factory.rb +0 -1
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
- data/lib/tasks/backfill_actor_types.rake +35 -0
- data/lib/tasks/sample_data.rake +8 -2
- data/lib/tasks/store_settings.rake +1 -0
- data/lib/tasks/upgrade.rake +6 -21
- metadata +23 -9
- /data/{app/models → lib}/spree/checkout/default_requirements.rb +0 -0
- /data/{app/models → lib}/spree/checkout/registry.rb +0 -0
- /data/{app/models → lib}/spree/checkout/requirement.rb +0 -0
- /data/{app/models → lib}/spree/checkout/requirements.rb +0 -0
- /data/{app/models → lib}/spree/checkout/step.rb +0 -0
|
@@ -35,11 +35,13 @@ module Spree
|
|
|
35
35
|
# @return [Spree::ServiceModule::Result] success carries
|
|
36
36
|
# `{ price_count: N }` — the number of rows passed to `upsert_all`.
|
|
37
37
|
# Fails with the offending rows when a batch carries an unusable
|
|
38
|
-
# `min_quantity`,
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
38
|
+
# `min_quantity`, puts a quantity on a base price, names one rung twice,
|
|
39
|
+
# would take a variant past
|
|
40
|
+
# {Spree::Price::MAXIMUM_BREAKS_PER_VARIANT} breaks on one list, or
|
|
41
|
+
# would leave a ladder charging more for a bigger order. This path
|
|
42
|
+
# writes in SQL and runs no model validations, so the checks live here
|
|
43
|
+
# rather than in each of the three writers that reach it
|
|
44
|
+
# (docs/plans/6.0-volume-pricing.md).
|
|
43
45
|
def call(rows:)
|
|
44
46
|
rows = Array(rows).map { |r| r.with_indifferent_access }
|
|
45
47
|
# Amounts are parsed once, here; everything below reads numbers.
|
|
@@ -54,20 +56,35 @@ module Spree
|
|
|
54
56
|
negative = rows_with_negative_amount(keyed)
|
|
55
57
|
return failure(nil, invalid_amounts: negative) if negative.any?
|
|
56
58
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
# A base price is one row per (variant, currency) whatever quantity it
|
|
60
|
+
# names, so a quantity here would upsert onto a key it does not
|
|
61
|
+
# describe. The model refuses it in `break_requires_price_list`, which
|
|
62
|
+
# this SQL path never runs.
|
|
63
|
+
misplaced = base_rows_with_quantity(keyed)
|
|
64
|
+
return failure(nil, quantities_on_base_prices: misplaced) if misplaced.any?
|
|
65
|
+
|
|
66
|
+
duplicates = rows_with_duplicate_quantity(keyed)
|
|
67
|
+
return failure(nil, duplicate_quantities: duplicates) if duplicates.any?
|
|
68
|
+
|
|
69
|
+
upsert_rows, clear_rows = keyed.partition { |r| r[:amount].present? }
|
|
70
|
+
# A clear at a quantity the batch also prices is a rung moving onto
|
|
71
|
+
# it, so the amount wins.
|
|
72
|
+
claimed = upsert_rows.map { |r| row_key(r) }.to_set
|
|
73
|
+
clear_rows = clear_rows.reject { |r| claimed.include?(row_key(r)) }
|
|
62
74
|
|
|
63
75
|
payload = build_payload(upsert_rows)
|
|
64
|
-
affected_keys =
|
|
76
|
+
affected_keys = (upsert_rows + clear_rows).map { |r| row_key(r) }
|
|
65
77
|
|
|
66
78
|
return success(price_count: 0) if affected_keys.empty?
|
|
67
79
|
|
|
68
|
-
|
|
80
|
+
stored = stored_rungs(upsert_rows + clear_rows)
|
|
81
|
+
|
|
82
|
+
over_cap = ladders_over_cap(upsert_rows, clear_rows, stored)
|
|
69
83
|
return failure(nil, over_cap: over_cap) if over_cap.any?
|
|
70
84
|
|
|
85
|
+
rising = rising_ladders(upsert_rows, clear_rows, stored)
|
|
86
|
+
return failure(nil, rising_ladders: rising) if rising.any?
|
|
87
|
+
|
|
71
88
|
base_rows, override_rows = payload.partition { |r| r[:price_list_id].nil? }
|
|
72
89
|
|
|
73
90
|
Spree::Price.transaction do
|
|
@@ -129,7 +146,7 @@ module Spree
|
|
|
129
146
|
# quantity address the bottom rung, which is every row written before
|
|
130
147
|
# breaks existed (docs/plans/6.0-volume-pricing.md).
|
|
131
148
|
def row_key(row)
|
|
132
|
-
[row[:variant_id], row[:currency], row[:price_list_id], quantity_of(row)]
|
|
149
|
+
[row[:variant_id].to_s, row[:currency], row[:price_list_id].presence&.to_s, quantity_of(row)]
|
|
133
150
|
end
|
|
134
151
|
|
|
135
152
|
# Absent means the ladder's bottom rung. Anything present must be a
|
|
@@ -182,55 +199,177 @@ module Spree
|
|
|
182
199
|
# constant is named: breaks *above* the bottom rung, so a variant priced
|
|
183
200
|
# at one figure plus ten breaks is exactly at the limit.
|
|
184
201
|
#
|
|
185
|
-
# Only ladders the batch actually adds a break to are checked, so an
|
|
186
|
-
# ordinary spreadsheet save — one quantity-1 row per variant — costs no
|
|
187
|
-
# extra queries at all.
|
|
188
|
-
#
|
|
189
202
|
# @param rows [Array<Hash>] the rows carrying an amount
|
|
190
203
|
# @param cleared_rows [Array<Hash>] the rows whose blank amount removes a rung
|
|
204
|
+
# @param stored [Hash] `{ ladder_key => { quantity => amount } }` as stored
|
|
191
205
|
# @return [Array<Hash>] `[{ variant_id:, currency:, price_list_id: }, ...]`
|
|
192
|
-
def ladders_over_cap(rows, cleared_rows
|
|
193
|
-
touched = rows
|
|
194
|
-
reject { |row| row[:price_list_id].blank? || quantity_of(row) == 1 }.
|
|
195
|
-
group_by { |row| [row[:variant_id].to_s, row[:currency], row[:price_list_id].to_s] }.
|
|
196
|
-
transform_values { |group| group.map { |row| quantity_of(row) }.to_set }
|
|
206
|
+
def ladders_over_cap(rows, cleared_rows, stored)
|
|
207
|
+
touched = breaks_by_ladder(rows)
|
|
197
208
|
return [] if touched.empty?
|
|
198
209
|
|
|
199
|
-
cleared = cleared_rows
|
|
200
|
-
reject { |row| row[:price_list_id].blank? || quantity_of(row) == 1 }.
|
|
201
|
-
group_by { |row| [row[:variant_id].to_s, row[:currency], row[:price_list_id].to_s] }.
|
|
202
|
-
transform_values { |group| group.map { |row| quantity_of(row) }.to_set }
|
|
203
|
-
|
|
204
|
-
# One query for every ladder in the batch rather than one each: an
|
|
205
|
-
# import writing a ladder across two hundred variants would otherwise
|
|
206
|
-
# issue two hundred round trips inside the request. Cross-pairs the
|
|
207
|
-
# `IN` clauses pull in are dropped by the lookup below, the same way
|
|
208
|
-
# #sweep handles them.
|
|
209
|
-
# Placeholder rows charge nothing, so they do not fill a ladder — the
|
|
210
|
-
# model's own guard counts the same way, and a cap two writers
|
|
211
|
-
# disagree about is one that refuses what it just allowed.
|
|
212
|
-
stored = Spree::Price.
|
|
213
|
-
where(variant_id: touched.keys.map { |key| key[0] },
|
|
214
|
-
currency: touched.keys.map { |key| key[1] },
|
|
215
|
-
price_list_id: touched.keys.map { |key| key[2] }).
|
|
216
|
-
breaks.
|
|
217
|
-
where.not(amount: nil).
|
|
218
|
-
pluck(:variant_id, :currency, :price_list_id, :min_quantity).
|
|
219
|
-
group_by { |variant_id, currency, price_list_id, _| [variant_id.to_s, currency, price_list_id.to_s] }
|
|
210
|
+
cleared = breaks_by_ladder(cleared_rows)
|
|
220
211
|
|
|
221
212
|
touched.filter_map do |key, incoming|
|
|
222
213
|
# Stored rungs this batch does not name are the ones that survive it;
|
|
223
214
|
# counting the rest as well would refuse a merchant who deleted two
|
|
224
215
|
# rungs and added two others, whose ladder ends the size it began.
|
|
225
|
-
|
|
226
|
-
# what is left after this batch is (survivors + incoming).
|
|
227
|
-
survivors = stored.fetch(key, []).map(&:last).to_set - cleared.fetch(key, Set.new)
|
|
216
|
+
survivors = stored.fetch(key, {}).keys.reject { |quantity| quantity == 1 }.to_set - cleared.fetch(key, Set.new)
|
|
228
217
|
next if (survivors | incoming).size <= Spree::Price::MAXIMUM_BREAKS_PER_VARIANT
|
|
229
218
|
|
|
230
219
|
{ variant_id: key[0], currency: key[1], price_list_id: key[2] }
|
|
231
220
|
end
|
|
232
221
|
end
|
|
233
222
|
|
|
223
|
+
# The break quantities each ladder in these rows carries.
|
|
224
|
+
def breaks_by_ladder(rows)
|
|
225
|
+
rows.
|
|
226
|
+
reject { |row| row[:price_list_id].blank? || quantity_of(row) == 1 }.
|
|
227
|
+
group_by { |row| ladder_key(row) }.
|
|
228
|
+
transform_values { |group| group.map { |row| quantity_of(row) }.to_set }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Base-price rows naming a quantity. A base price is one row per
|
|
232
|
+
# (variant, currency), so the quantity describes nothing and the row
|
|
233
|
+
# would upsert onto a key it does not match.
|
|
234
|
+
#
|
|
235
|
+
# @return [Array<Hash>] `[{ index: }, ...]`
|
|
236
|
+
def base_rows_with_quantity(rows)
|
|
237
|
+
rows.each_with_index.filter_map do |row, index|
|
|
238
|
+
{ index: index } if row[:price_list_id].blank? && quantity_of(row) != 1
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Rows repeating a rung the batch has already priced. Only amounts
|
|
243
|
+
# collide: a clear at the same key is a rung moving onto it, which #call
|
|
244
|
+
# resolves in favour of the amount.
|
|
245
|
+
#
|
|
246
|
+
# @param rows [Array<Hash>]
|
|
247
|
+
# @return [Array<Hash>] `[{ index:, min_quantity: }, ...]`
|
|
248
|
+
def rows_with_duplicate_quantity(rows)
|
|
249
|
+
seen = Set.new
|
|
250
|
+
|
|
251
|
+
rows.each_with_index.filter_map do |row, index|
|
|
252
|
+
next if row[:amount].blank?
|
|
253
|
+
|
|
254
|
+
{ index: index, min_quantity: quantity_of(row) } unless seen.add?(row_key(row))
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Ladders this batch would leave charging more for a bigger order. A rung
|
|
259
|
+
# must not cost more than the rung beneath it, or than the base price for
|
|
260
|
+
# a ladder starting above one unit; a bottom rung above base is a dearer
|
|
261
|
+
# agreement, which is allowed. Judged on the state the batch leaves, not
|
|
262
|
+
# on what it changed: no write path can produce a rising ladder, so one
|
|
263
|
+
# found rising came from outside them and must be repaired rather than
|
|
264
|
+
# built on. Base is the floor even where another list would undercut it
|
|
265
|
+
# below the threshold — the write path cannot know which list wins for
|
|
266
|
+
# which buyer (docs/plans/6.0-volume-pricing.md).
|
|
267
|
+
#
|
|
268
|
+
# @param rows [Array<Hash>] the rows carrying an amount
|
|
269
|
+
# @param cleared_rows [Array<Hash>] the rows whose blank amount removes a rung
|
|
270
|
+
# @param stored [Hash] `{ ladder_key => { quantity => amount } }` as stored
|
|
271
|
+
# @return [Array<Hash>] the offending rung and the price it must not exceed
|
|
272
|
+
def rising_ladders(rows, cleared_rows, stored)
|
|
273
|
+
keys = ladder_keys_to_check(rows + cleared_rows, stored)
|
|
274
|
+
return [] if keys.empty?
|
|
275
|
+
|
|
276
|
+
incoming = rungs_by_ladder(rows)
|
|
277
|
+
cleared = breaks_and_bottom_by_ladder(cleared_rows)
|
|
278
|
+
floors = base_floors(rows, cleared_rows, keys)
|
|
279
|
+
|
|
280
|
+
keys.filter_map do |key|
|
|
281
|
+
dropped = cleared.fetch(key, Set.new)
|
|
282
|
+
now = stored.fetch(key, {}).reject { |quantity, _| dropped.include?(quantity) }.
|
|
283
|
+
merge(incoming.fetch(key, {}))
|
|
284
|
+
|
|
285
|
+
breach = Spree::Price.rising_rung(now.sort_by(&:first), floors[key.first(2)])
|
|
286
|
+
next if breach.nil?
|
|
287
|
+
|
|
288
|
+
{ variant_id: key[0], currency: key[1], price_list_id: key[2],
|
|
289
|
+
min_quantity: breach[0], amount: breach[1], floor: breach[2] }
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# The ladders whose shape or floor this batch could change: those it
|
|
294
|
+
# writes rows on, plus every stored ladder of a variant whose base price
|
|
295
|
+
# it moves — lowering the shop price under a break raises the bill at the
|
|
296
|
+
# threshold just as surely as mispricing the break.
|
|
297
|
+
def ladder_keys_to_check(rows, stored)
|
|
298
|
+
own = rows.reject { |row| row[:price_list_id].blank? }.map { |row| ladder_key(row) }
|
|
299
|
+
rebased = rows.select { |row| row[:price_list_id].blank? }.
|
|
300
|
+
map { |row| [row[:variant_id].to_s, row[:currency]] }.to_set
|
|
301
|
+
return own.uniq if rebased.empty?
|
|
302
|
+
|
|
303
|
+
(own + stored.keys.select { |key| rebased.include?(key.first(2)) }).uniq
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# `{ ladder_key => { quantity => amount } }` for these rows.
|
|
307
|
+
def rungs_by_ladder(rows)
|
|
308
|
+
rows.
|
|
309
|
+
reject { |row| row[:price_list_id].blank? }.
|
|
310
|
+
group_by { |row| ladder_key(row) }.
|
|
311
|
+
transform_values { |group| group.to_h { |row| [quantity_of(row), row[:amount]] } }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Every quantity these rows remove, bottom rung included — unlike the cap
|
|
315
|
+
# check, which only counts breaks.
|
|
316
|
+
def breaks_and_bottom_by_ladder(rows)
|
|
317
|
+
rows.
|
|
318
|
+
reject { |row| row[:price_list_id].blank? }.
|
|
319
|
+
group_by { |row| ladder_key(row) }.
|
|
320
|
+
transform_values { |group| group.map { |row| quantity_of(row) }.to_set }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# The stored rungs of every ladder this batch touches, and of every
|
|
324
|
+
# ladder whose base price it moves. Placeholder rows charge nothing, so a
|
|
325
|
+
# ladder whose quantity-1 row is a placeholder starts at its first real
|
|
326
|
+
# break. Cross-pairs the `IN` clauses pull in are dropped by the lookup,
|
|
327
|
+
# as in #sweep.
|
|
328
|
+
#
|
|
329
|
+
# @param rows [Array<Hash>]
|
|
330
|
+
# @return [Hash] `{ ladder_key => { quantity => amount } }`
|
|
331
|
+
def stored_rungs(rows)
|
|
332
|
+
variant_ids = rows.map { |row| row[:variant_id] }.uniq
|
|
333
|
+
currencies = rows.map { |row| row[:currency] }.uniq
|
|
334
|
+
return {} if variant_ids.empty?
|
|
335
|
+
|
|
336
|
+
Spree::Price.
|
|
337
|
+
where(variant_id: variant_ids, currency: currencies).
|
|
338
|
+
where.not(price_list_id: nil).
|
|
339
|
+
where.not(amount: nil).
|
|
340
|
+
pluck(:variant_id, :currency, :price_list_id, :min_quantity, :amount).
|
|
341
|
+
group_by { |variant_id, currency, price_list_id, _, _| [variant_id.to_s, currency, price_list_id.to_s] }.
|
|
342
|
+
transform_values { |rungs| rungs.to_h { |rung| [rung[3], rung[4]] } }
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# The base price each ladder falls through to, as this batch would leave
|
|
346
|
+
# it, so a save that moves both the shop price and a break is judged on
|
|
347
|
+
# the pair it sent.
|
|
348
|
+
#
|
|
349
|
+
# @return [Hash] `{ [variant_id, currency] => amount }`
|
|
350
|
+
def base_floors(rows, cleared_rows, keys)
|
|
351
|
+
pairs = keys.map { |key| key.first(2) }.uniq
|
|
352
|
+
return {} if pairs.empty?
|
|
353
|
+
|
|
354
|
+
stored = Spree::Price.
|
|
355
|
+
where(variant_id: pairs.map(&:first), currency: pairs.map(&:last), price_list_id: nil).
|
|
356
|
+
where.not(amount: nil).
|
|
357
|
+
pluck(:variant_id, :currency, :amount).
|
|
358
|
+
to_h { |variant_id, currency, amount| [[variant_id.to_s, currency], amount] }
|
|
359
|
+
|
|
360
|
+
written = rows.select { |row| row[:price_list_id].blank? }.
|
|
361
|
+
to_h { |row| [[row[:variant_id].to_s, row[:currency]], row[:amount]] }
|
|
362
|
+
removed = cleared_rows.select { |row| row[:price_list_id].blank? }.
|
|
363
|
+
map { |row| [row[:variant_id].to_s, row[:currency]] }
|
|
364
|
+
|
|
365
|
+
stored.merge(written).except(*removed)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# A ladder is one variant's rungs, in one currency, on one list.
|
|
369
|
+
def ladder_key(row)
|
|
370
|
+
[row[:variant_id].to_s, row[:currency], row[:price_list_id].to_s]
|
|
371
|
+
end
|
|
372
|
+
|
|
234
373
|
# Parses locale-aware decimal input ("1.234,56" in DE, "1,234.56"
|
|
235
374
|
# in en-US). Numeric values pass through; blank values become nil.
|
|
236
375
|
def parse_amount(value)
|
|
@@ -254,7 +393,7 @@ module Spree
|
|
|
254
393
|
.pluck(:id, :variant_id, :currency, :price_list_id, :min_quantity, :amount)
|
|
255
394
|
|
|
256
395
|
doomed_ids = candidates.filter_map do |id, variant_id, currency, price_list_id, min_quantity, amount|
|
|
257
|
-
key = [variant_id, currency, price_list_id, min_quantity]
|
|
396
|
+
key = [variant_id.to_s, currency, price_list_id&.to_s, min_quantity]
|
|
258
397
|
next unless affected_set.include?(key)
|
|
259
398
|
next id if amount.nil?
|
|
260
399
|
next id if cleared_keys.include?(key)
|
|
@@ -7,7 +7,7 @@ module Spree
|
|
|
7
7
|
# runs — a price-list import's `preferred_price_list_id`, say
|
|
8
8
|
# @return [Spree::Import] persisted, unprocessed
|
|
9
9
|
def self.call(csv_path:, import_class:, store: nil, user: nil, inline: false, skip_events: false, attributes: {})
|
|
10
|
-
store ||= Spree::
|
|
10
|
+
store ||= Spree::Current.store
|
|
11
11
|
# An admin of the target store, not `admin_user_class.first` — in a
|
|
12
12
|
# multi-store or multi-tenant app that global first can belong to a
|
|
13
13
|
# different store entirely.
|
|
@@ -4,65 +4,79 @@ module Spree
|
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
5
|
include Spree::SampleData::Helper
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Loads the demo data into one store. The sample files and the imports
|
|
8
|
+
# they run read `Spree::Current.store`, so it is set for the whole run
|
|
9
|
+
# and restored afterwards.
|
|
10
|
+
#
|
|
11
|
+
# @param store [Spree::Store, nil] the store to load into; defaults to
|
|
12
|
+
# the default store, resolved after the seeds so a bare install works
|
|
13
|
+
# @return [Spree::ServiceModule::Result]
|
|
14
|
+
def call(store: nil)
|
|
8
15
|
Spree::Events.disable do
|
|
9
16
|
without_geocoding do
|
|
10
17
|
ActiveRecord::Base.no_touching do
|
|
11
18
|
puts 'Running seeds first...' unless seeds_loaded?
|
|
12
19
|
ensure_seeds_loaded
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
@store = store || Spree::Store.default
|
|
22
|
+
Spree::Current.set(store: @store) { load_sample_data }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
load_ruby_file('channels')
|
|
28
|
+
private
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
load_ruby_file('custom_field_definitions')
|
|
30
|
+
attr_reader :store
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
def load_sample_data
|
|
33
|
+
puts 'Loading sample configuration data...'
|
|
34
|
+
load_configuration_data
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
puts 'Loading sample channels...'
|
|
37
|
+
load_ruby_file('channels')
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
puts 'Loading sample custom_field definitions...'
|
|
40
|
+
load_ruby_file('custom_field_definitions')
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
puts 'Loading sample options...'
|
|
43
|
+
load_ruby_file('options')
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
puts 'Loading sample product types...'
|
|
46
|
+
load_ruby_file('product_types')
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
puts 'Loading sample products...'
|
|
49
|
+
load_products
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
puts 'Attaching the sample digital product file...'
|
|
52
|
+
load_ruby_file('digital_products')
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
puts 'Publishing sample products on every channel...'
|
|
55
|
+
publish_sample_products(store)
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
puts 'Loading sample product translations...'
|
|
58
|
+
load_product_translations
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
puts 'Linking product types to categories...'
|
|
61
|
+
load_ruby_file('product_type_categories')
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
puts 'Loading sample collections...'
|
|
64
|
+
load_ruby_file('collections')
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
puts 'Loading sample customers...'
|
|
67
|
+
load_customers
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
69
|
+
puts 'Loading wholesale demo data...'
|
|
70
|
+
load_ruby_file('wholesale')
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
puts 'Loading sample orders...'
|
|
73
|
+
load_ruby_file('orders')
|
|
74
|
+
|
|
75
|
+
puts 'Loading sample posts...'
|
|
76
|
+
load_ruby_file('posts')
|
|
77
|
+
|
|
78
|
+
puts 'Sample data loaded successfully!'
|
|
79
|
+
end
|
|
66
80
|
|
|
67
81
|
def load_configuration_data
|
|
68
82
|
load_ruby_file('fulfillment')
|
|
@@ -72,19 +86,19 @@ module Spree
|
|
|
72
86
|
|
|
73
87
|
def load_products
|
|
74
88
|
csv_path = sample_data_path.join('products.csv')
|
|
75
|
-
Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Products, inline: true)
|
|
89
|
+
Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Products, store: store, inline: true)
|
|
76
90
|
end
|
|
77
91
|
|
|
78
92
|
def load_product_translations
|
|
79
93
|
csv_path = sample_data_path.join('product_translations.csv')
|
|
80
94
|
return unless csv_path.exist?
|
|
81
95
|
|
|
82
|
-
Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::ProductTranslations, inline: true)
|
|
96
|
+
Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::ProductTranslations, store: store, inline: true)
|
|
83
97
|
end
|
|
84
98
|
|
|
85
99
|
def load_customers
|
|
86
100
|
csv_path = sample_data_path.join('customers.csv')
|
|
87
|
-
Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Customers, inline: true)
|
|
101
|
+
Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Customers, store: store, inline: true)
|
|
88
102
|
end
|
|
89
103
|
end
|
|
90
104
|
end
|
|
@@ -10,39 +10,14 @@ module Spree
|
|
|
10
10
|
# countries gem, and zones are migration-only, so none of them
|
|
11
11
|
# are seeded.
|
|
12
12
|
|
|
13
|
-
# additional data
|
|
14
|
-
TaxCategories.call
|
|
15
|
-
|
|
16
|
-
# store & channels
|
|
17
13
|
Stores.call
|
|
18
|
-
|
|
19
|
-
# Roles are store-owned, so they can only be seeded once a store
|
|
20
|
-
# exists — every store gets its own immutable admin role.
|
|
21
|
-
Roles.call
|
|
22
|
-
# Store-scoped, so it can only run once a store exists.
|
|
23
|
-
DigitalDelivery.call
|
|
14
|
+
Spree::Store.find_each { |store| StoreResources.call(store: store) }
|
|
24
15
|
# The warehouse, delivery zones and pickup are not seeded: their
|
|
25
16
|
# shape depends on which country the shop sells from, and nobody
|
|
26
17
|
# has answered that yet. Spree::Stores::ProvisionDefaults builds
|
|
27
18
|
# them from the merchant's answer — at first-run setup, or here
|
|
28
19
|
# when ADMIN_EMAIL/ADMIN_PASSWORD name an install that skips it.
|
|
29
20
|
AdminUser.call
|
|
30
|
-
|
|
31
|
-
# add store resources
|
|
32
|
-
PaymentMethods.call
|
|
33
|
-
TaxCategories.call
|
|
34
|
-
ProductTypes.call
|
|
35
|
-
CustomerGroups.call
|
|
36
|
-
ReturnsEnvironment.call
|
|
37
|
-
# The marketplace's catch-all rate, at the bottom of the list so
|
|
38
|
-
# narrower rates added later resolve ahead of it.
|
|
39
|
-
CommissionRates.call
|
|
40
|
-
# What a new seller has to do before this marketplace will let
|
|
41
|
-
# them trade. Runs after stores for the same reason roles do.
|
|
42
|
-
SellerRequirements.call
|
|
43
|
-
ApiKeys.call
|
|
44
|
-
SavedReports.call
|
|
45
|
-
AllowedOrigins.call
|
|
46
21
|
end
|
|
47
22
|
end
|
|
48
23
|
end
|
|
@@ -2,11 +2,12 @@ module Spree
|
|
|
2
2
|
module Seeds
|
|
3
3
|
class AllowedOrigins
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
|
+
include StoreScoped
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def seed(store)
|
|
10
|
+
store.allowed_origins.find_or_create_by!(origin: 'http://localhost')
|
|
10
11
|
end
|
|
11
12
|
end
|
|
12
13
|
end
|
|
@@ -2,17 +2,18 @@ module Spree
|
|
|
2
2
|
module Seeds
|
|
3
3
|
class ApiKeys
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
|
+
include StoreScoped
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
Spree::Store.find_each do |store|
|
|
8
|
-
unless store.api_keys.active.publishable.where(channel_id: nil).exists?
|
|
9
|
-
store.api_keys.create!(name: 'Default', key_type: 'publishable')
|
|
10
|
-
end
|
|
7
|
+
private
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
def seed(store)
|
|
10
|
+
unless store.api_keys.active.publishable.where(channel_id: nil).exists?
|
|
11
|
+
store.api_keys.create!(name: 'Default', key_type: 'publishable')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
wholesale = store.channels.find_by(code: Channels::WHOLESALE_CODE)
|
|
15
|
+
if wholesale && !store.api_keys.active.publishable.where(channel: wholesale).exists?
|
|
16
|
+
store.api_keys.create!(name: 'Storefront (Wholesale)', key_type: 'publishable', channel: wholesale)
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -6,18 +6,19 @@ module Spree
|
|
|
6
6
|
# See docs/plans/5.6-store-channel-context-and-key-binding.md.
|
|
7
7
|
class Channels
|
|
8
8
|
prepend Spree::ServiceModule::Base
|
|
9
|
+
include StoreScoped
|
|
9
10
|
|
|
10
11
|
WHOLESALE_CODE = 'wholesale'.freeze
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
Spree::Store.find_each do |store|
|
|
14
|
-
store.ensure_default_channel
|
|
13
|
+
private
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
def seed(store)
|
|
16
|
+
store.ensure_default_channel
|
|
17
|
+
|
|
18
|
+
store.channels.find_or_create_by!(code: WHOLESALE_CODE) do |channel|
|
|
19
|
+
channel.name = 'Wholesale'
|
|
20
|
+
channel.preferred_storefront_access = 'login_required'
|
|
21
|
+
channel.preferred_guest_checkout = false
|
|
21
22
|
end
|
|
22
23
|
end
|
|
23
24
|
end
|
|
@@ -22,31 +22,32 @@ module Spree
|
|
|
22
22
|
# setting the value is the operator's first commission decision.
|
|
23
23
|
class CommissionRates
|
|
24
24
|
prepend Spree::ServiceModule::Base
|
|
25
|
+
include StoreScoped
|
|
25
26
|
|
|
26
27
|
DEFAULT_CODE = 'marketplace-default'.freeze
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
Spree::Store.find_each do |store|
|
|
30
|
-
# Live rows only, matching the partial unique index: an operator who
|
|
31
|
-
# deleted the seeded fallback gets it back on the next run rather
|
|
32
|
-
# than being left with no bottom-of-list catch-all forever.
|
|
33
|
-
next if store.commission_rates.exists?(code: DEFAULT_CODE)
|
|
29
|
+
private
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
enabled: false
|
|
41
|
-
)
|
|
31
|
+
def seed(store)
|
|
32
|
+
# Live rows only, matching the partial unique index: an operator who
|
|
33
|
+
# deleted the seeded fallback gets it back on the next run rather
|
|
34
|
+
# than being left with no bottom-of-list catch-all forever.
|
|
35
|
+
return if store.commission_rates.exists?(code: DEFAULT_CODE)
|
|
42
36
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
rate = store.commission_rates.create!(
|
|
38
|
+
name: Spree.t('seed.commission_rates.marketplace_default'),
|
|
39
|
+
code: DEFAULT_CODE,
|
|
40
|
+
kind: 'percentage',
|
|
41
|
+
value: 0,
|
|
42
|
+
enabled: false
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# New rates are born at the top, which is right for one an operator
|
|
46
|
+
# adds and wrong for this one: it matches every sale, so anywhere but
|
|
47
|
+
# the bottom leaves the rates below it unreachable. A store seeded
|
|
48
|
+
# after it already has rates would otherwise have them all shadowed
|
|
49
|
+
# the moment this was enabled.
|
|
50
|
+
rate.move_to_bottom
|
|
50
51
|
end
|
|
51
52
|
end
|
|
52
53
|
end
|
|
@@ -5,13 +5,14 @@ module Spree
|
|
|
5
5
|
# wholesale price lists key off.
|
|
6
6
|
class CustomerGroups
|
|
7
7
|
prepend Spree::ServiceModule::Base
|
|
8
|
+
include StoreScoped
|
|
8
9
|
|
|
9
10
|
WHOLESALE_NAME = 'Wholesale'.freeze
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def seed(store)
|
|
15
|
+
store.customer_groups.find_or_create_by!(name: WHOLESALE_NAME)
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|