spree_core 5.4.3 → 5.5.0.rc2
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 +60 -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 +7 -3
- 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 +45 -3
- data/app/models/spree/exports/coupon_codes.rb +4 -0
- data/app/models/spree/exports/newsletter_subscribers.rb +4 -0
- data/app/models/spree/exports/product_translations.rb +4 -0
- 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 +83 -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 +28 -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/migrate/20260612000001_change_spree_user_identities_info_to_jsonb.rb +13 -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 +17 -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/generators/spree/subscriber/subscriber_generator.rb +116 -0
- data/lib/generators/spree/subscriber/templates/subscriber.rb.tt +17 -0
- data/lib/generators/spree/subscriber/templates/subscriber_spec.rb.tt +9 -0
- 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/cli.rake +2 -1
- 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 +87 -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:
|
|
@@ -574,6 +578,7 @@ en:
|
|
|
574
578
|
invalid_token: Invalid authentication token.
|
|
575
579
|
locked: Your account is locked.
|
|
576
580
|
timeout: Your session expired, please sign in again to continue.
|
|
581
|
+
too_many_attempts: Too many attempts. Please wait a moment and try again.
|
|
577
582
|
unauthenticated: You need to sign in or sign up before continuing.
|
|
578
583
|
unconfirmed: You have to confirm your account before continuing.
|
|
579
584
|
mailer:
|
|
@@ -785,6 +790,8 @@ en:
|
|
|
785
790
|
are_you_sure: Are you sure?
|
|
786
791
|
are_you_sure_delete: Are you sure you want to delete this record?
|
|
787
792
|
assets: Media
|
|
793
|
+
assigned_variants: Assigned variants
|
|
794
|
+
assigned_variants_help: Pick the variants this image represents. Leave blank to apply to all variants.
|
|
788
795
|
associated_adjustment_closed: The associated adjustment is closed, and will not be recalculated. Do you want to open it?
|
|
789
796
|
at_symbol: "@"
|
|
790
797
|
attachments: Attachments
|
|
@@ -879,7 +886,8 @@ en:
|
|
|
879
886
|
change: Change
|
|
880
887
|
change_password: Change password
|
|
881
888
|
changes_published: Changes published!
|
|
882
|
-
channel:
|
|
889
|
+
channel: Sales channel
|
|
890
|
+
channels: Sales channels
|
|
883
891
|
charged: Charged
|
|
884
892
|
checkout: Checkout
|
|
885
893
|
checkout_message: Checkout message
|
|
@@ -1129,8 +1137,12 @@ en:
|
|
|
1129
1137
|
errors:
|
|
1130
1138
|
messages:
|
|
1131
1139
|
blank: can't be blank
|
|
1140
|
+
cannot_delete_default_channel: Default channel cannot be deleted. Promote another channel to default first.
|
|
1132
1141
|
cannot_remove_icon: Cannot remove image
|
|
1142
|
+
channel_store_mismatch: must belong to the same store
|
|
1133
1143
|
could_not_create_taxon: Could not create taxon
|
|
1144
|
+
invalid_order_routing_rule: is not a registered order routing rule
|
|
1145
|
+
invalid_order_routing_strategy: is not a registered order routing strategy
|
|
1134
1146
|
must_be_origin_only: must be an origin (scheme and host) without path, query, or fragment
|
|
1135
1147
|
no_shipping_methods_available: No shipping methods available for selected location, please change your address and try again.
|
|
1136
1148
|
store_association_can_not_be_changed: The store association can not be changed
|
|
@@ -1453,6 +1465,7 @@ en:
|
|
|
1453
1465
|
new_api_key: New API Key
|
|
1454
1466
|
new_balance: New balance
|
|
1455
1467
|
new_billing_address: New Billing Address
|
|
1468
|
+
new_channel: New sales channel
|
|
1456
1469
|
new_country: New Country
|
|
1457
1470
|
new_custom_domain: New Custom Domain
|
|
1458
1471
|
new_customer: New Customer
|
|
@@ -1641,6 +1654,10 @@ en:
|
|
|
1641
1654
|
order_number: Order %{number}
|
|
1642
1655
|
order_processed_successfully: Your order has been processed successfully
|
|
1643
1656
|
order_resumed: Order resumed
|
|
1657
|
+
order_routing:
|
|
1658
|
+
strategies:
|
|
1659
|
+
legacy: Legacy
|
|
1660
|
+
rules: Rules (ordered)
|
|
1644
1661
|
order_state:
|
|
1645
1662
|
address: address
|
|
1646
1663
|
awaiting_return: awaiting return
|
|
@@ -1886,15 +1903,24 @@ en:
|
|
|
1886
1903
|
promotion_not_cloned: 'Promotion has not been cloned. Reason: %{error}'
|
|
1887
1904
|
promotion_rule: Promotion Rule
|
|
1888
1905
|
promotion_rule_types:
|
|
1906
|
+
category:
|
|
1907
|
+
description: Order includes products in specified categories
|
|
1908
|
+
name: Categories
|
|
1889
1909
|
country:
|
|
1890
1910
|
description: Limit to orders with shipping address in a specific country
|
|
1891
1911
|
name: Country
|
|
1892
1912
|
currency:
|
|
1893
1913
|
description: Limit to orders in a specific currency
|
|
1894
1914
|
name: Currency
|
|
1915
|
+
customer:
|
|
1916
|
+
description: Available only to the specified customers
|
|
1917
|
+
name: Customers
|
|
1895
1918
|
customer_group:
|
|
1896
1919
|
description: Available only to customers in specified customer group(s)
|
|
1897
1920
|
name: Customer Group(s)
|
|
1921
|
+
customer_logged_in:
|
|
1922
|
+
description: Available only to logged in customers
|
|
1923
|
+
name: Only logged in customers
|
|
1898
1924
|
first_order:
|
|
1899
1925
|
description: Must be the customer's first order
|
|
1900
1926
|
name: First order
|
|
@@ -1910,15 +1936,6 @@ en:
|
|
|
1910
1936
|
product:
|
|
1911
1937
|
description: Order includes specified product(s)
|
|
1912
1938
|
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
1939
|
promotion_uses: Promotion uses
|
|
1923
1940
|
promotionable: Promotable
|
|
1924
1941
|
promotions: Promotions
|
|
@@ -2487,6 +2504,7 @@ en:
|
|
|
2487
2504
|
view: View
|
|
2488
2505
|
view_all: View all
|
|
2489
2506
|
view_full_details: View full details
|
|
2507
|
+
view_full_size: View full size
|
|
2490
2508
|
view_store: View store
|
|
2491
2509
|
view_your_store: View your store
|
|
2492
2510
|
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
|