spree_core 5.4.3 → 5.5.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 +0 -82
- data/app/helpers/spree/currency_helper.rb +0 -12
- data/app/helpers/spree/products_helper.rb +0 -8
- data/app/jobs/spree/base_job.rb +18 -0
- data/app/jobs/spree/events/subscriber_job.rb +2 -1
- data/app/jobs/spree/exports/generate_job.rb +11 -0
- data/app/jobs/spree/images/save_from_url_job.rb +23 -8
- data/app/jobs/spree/imports/assign_tags_job.rb +11 -0
- data/app/jobs/spree/imports/base_job.rb +15 -0
- data/app/jobs/spree/imports/create_categories_job.rb +37 -0
- data/app/jobs/spree/imports/create_rows_job.rb +1 -3
- data/app/jobs/spree/imports/process_group_job.rb +8 -6
- data/app/jobs/spree/imports/process_rows_job.rb +1 -3
- data/app/jobs/spree/media/migrate_product_assets_job.rb +83 -0
- data/app/jobs/spree/products/refresh_metrics_job.rb +15 -4
- data/app/jobs/spree/reports/generate_job.rb +11 -0
- data/app/jobs/spree/search_provider/index_job.rb +5 -1
- data/app/jobs/spree/search_provider/remove_job.rb +4 -0
- data/app/jobs/spree/stock_reservations/expire_job.rb +11 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +34 -1
- data/app/models/concerns/spree/display_on.rb +31 -0
- data/app/models/concerns/spree/metafields.rb +167 -5
- data/app/models/concerns/spree/preference_schema.rb +191 -0
- data/app/models/concerns/spree/prefixed_id.rb +94 -11
- data/app/models/concerns/spree/product_scopes.rb +36 -17
- data/app/models/concerns/spree/ransackable_attributes.rb +5 -1
- data/app/models/concerns/spree/search_indexable.rb +8 -7
- data/app/models/concerns/spree/searchable.rb +11 -2
- data/app/models/concerns/spree/stores/channels.rb +20 -0
- data/app/models/concerns/spree/stores/markets.rb +21 -5
- data/app/models/concerns/spree/typed_associations.rb +120 -0
- data/app/models/concerns/spree/user_methods.rb +71 -12
- data/app/models/spree/ability.rb +4 -117
- data/app/models/spree/api_key.rb +53 -0
- data/app/models/spree/asset.rb +28 -5
- data/app/models/spree/authentication/strategy_registry.rb +72 -0
- data/app/models/spree/base.rb +18 -1
- data/app/models/spree/channel.rb +159 -0
- data/app/models/spree/country.rb +2 -0
- data/app/models/spree/current.rb +5 -1
- data/app/models/spree/custom_field.rb +9 -0
- data/app/models/spree/custom_field_definition.rb +7 -0
- data/app/models/spree/customer_group.rb +8 -2
- data/app/models/spree/export.rb +30 -3
- data/app/models/spree/gateway.rb +25 -0
- data/app/models/spree/gift_card.rb +1 -1
- data/app/models/spree/gift_card_batch.rb +4 -1
- data/app/models/spree/import.rb +5 -0
- data/app/models/spree/import_row.rb +12 -0
- data/app/models/spree/line_item.rb +6 -1
- data/app/models/spree/market.rb +32 -1
- data/app/models/spree/metafield.rb +38 -0
- data/app/models/spree/metafield_definition.rb +29 -6
- data/app/models/spree/metafields/json.rb +10 -0
- data/app/models/spree/newsletter_subscriber.rb +19 -3
- data/app/models/spree/option_type.rb +48 -7
- data/app/models/spree/order/checkout.rb +3 -3
- data/app/models/spree/order.rb +102 -6
- data/app/models/spree/order_approval.rb +19 -0
- data/app/models/spree/order_cancellation.rb +19 -0
- data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
- data/app/models/spree/order_routing/rules/default_location.rb +16 -0
- data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
- data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
- data/app/models/spree/order_routing/strategy/base.rb +47 -0
- data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
- data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
- data/app/models/spree/order_routing/strategy/rules.rb +81 -0
- data/app/models/spree/order_routing_rule.rb +75 -0
- data/app/models/spree/permission_sets/configuration_management.rb +16 -0
- data/app/models/spree/permission_sets/product_display.rb +2 -0
- data/app/models/spree/permission_sets/product_management.rb +2 -0
- data/app/models/spree/price.rb +14 -1
- data/app/models/spree/price_list.rb +129 -17
- data/app/models/spree/price_rule.rb +11 -1
- data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
- data/app/models/spree/price_rules/market_rule.rb +16 -1
- data/app/models/spree/price_rules/user_rule.rb +21 -2
- data/app/models/spree/product/channels.rb +149 -0
- data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
- data/app/models/spree/product/slugs.rb +1 -1
- data/app/models/spree/product.rb +172 -31
- data/app/models/spree/product_publication.rb +43 -0
- data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
- data/app/models/spree/promotion/rules/country.rb +40 -18
- data/app/models/spree/promotion/rules/customer_group.rb +10 -1
- data/app/models/spree/promotion/rules/product.rb +4 -0
- data/app/models/spree/promotion/rules/taxon.rb +24 -1
- data/app/models/spree/promotion/rules/user.rb +21 -0
- data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
- data/app/models/spree/promotion.rb +22 -1
- data/app/models/spree/promotion_action.rb +17 -11
- data/app/models/spree/promotion_rule.rb +17 -18
- data/app/models/spree/search_provider/meilisearch.rb +12 -2
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/quantifier.rb +89 -9
- data/app/models/spree/stock_item.rb +36 -0
- data/app/models/spree/stock_location.rb +52 -0
- data/app/models/spree/stock_reservation.rb +38 -0
- data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
- data/app/models/spree/store.rb +18 -72
- data/app/models/spree/store_credit.rb +0 -8
- data/app/models/spree/store_product.rb +11 -23
- data/app/models/spree/taxon.rb +0 -5
- data/app/models/spree/user_identity.rb +1 -2
- data/app/models/spree/variant.rb +132 -18
- data/app/models/spree/variant_media.rb +46 -0
- data/app/models/spree/webhook_delivery.rb +1 -1
- data/app/models/spree/webhook_endpoint.rb +24 -0
- data/app/models/spree/wished_item.rb +0 -13
- data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
- data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
- data/app/presenters/spree/variant_presenter.rb +4 -3
- data/app/services/spree/addresses/update.rb +6 -8
- data/app/services/spree/cart/add_item.rb +10 -0
- data/app/services/spree/cart/empty.rb +2 -0
- data/app/services/spree/cart/remove_line_item.rb +10 -0
- data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
- data/app/services/spree/cart/set_quantity.rb +10 -0
- data/app/services/spree/carts/complete.rb +1 -0
- data/app/services/spree/carts/create.rb +1 -0
- data/app/services/spree/carts/update.rb +18 -2
- data/app/services/spree/carts/upsert_items.rb +6 -6
- data/app/services/spree/imports/row_processors/customer.rb +4 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
- data/app/services/spree/newsletter/link_user.rb +53 -0
- data/app/services/spree/newsletter/subscribe.rb +31 -9
- data/app/services/spree/orders/approve.rb +27 -6
- data/app/services/spree/orders/build_shipments.rb +29 -0
- data/app/services/spree/orders/cancel.rb +34 -3
- data/app/services/spree/orders/complete.rb +53 -0
- data/app/services/spree/orders/create.rb +156 -0
- data/app/services/spree/orders/update.rb +51 -0
- data/app/services/spree/orders/upsert_items.rb +70 -0
- data/app/services/spree/prices/bulk_upsert.rb +201 -0
- data/app/services/spree/products/duplicator.rb +1 -1
- data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
- data/app/services/spree/sample_data/loader.rb +30 -0
- data/app/services/spree/stock_reservations/extend.rb +19 -0
- data/app/services/spree/stock_reservations/release.rb +12 -0
- data/app/services/spree/stock_reservations/reserve.rb +103 -0
- data/app/services/spree/taxons/remove_products.rb +7 -1
- data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
- data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
- data/config/locales/en.yml +27 -10
- data/config/routes.rb +9 -0
- data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
- data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
- data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
- data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
- data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
- data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
- data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
- data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
- data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
- data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
- data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
- data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
- data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
- data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
- data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
- data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
- data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
- data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
- data/db/sample_data/channels.rb +12 -0
- data/db/sample_data/orders.rb +1 -1
- data/db/sample_data/products.csv +212 -212
- data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
- data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
- data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
- data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
- data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +14 -0
- data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
- data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
- data/lib/generators/spree/model/model_generator.rb +73 -7
- data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
- data/lib/generators/spree/model/templates/model.rb.tt +28 -2
- data/lib/spree/core/configuration.rb +7 -0
- data/lib/spree/core/controller_helpers/auth.rb +0 -12
- data/lib/spree/core/controller_helpers/currency.rb +0 -17
- data/lib/spree/core/controller_helpers/order.rb +0 -19
- data/lib/spree/core/dependencies.rb +5 -2
- data/lib/spree/core/engine.rb +54 -7
- data/lib/spree/core/permission_configuration.rb +15 -0
- data/lib/spree/core/preferences/masking.rb +47 -0
- data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +56 -5
- data/lib/spree/permitted_attributes.rb +9 -7
- data/lib/spree/testing_support/factories/address_factory.rb +16 -9
- data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
- data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
- data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
- data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
- data/lib/spree/testing_support/factories/product_factory.rb +16 -7
- data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
- data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
- data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
- data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
- data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
- data/lib/spree/testing_support/order_walkthrough.rb +1 -1
- data/lib/spree/testing_support/store.rb +10 -0
- data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
- data/lib/tasks/channels.rake +94 -0
- data/lib/tasks/core.rake +1 -0
- data/lib/tasks/media.rake +27 -0
- data/lib/tasks/products.rake +4 -6
- data/lib/tasks/publications.rake +60 -0
- data/lib/tasks/upgrade.rake +211 -0
- metadata +83 -18
- data/app/finders/spree/variants/visible_finder.rb +0 -23
- data/app/paginators/spree/shared/paginate.rb +0 -30
- data/app/presenters/spree/filters/price_presenter.rb +0 -23
- data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
- data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
- data/app/presenters/spree/product_summary_presenter.rb +0 -27
- data/app/presenters/spree/variants/options_presenter.rb +0 -82
- data/app/services/spree/classifications/reposition.rb +0 -23
- data/app/sorters/spree/orders/sort.rb +0 -10
- data/lib/spree/core/controller_helpers/common.rb +0 -14
- data/lib/spree/core/token_generator.rb +0 -23
- data/lib/spree/database_type_utilities.rb +0 -22
- data/lib/spree/testing_support/bar_ability.rb +0 -14
- data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Prices
|
|
3
|
+
# Bulk-writes Spree::Price rows and sweeps stale placeholder rows in
|
|
4
|
+
# one transaction.
|
|
5
|
+
#
|
|
6
|
+
# `spree_prices` is guarded by two partial unique indexes on PG/SQLite
|
|
7
|
+
# (collapsed to one composite index on MySQL):
|
|
8
|
+
# - base prices (price_list_id IS NULL): unique on (variant_id, currency)
|
|
9
|
+
# - overrides (price_list_id IS NOT NULL): unique on (variant_id, currency, price_list_id)
|
|
10
|
+
# A single `upsert_all` can only target one index, so rows ship in two
|
|
11
|
+
# batches — base vs override — each routed to the correct ON CONFLICT.
|
|
12
|
+
#
|
|
13
|
+
# Both indexes are also partial on `amount IS NOT NULL`, so `upsert_all`
|
|
14
|
+
# can't see placeholder rows (amount IS NULL) as conflict targets —
|
|
15
|
+
# filling in a placeholder via upsert inserts a sibling row instead of
|
|
16
|
+
# updating. The post-write sweep removes those.
|
|
17
|
+
class BulkUpsert
|
|
18
|
+
prepend Spree::ServiceModule::Base
|
|
19
|
+
|
|
20
|
+
# Two partial unique indexes guard `spree_prices` on PG/SQLite:
|
|
21
|
+
# - base prices (price_list_id IS NULL): unique on (variant_id, currency)
|
|
22
|
+
# - overrides (price_list_id IS NOT NULL): unique on (variant_id, currency, price_list_id)
|
|
23
|
+
# A single `upsert_all` can only target one index, so base-price rows
|
|
24
|
+
# and override rows ship in separate batches.
|
|
25
|
+
BASE_UNIQUE_BY = %i[variant_id currency].freeze
|
|
26
|
+
OVERRIDE_UNIQUE_BY = %i[variant_id currency price_list_id].freeze
|
|
27
|
+
|
|
28
|
+
# @param rows [Array<Hash>] each row must carry
|
|
29
|
+
# `variant_id`, `currency`, and `amount`; `price_list_id` and
|
|
30
|
+
# `compare_at_amount` are optional. A blank `amount` is treated
|
|
31
|
+
# as "clear this price."
|
|
32
|
+
# @return [Spree::ServiceModule::Result] success carries
|
|
33
|
+
# `{ price_count: N }` — the number of rows passed to
|
|
34
|
+
# `upsert_all`.
|
|
35
|
+
def call(rows:)
|
|
36
|
+
rows = Array(rows).map { |r| r.with_indifferent_access }
|
|
37
|
+
keyed = rows.select { |r| r[:variant_id].present? && r[:currency].present? }
|
|
38
|
+
# PG rejects an upsert with two rows hitting the same unique-key
|
|
39
|
+
# triple in one statement ("ON CONFLICT DO UPDATE command cannot
|
|
40
|
+
# affect row a second time"). Last-write-wins: keep the last
|
|
41
|
+
# occurrence of each triple.
|
|
42
|
+
deduped = keyed.reverse.uniq { |r| [r[:variant_id], r[:currency], r[:price_list_id]] }.reverse
|
|
43
|
+
upsert_rows, clear_rows = deduped.partition { |r| r[:amount].present? }
|
|
44
|
+
|
|
45
|
+
payload = build_payload(upsert_rows)
|
|
46
|
+
affected_keys = deduped.map { |r| [r[:variant_id], r[:currency], r[:price_list_id]] }
|
|
47
|
+
|
|
48
|
+
return success(price_count: 0) if affected_keys.empty?
|
|
49
|
+
|
|
50
|
+
base_rows, override_rows = payload.partition { |r| r[:price_list_id].nil? }
|
|
51
|
+
|
|
52
|
+
Spree::Price.transaction do
|
|
53
|
+
# MySQL treats NULL values as distinct in unique indexes, so
|
|
54
|
+
# `ON DUPLICATE KEY UPDATE` never fires for base prices —
|
|
55
|
+
# `upsert_all` would silently insert a sibling row. Route base
|
|
56
|
+
# rows through a SELECT-then-UPDATE/INSERT path on MySQL only.
|
|
57
|
+
if base_rows.any? && mysql?
|
|
58
|
+
upsert_base_rows_for_mysql(base_rows)
|
|
59
|
+
else
|
|
60
|
+
upsert_batch(base_rows, BASE_UNIQUE_BY)
|
|
61
|
+
end
|
|
62
|
+
upsert_batch(override_rows, OVERRIDE_UNIQUE_BY)
|
|
63
|
+
sweep(affected_keys, clear_rows)
|
|
64
|
+
# `upsert_all` and `delete_all` both skip AR callbacks, so the
|
|
65
|
+
# `Price -> Variant -> Product` `touch:` chain never fires —
|
|
66
|
+
# downstream caches (`cache_key_with_version`) would stay stale.
|
|
67
|
+
# Re-trigger the chain with one `.touch` per affected variant.
|
|
68
|
+
touch_variants(affected_keys.map(&:first).uniq)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
success(price_count: payload.length)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# `update_only` lists only domain columns. Rails adds `updated_at`
|
|
77
|
+
# automatically (when `record_timestamps` is on, which it is for
|
|
78
|
+
# `Spree::Price`); listing it explicitly here produces
|
|
79
|
+
# `SET updated_at = …, updated_at = …` on PG and the statement fails
|
|
80
|
+
# with "multiple assignments to same column".
|
|
81
|
+
def upsert_batch(rows, unique_by)
|
|
82
|
+
return if rows.empty?
|
|
83
|
+
|
|
84
|
+
Spree::Price.upsert_all(
|
|
85
|
+
rows,
|
|
86
|
+
update_only: %i[amount compare_at_amount],
|
|
87
|
+
**upsert_opts(unique_by)
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_payload(rows)
|
|
92
|
+
now = Time.current
|
|
93
|
+
rows.map do |row|
|
|
94
|
+
{
|
|
95
|
+
variant_id: row[:variant_id],
|
|
96
|
+
currency: row[:currency],
|
|
97
|
+
price_list_id: row[:price_list_id],
|
|
98
|
+
amount: parse_amount(row[:amount]),
|
|
99
|
+
compare_at_amount: parse_amount(row[:compare_at_amount]),
|
|
100
|
+
created_at: now,
|
|
101
|
+
updated_at: now
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Parses locale-aware decimal input ("1.234,56" in DE, "1,234.56"
|
|
107
|
+
# in en-US). Numeric values pass through; blank values become nil.
|
|
108
|
+
def parse_amount(value)
|
|
109
|
+
return nil if value.blank?
|
|
110
|
+
return value if value.is_a?(Numeric)
|
|
111
|
+
|
|
112
|
+
Spree::LocalizedNumber.parse(value)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def sweep(affected_keys, clear_rows)
|
|
116
|
+
cleared_keys = clear_rows.map { |r| [r[:variant_id], r[:currency], r[:price_list_id]] }.to_set
|
|
117
|
+
affected_set = affected_keys.to_set
|
|
118
|
+
|
|
119
|
+
candidates = Spree::Price
|
|
120
|
+
.where(
|
|
121
|
+
variant_id: affected_keys.map(&:first).uniq,
|
|
122
|
+
currency: affected_keys.map { |k| k[1] }.uniq,
|
|
123
|
+
price_list_id: affected_keys.map(&:last).uniq
|
|
124
|
+
)
|
|
125
|
+
.pluck(:id, :variant_id, :currency, :price_list_id, :amount)
|
|
126
|
+
|
|
127
|
+
doomed_ids = candidates.filter_map do |id, variant_id, currency, price_list_id, amount|
|
|
128
|
+
key = [variant_id, currency, price_list_id]
|
|
129
|
+
next unless affected_set.include?(key)
|
|
130
|
+
next id if amount.nil?
|
|
131
|
+
next id if cleared_keys.include?(key)
|
|
132
|
+
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Spree::Price.where(id: doomed_ids).delete_all if doomed_ids.any?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Bumps `updated_at` on the affected variants and their parent
|
|
140
|
+
# products to invalidate `cache_key_with_version`-based caches —
|
|
141
|
+
# `upsert_all` and `delete_all` skip the `Price -> Variant` and
|
|
142
|
+
# `Variant -> Product` `touch:` chains otherwise.
|
|
143
|
+
#
|
|
144
|
+
# We deliberately bypass AR callbacks: invoking `Variant#touch`
|
|
145
|
+
# would fire `after_commit :remove_prices_from_master_variant`,
|
|
146
|
+
# which `delete_all`s the master's prices whenever a non-master
|
|
147
|
+
# sibling has any prices — wiping exactly the rows the bulk
|
|
148
|
+
# upsert just persisted on a freshly-created list. `touch_all`
|
|
149
|
+
# gives us the cache bust without that side effect.
|
|
150
|
+
def touch_variants(variant_ids)
|
|
151
|
+
return if variant_ids.empty?
|
|
152
|
+
|
|
153
|
+
variants = Spree::Variant.where(id: variant_ids)
|
|
154
|
+
product_ids = variants.pluck(:product_id).uniq
|
|
155
|
+
variants.touch_all
|
|
156
|
+
Spree::Product.where(id: product_ids).touch_all if product_ids.any?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# MySQL infers conflict targets from its own unique indexes and
|
|
160
|
+
# rejects an explicit `unique_by`.
|
|
161
|
+
def upsert_opts(unique_by)
|
|
162
|
+
return {} if mysql?
|
|
163
|
+
|
|
164
|
+
{ unique_by: unique_by }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def mysql?
|
|
168
|
+
ActiveRecord::Base.connection.adapter_name == 'Mysql2'
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# MySQL-only path: NULLs are distinct in unique indexes, so
|
|
172
|
+
# `(variant_id, currency, NULL)` doesn't conflict with another
|
|
173
|
+
# `(variant_id, currency, NULL)` — `upsert_all` would insert a
|
|
174
|
+
# sibling instead of updating. Look up existing base rows first,
|
|
175
|
+
# update them one by one, and `insert_all` the rest.
|
|
176
|
+
def upsert_base_rows_for_mysql(rows)
|
|
177
|
+
rows_by_key = rows.index_by { |r| [r[:variant_id], r[:currency]] }
|
|
178
|
+
|
|
179
|
+
Spree::Price.where(
|
|
180
|
+
variant_id: rows.map { |r| r[:variant_id] }.uniq,
|
|
181
|
+
currency: rows.map { |r| r[:currency] }.uniq,
|
|
182
|
+
price_list_id: nil
|
|
183
|
+
).find_each do |price|
|
|
184
|
+
# The `IN (...)` query can return cross-pairs (e.g. `v=1,c=EUR`
|
|
185
|
+
# exists in the DB even though the caller only passed `v=1,c=USD`
|
|
186
|
+
# and `v=2,c=EUR`). Skip rows the caller didn't request.
|
|
187
|
+
row = rows_by_key.delete([price.variant_id, price.currency])
|
|
188
|
+
next unless row
|
|
189
|
+
|
|
190
|
+
price.update_columns(
|
|
191
|
+
amount: row[:amount],
|
|
192
|
+
compare_at_amount: row[:compare_at_amount],
|
|
193
|
+
updated_at: row[:updated_at]
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
Spree::Price.insert_all(rows_by_key.values) if rows_by_key.any?
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -28,7 +28,7 @@ module Spree
|
|
|
28
28
|
new_product.status = :draft
|
|
29
29
|
new_product.name = "COPY OF #{product.name}"
|
|
30
30
|
new_product.taxons = product.taxons
|
|
31
|
-
new_product.
|
|
31
|
+
new_product.channels = product.channels
|
|
32
32
|
new_product.created_at = nil
|
|
33
33
|
new_product.deleted_at = nil
|
|
34
34
|
new_product.updated_at = nil
|
|
@@ -67,14 +67,8 @@ module Spree
|
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
# ensure
|
|
71
|
-
params[:
|
|
72
|
-
|
|
73
|
-
# Preserve taxon associations from other stores
|
|
74
|
-
# Only merge taxon_ids from other stores if taxon_ids are being updated
|
|
75
|
-
if params.key?(:taxon_ids)
|
|
76
|
-
params[:taxon_ids] = merge_taxons_from_other_stores(params[:taxon_ids])
|
|
77
|
-
end
|
|
70
|
+
# ensure the product is owned by a store
|
|
71
|
+
params[:store_id] = store.id if params[:store_id].blank? && product.store_id.blank?
|
|
78
72
|
|
|
79
73
|
# Add empty list for option_type_ids and mark variants as removed if there are no variants and options
|
|
80
74
|
if params[:variants_attributes].blank? && variants_to_remove.any? && !params.key?(:option_type_ids)
|
|
@@ -186,28 +180,6 @@ module Spree
|
|
|
186
180
|
attributes
|
|
187
181
|
end
|
|
188
182
|
|
|
189
|
-
# Merges taxon IDs from other stores with submitted taxon IDs from current store.
|
|
190
|
-
#
|
|
191
|
-
# This prevents the loss of taxon associations from other stores when a product
|
|
192
|
-
# is edited in one store. Each store's taxonomy is independent, so editing
|
|
193
|
-
# categories in Store A should not affect categories in Store B.
|
|
194
|
-
#
|
|
195
|
-
# @param submitted_taxon_ids [Array<String>] Taxon IDs from the current store
|
|
196
|
-
# @return [Array<String>] Combined unique taxon IDs
|
|
197
|
-
def merge_taxons_from_other_stores(submitted_taxon_ids)
|
|
198
|
-
return submitted_taxon_ids if product.new_record?
|
|
199
|
-
|
|
200
|
-
# Get taxon IDs from other stores that should be preserved
|
|
201
|
-
other_stores_taxon_ids = product.taxons
|
|
202
|
-
.joins(:taxonomy)
|
|
203
|
-
.where.not(spree_taxonomies: { store_id: store.id })
|
|
204
|
-
.pluck(:id)
|
|
205
|
-
.map(&:to_s)
|
|
206
|
-
|
|
207
|
-
# Merge with submitted taxon IDs from current store and remove duplicates
|
|
208
|
-
(submitted_taxon_ids + other_stores_taxon_ids).uniq
|
|
209
|
-
end
|
|
210
|
-
|
|
211
183
|
def update_option_value_variants(option_value_params, existing_variant)
|
|
212
184
|
return {} unless option_value_params.present?
|
|
213
185
|
return {} unless can_manage_option_types?
|
|
@@ -15,6 +15,9 @@ module Spree
|
|
|
15
15
|
puts 'Loading sample markets...'
|
|
16
16
|
load_ruby_file('markets')
|
|
17
17
|
|
|
18
|
+
puts 'Loading sample channels...'
|
|
19
|
+
load_ruby_file('channels')
|
|
20
|
+
|
|
18
21
|
puts 'Loading sample metafield definitions...'
|
|
19
22
|
load_ruby_file('metafield_definitions')
|
|
20
23
|
|
|
@@ -24,6 +27,12 @@ module Spree
|
|
|
24
27
|
puts 'Loading sample products...'
|
|
25
28
|
load_products
|
|
26
29
|
|
|
30
|
+
puts 'Publishing sample products on the default channel...'
|
|
31
|
+
publish_sample_products
|
|
32
|
+
|
|
33
|
+
puts 'Loading sample categories...'
|
|
34
|
+
load_categories
|
|
35
|
+
|
|
27
36
|
puts 'Loading sample product translations...'
|
|
28
37
|
load_product_translations
|
|
29
38
|
|
|
@@ -67,6 +76,27 @@ module Spree
|
|
|
67
76
|
Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Products)
|
|
68
77
|
end
|
|
69
78
|
|
|
79
|
+
def publish_sample_products
|
|
80
|
+
store = Spree::Store.default
|
|
81
|
+
store.default_channel.add_products(store.product_ids)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def load_categories
|
|
85
|
+
store = Spree::Store.default
|
|
86
|
+
csv_path = sample_data_path.join('products.csv')
|
|
87
|
+
|
|
88
|
+
require 'csv'
|
|
89
|
+
::CSV.foreach(csv_path, headers: true) do |row|
|
|
90
|
+
product = store.products.find_by(slug: row['slug'])
|
|
91
|
+
next unless product
|
|
92
|
+
|
|
93
|
+
categories = [row['category1'], row['category2'], row['category3']].compact_blank
|
|
94
|
+
next if categories.empty?
|
|
95
|
+
|
|
96
|
+
Spree::Imports::CreateCategoriesJob.perform_now(product.id, store.id, categories)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
70
100
|
def load_product_translations
|
|
71
101
|
csv_path = sample_data_path.join('product_translations.csv')
|
|
72
102
|
return unless csv_path.exist?
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module StockReservations
|
|
3
|
+
class Extend
|
|
4
|
+
prepend Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
def call(order:)
|
|
7
|
+
return success(order) unless Spree::Config[:stock_reservations_enabled]
|
|
8
|
+
|
|
9
|
+
expires_at = Time.current + Spree::StockReservation.ttl_for(order)
|
|
10
|
+
|
|
11
|
+
Spree::StockReservation
|
|
12
|
+
.where(order_id: order.id)
|
|
13
|
+
.update_all(expires_at: expires_at, updated_at: Time.current)
|
|
14
|
+
|
|
15
|
+
success(order)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module StockReservations
|
|
3
|
+
class Reserve
|
|
4
|
+
prepend Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
def call(order:)
|
|
7
|
+
return success(order) unless Spree::Config[:stock_reservations_enabled]
|
|
8
|
+
|
|
9
|
+
expires_at = Time.current + Spree::StockReservation.ttl_for(order)
|
|
10
|
+
|
|
11
|
+
ApplicationRecord.transaction do
|
|
12
|
+
targets = build_targets(order)
|
|
13
|
+
break if targets.empty?
|
|
14
|
+
|
|
15
|
+
# Pessimistic lock + fresh read of count_on_hand. The lock serializes
|
|
16
|
+
# concurrent checkouts and we use the locked rows below so we never
|
|
17
|
+
# check stock against a stale association cache.
|
|
18
|
+
locked_stock_items = Spree::StockItem
|
|
19
|
+
.where(id: targets.map { |_, si| si.id })
|
|
20
|
+
.lock
|
|
21
|
+
.index_by(&:id)
|
|
22
|
+
|
|
23
|
+
held = held_by_others(locked_stock_items.keys, order.id)
|
|
24
|
+
existing = existing_reservations_for(targets)
|
|
25
|
+
|
|
26
|
+
this_order_used = Hash.new(0)
|
|
27
|
+
|
|
28
|
+
targets.each do |line_item, stock_item|
|
|
29
|
+
stock_item = locked_stock_items.fetch(stock_item.id)
|
|
30
|
+
available = stock_item.count_on_hand - held.fetch(stock_item.id, 0) - this_order_used[stock_item.id]
|
|
31
|
+
|
|
32
|
+
if available < line_item.quantity
|
|
33
|
+
raise InsufficientStockError.new(
|
|
34
|
+
line_item,
|
|
35
|
+
Spree.t(
|
|
36
|
+
:insufficient_stock_for_reservation,
|
|
37
|
+
default: '%{item} has only %{available} available',
|
|
38
|
+
item: line_item.variant.name,
|
|
39
|
+
available: [available, 0].max
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
this_order_used[stock_item.id] += line_item.quantity
|
|
45
|
+
|
|
46
|
+
reservation = existing[[stock_item.id, line_item.id]] ||
|
|
47
|
+
Spree::StockReservation.new(stock_item: stock_item, line_item: line_item)
|
|
48
|
+
reservation.order = order
|
|
49
|
+
reservation.quantity = line_item.quantity
|
|
50
|
+
reservation.expires_at = expires_at
|
|
51
|
+
reservation.save!
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
success(order)
|
|
56
|
+
rescue InsufficientStockError => e
|
|
57
|
+
failure(e.line_item, e.message)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def build_targets(order)
|
|
63
|
+
order.line_items.includes(variant: { stock_items: :stock_location }).filter_map do |line_item|
|
|
64
|
+
variant = line_item.variant
|
|
65
|
+
next unless variant&.should_track_inventory?
|
|
66
|
+
|
|
67
|
+
stock_item = select_stock_item(variant)
|
|
68
|
+
next if stock_item.nil? || stock_item.backorderable?
|
|
69
|
+
|
|
70
|
+
[line_item, stock_item]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def select_stock_item(variant)
|
|
75
|
+
variant.stock_items.detect { |si| si.stock_location&.active? && si.available? }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def held_by_others(stock_item_ids, exclude_order_id)
|
|
79
|
+
return {} if stock_item_ids.empty?
|
|
80
|
+
|
|
81
|
+
Spree::StockReservation
|
|
82
|
+
.active
|
|
83
|
+
.where(stock_item_id: stock_item_ids)
|
|
84
|
+
.where.not(order_id: exclude_order_id)
|
|
85
|
+
.group(:stock_item_id)
|
|
86
|
+
.sum(:quantity)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# One SELECT for all (stock_item_id, line_item_id) pairs we need to
|
|
90
|
+
# upsert. Returns a hash keyed by [stock_item_id, line_item_id].
|
|
91
|
+
def existing_reservations_for(targets)
|
|
92
|
+
return {} if targets.empty?
|
|
93
|
+
|
|
94
|
+
stock_item_ids = targets.map { |_, si| si.id }
|
|
95
|
+
line_item_ids = targets.map { |li, _| li.id }
|
|
96
|
+
|
|
97
|
+
Spree::StockReservation
|
|
98
|
+
.where(stock_item_id: stock_item_ids, line_item_id: line_item_ids)
|
|
99
|
+
.index_by { |r| [r.stock_item_id, r.line_item_id] }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -36,7 +36,7 @@ module Spree
|
|
|
36
36
|
|
|
37
37
|
if classifications_params.any?
|
|
38
38
|
opts = {}
|
|
39
|
-
opts[:unique_by] = :index_spree_products_taxons_on_product_id_and_taxon_id unless
|
|
39
|
+
opts[:unique_by] = :index_spree_products_taxons_on_product_id_and_taxon_id unless mysql_adapter?
|
|
40
40
|
|
|
41
41
|
Spree::Classification.upsert_all(
|
|
42
42
|
classifications_params,
|
|
@@ -58,6 +58,12 @@ module Spree
|
|
|
58
58
|
|
|
59
59
|
success(true)
|
|
60
60
|
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def mysql_adapter?
|
|
65
|
+
ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
|
|
66
|
+
end
|
|
61
67
|
end
|
|
62
68
|
end
|
|
63
69
|
end
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Spree
|
|
4
|
-
# Handles order completion events to update product metrics
|
|
5
|
-
#
|
|
6
|
-
# When an order is completed, this subscriber enqueues background jobs
|
|
7
|
-
# to refresh the metrics (units_sold_count, revenue) for each product
|
|
8
|
-
# in the order.
|
|
4
|
+
# Handles order completion events to update product metrics
|
|
5
|
+
# (+units_sold_count+, +revenue+).
|
|
9
6
|
class ProductMetricsSubscriber < Spree::Subscriber
|
|
10
7
|
subscribes_to 'order.completed'
|
|
11
8
|
|
|
@@ -17,12 +14,11 @@ module Spree
|
|
|
17
14
|
|
|
18
15
|
order = Spree::Order.find_by_param(order_id)
|
|
19
16
|
return unless order
|
|
20
|
-
return unless order.store_id
|
|
21
17
|
|
|
22
18
|
product_ids = order.line_items.includes(:variant).map { |li| li.variant.product_id }.uniq
|
|
23
19
|
return if product_ids.empty?
|
|
24
20
|
|
|
25
|
-
jobs = product_ids.map { |product_id| Spree::Products::RefreshMetricsJob.new(product_id
|
|
21
|
+
jobs = product_ids.map { |product_id| Spree::Products::RefreshMetricsJob.new(product_id) }
|
|
26
22
|
ActiveJob.perform_all_later(jobs)
|
|
27
23
|
end
|
|
28
24
|
end
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
<p>
|
|
13
13
|
<%= link_to Spree.t(:accept), spree.admin_invitation_url(@invitation, token: @invitation.token, host: @invitation.store.formatted_url) %>
|
|
14
14
|
</p>
|
|
15
|
+
<% else %>
|
|
16
|
+
<p>
|
|
17
|
+
<%= link_to Spree.t(:accept), main_app.admin_invitation_acceptance_url(@invitation) %>
|
|
18
|
+
</p>
|
|
15
19
|
<% end %>
|
|
16
20
|
|
|
17
21
|
<p>
|
data/config/locales/en.yml
CHANGED
|
@@ -323,6 +323,10 @@ en:
|
|
|
323
323
|
cannot_destroy_if_attached_to_line_items: Cannot delete Products that are added to placed Orders. In such cases, please discontinue them.
|
|
324
324
|
discontinue_on:
|
|
325
325
|
invalid_date_range: must be later than available date
|
|
326
|
+
spree/product_publication:
|
|
327
|
+
attributes:
|
|
328
|
+
unpublished_at:
|
|
329
|
+
must_be_after_published_at: must be after published date
|
|
326
330
|
spree/promotion:
|
|
327
331
|
attributes:
|
|
328
332
|
expires_at:
|
|
@@ -785,6 +789,8 @@ en:
|
|
|
785
789
|
are_you_sure: Are you sure?
|
|
786
790
|
are_you_sure_delete: Are you sure you want to delete this record?
|
|
787
791
|
assets: Media
|
|
792
|
+
assigned_variants: Assigned variants
|
|
793
|
+
assigned_variants_help: Pick the variants this image represents. Leave blank to apply to all variants.
|
|
788
794
|
associated_adjustment_closed: The associated adjustment is closed, and will not be recalculated. Do you want to open it?
|
|
789
795
|
at_symbol: "@"
|
|
790
796
|
attachments: Attachments
|
|
@@ -879,7 +885,8 @@ en:
|
|
|
879
885
|
change: Change
|
|
880
886
|
change_password: Change password
|
|
881
887
|
changes_published: Changes published!
|
|
882
|
-
channel:
|
|
888
|
+
channel: Sales channel
|
|
889
|
+
channels: Sales channels
|
|
883
890
|
charged: Charged
|
|
884
891
|
checkout: Checkout
|
|
885
892
|
checkout_message: Checkout message
|
|
@@ -1129,8 +1136,12 @@ en:
|
|
|
1129
1136
|
errors:
|
|
1130
1137
|
messages:
|
|
1131
1138
|
blank: can't be blank
|
|
1139
|
+
cannot_delete_default_channel: Default channel cannot be deleted. Promote another channel to default first.
|
|
1132
1140
|
cannot_remove_icon: Cannot remove image
|
|
1141
|
+
channel_store_mismatch: must belong to the same store
|
|
1133
1142
|
could_not_create_taxon: Could not create taxon
|
|
1143
|
+
invalid_order_routing_rule: is not a registered order routing rule
|
|
1144
|
+
invalid_order_routing_strategy: is not a registered order routing strategy
|
|
1134
1145
|
must_be_origin_only: must be an origin (scheme and host) without path, query, or fragment
|
|
1135
1146
|
no_shipping_methods_available: No shipping methods available for selected location, please change your address and try again.
|
|
1136
1147
|
store_association_can_not_be_changed: The store association can not be changed
|
|
@@ -1453,6 +1464,7 @@ en:
|
|
|
1453
1464
|
new_api_key: New API Key
|
|
1454
1465
|
new_balance: New balance
|
|
1455
1466
|
new_billing_address: New Billing Address
|
|
1467
|
+
new_channel: New sales channel
|
|
1456
1468
|
new_country: New Country
|
|
1457
1469
|
new_custom_domain: New Custom Domain
|
|
1458
1470
|
new_customer: New Customer
|
|
@@ -1641,6 +1653,10 @@ en:
|
|
|
1641
1653
|
order_number: Order %{number}
|
|
1642
1654
|
order_processed_successfully: Your order has been processed successfully
|
|
1643
1655
|
order_resumed: Order resumed
|
|
1656
|
+
order_routing:
|
|
1657
|
+
strategies:
|
|
1658
|
+
legacy: Legacy
|
|
1659
|
+
rules: Rules (ordered)
|
|
1644
1660
|
order_state:
|
|
1645
1661
|
address: address
|
|
1646
1662
|
awaiting_return: awaiting return
|
|
@@ -1886,15 +1902,24 @@ en:
|
|
|
1886
1902
|
promotion_not_cloned: 'Promotion has not been cloned. Reason: %{error}'
|
|
1887
1903
|
promotion_rule: Promotion Rule
|
|
1888
1904
|
promotion_rule_types:
|
|
1905
|
+
category:
|
|
1906
|
+
description: Order includes products in specified categories
|
|
1907
|
+
name: Categories
|
|
1889
1908
|
country:
|
|
1890
1909
|
description: Limit to orders with shipping address in a specific country
|
|
1891
1910
|
name: Country
|
|
1892
1911
|
currency:
|
|
1893
1912
|
description: Limit to orders in a specific currency
|
|
1894
1913
|
name: Currency
|
|
1914
|
+
customer:
|
|
1915
|
+
description: Available only to the specified customers
|
|
1916
|
+
name: Customers
|
|
1895
1917
|
customer_group:
|
|
1896
1918
|
description: Available only to customers in specified customer group(s)
|
|
1897
1919
|
name: Customer Group(s)
|
|
1920
|
+
customer_logged_in:
|
|
1921
|
+
description: Available only to logged in customers
|
|
1922
|
+
name: Only logged in customers
|
|
1898
1923
|
first_order:
|
|
1899
1924
|
description: Must be the customer's first order
|
|
1900
1925
|
name: First order
|
|
@@ -1910,15 +1935,6 @@ en:
|
|
|
1910
1935
|
product:
|
|
1911
1936
|
description: Order includes specified product(s)
|
|
1912
1937
|
name: Product(s)
|
|
1913
|
-
taxon:
|
|
1914
|
-
description: Order includes products with specified taxon(s)
|
|
1915
|
-
name: Taxon(s)
|
|
1916
|
-
user:
|
|
1917
|
-
description: Available only to the specified customers
|
|
1918
|
-
name: User
|
|
1919
|
-
user_logged_in:
|
|
1920
|
-
description: Available only to logged in users
|
|
1921
|
-
name: User Logged In
|
|
1922
1938
|
promotion_uses: Promotion uses
|
|
1923
1939
|
promotionable: Promotable
|
|
1924
1940
|
promotions: Promotions
|
|
@@ -2487,6 +2503,7 @@ en:
|
|
|
2487
2503
|
view: View
|
|
2488
2504
|
view_all: View all
|
|
2489
2505
|
view_full_details: View full details
|
|
2506
|
+
view_full_size: View full size
|
|
2490
2507
|
view_store: View store
|
|
2491
2508
|
view_your_store: View your store
|
|
2492
2509
|
visibility: Visibility
|
data/config/routes.rb
CHANGED
|
@@ -40,6 +40,15 @@ Rails.application.routes.draw do
|
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
|
+
# Used by admin mailers; the SPA derives the URL from `window.location.origin` instead.
|
|
44
|
+
direct :admin_invitation_acceptance do |invitation, _options = {}|
|
|
45
|
+
path = "/accept-invitation/#{invitation.prefixed_id}?token=#{invitation.token}"
|
|
46
|
+
base = Spree::Config[:admin_url].presence ||
|
|
47
|
+
(Rails.env.development? ? 'http://localhost:5173' : nil) ||
|
|
48
|
+
invitation.store&.formatted_url
|
|
49
|
+
|
|
50
|
+
base.present? ? "#{base.chomp('/')}#{path}" : path
|
|
51
|
+
end
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
Spree::Core::Engine.draw_routes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class CreateSpreeOrderCancellations < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_order_cancellations do |t|
|
|
4
|
+
t.references :order, null: false, index: false
|
|
5
|
+
t.string :reason, null: false
|
|
6
|
+
t.text :note
|
|
7
|
+
t.boolean :restock_items, null: false
|
|
8
|
+
t.boolean :refund_payments, null: false
|
|
9
|
+
t.decimal :refund_amount, precision: 10, scale: 2
|
|
10
|
+
t.boolean :notify_customer, null: false
|
|
11
|
+
t.references :canceled_by, polymorphic: true, index: false
|
|
12
|
+
if t.respond_to? :jsonb
|
|
13
|
+
t.jsonb :metadata
|
|
14
|
+
else
|
|
15
|
+
t.json :metadata
|
|
16
|
+
end
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :spree_order_cancellations, :order_id
|
|
21
|
+
add_index :spree_order_cancellations, [:canceled_by_id, :canceled_by_type],
|
|
22
|
+
name: 'idx_order_cancellations_canceled_by'
|
|
23
|
+
add_index :spree_order_cancellations, :created_at
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateSpreeOrderApprovals < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_order_approvals do |t|
|
|
4
|
+
t.references :order, null: false, index: false
|
|
5
|
+
t.string :status, null: false
|
|
6
|
+
t.string :level
|
|
7
|
+
t.text :note
|
|
8
|
+
t.references :approver, polymorphic: true, index: false
|
|
9
|
+
t.datetime :decided_at
|
|
10
|
+
if t.respond_to? :jsonb
|
|
11
|
+
t.jsonb :metadata
|
|
12
|
+
else
|
|
13
|
+
t.json :metadata
|
|
14
|
+
end
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :spree_order_approvals, [:order_id, :status]
|
|
19
|
+
add_index :spree_order_approvals, [:approver_id, :approver_type],
|
|
20
|
+
name: 'idx_order_approvals_approver'
|
|
21
|
+
end
|
|
22
|
+
end
|