spree_core 5.4.2 → 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 +3 -2
- 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/publishable.rb +1 -1
- 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 +37 -14
- 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/event.rb +6 -6
- data/app/models/spree/export.rb +32 -5
- data/app/models/spree/exports/product_translations.rb +1 -1
- data/app/models/spree/gateway/bogus.rb +6 -1
- 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 +7 -2
- data/app/models/spree/market.rb +57 -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_inventory.rb +24 -2
- 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/payment_setup_sessions/bogus.rb +4 -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/shipment.rb +10 -4
- 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/subscriber.rb +12 -12
- 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/formula_sanitizer.rb +28 -0
- 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/credit_cards/destroy.rb +1 -1
- 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/payments/handle_webhook.rb +3 -10
- 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/event_log_subscriber.rb +1 -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 +35 -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/20260504103113_add_type_to_spree_payment_setup_sessions.rb +6 -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/events/adapters/active_support_notifications.rb +1 -1
- data/lib/spree/events/adapters/base.rb +3 -3
- data/lib/spree/events/registry.rb +1 -1
- data/lib/spree/events.rb +1 -1
- 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 +86 -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
data/app/models/spree/market.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Spree
|
|
|
10
10
|
#
|
|
11
11
|
# Associations
|
|
12
12
|
#
|
|
13
|
-
belongs_to :store, class_name: 'Spree::Store', touch: true
|
|
13
|
+
belongs_to :store, class_name: 'Spree::Store', touch: true, inverse_of: :markets
|
|
14
14
|
has_many :market_countries, class_name: 'Spree::MarketCountry', dependent: :destroy
|
|
15
15
|
has_many :countries, through: :market_countries, class_name: 'Spree::Country'
|
|
16
16
|
has_many :orders, class_name: 'Spree::Order', dependent: :nullify
|
|
@@ -28,6 +28,7 @@ module Spree
|
|
|
28
28
|
# Callbacks
|
|
29
29
|
#
|
|
30
30
|
before_save :ensure_single_default
|
|
31
|
+
before_destroy :ensure_can_be_deleted
|
|
31
32
|
|
|
32
33
|
#
|
|
33
34
|
# Scopes
|
|
@@ -80,12 +81,67 @@ module Spree
|
|
|
80
81
|
@supported_locales_list ||= (supported_locales.to_s.split(',').map(&:strip) << default_locale).compact.uniq.sort
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
# Accepts an Array of locale codes and persists them as a comma-separated
|
|
85
|
+
# string on the `supported_locales` column. Strings are still accepted
|
|
86
|
+
# verbatim so legacy callers (the Rails admin form, raw seed scripts)
|
|
87
|
+
# keep working.
|
|
88
|
+
#
|
|
89
|
+
# @param value [Array<String>, String, nil]
|
|
90
|
+
def supported_locales=(value)
|
|
91
|
+
@supported_locales_list = nil
|
|
92
|
+
normalized = value.is_a?(Array) ? value.compact.uniq.reject(&:blank?).join(',') : value
|
|
93
|
+
super(normalized)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Read companion for `country_isos=`. Returns the sorted list of ISO codes
|
|
97
|
+
# currently assigned to the market.
|
|
98
|
+
#
|
|
99
|
+
# @return [Array<String>]
|
|
100
|
+
def country_isos
|
|
101
|
+
countries.map(&:iso).compact.sort
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Accepts an Array of 2-letter ISO codes and resolves them to the matching
|
|
105
|
+
# `Spree::Country` records, replacing the market's countries. Unknown codes
|
|
106
|
+
# are silently dropped — the `validates :countries, presence: true` covers
|
|
107
|
+
# the "every ISO was bogus" case.
|
|
108
|
+
#
|
|
109
|
+
# @param values [Array<String>]
|
|
110
|
+
def country_isos=(values)
|
|
111
|
+
isos = Array(values).compact.map { |v| v.to_s.upcase }.reject(&:blank?)
|
|
112
|
+
self.countries = isos.any? ? Spree::Country.where(iso: isos) : []
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Returns true when the market is safe to delete. A market cannot be deleted
|
|
116
|
+
# if it is the default market or the only market in the store, since
|
|
117
|
+
# Spree::Current.currency would have no fallback.
|
|
118
|
+
#
|
|
119
|
+
# @return [Boolean]
|
|
120
|
+
def can_be_deleted?
|
|
121
|
+
!default? && !last_in_store?
|
|
122
|
+
end
|
|
123
|
+
|
|
83
124
|
private
|
|
84
125
|
|
|
126
|
+
def last_in_store?
|
|
127
|
+
!self.class.where(store_id: store_id).where.not(id: id).exists?
|
|
128
|
+
end
|
|
129
|
+
|
|
85
130
|
def ensure_single_default
|
|
86
131
|
return unless default? && default_changed?
|
|
87
132
|
|
|
88
133
|
self.class.where(store_id: store_id, default: true).where.not(id: id).update_all(default: false)
|
|
89
134
|
end
|
|
135
|
+
|
|
136
|
+
def ensure_can_be_deleted
|
|
137
|
+
return if can_be_deleted?
|
|
138
|
+
|
|
139
|
+
if default?
|
|
140
|
+
errors.add(:base, :cannot_destroy_default_market)
|
|
141
|
+
else
|
|
142
|
+
errors.add(:base, :cannot_destroy_last_market)
|
|
143
|
+
end
|
|
144
|
+
throw(:abort)
|
|
145
|
+
end
|
|
90
146
|
end
|
|
91
147
|
end
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
class Metafield < Spree.base_class
|
|
3
|
+
# Map of API-facing tokens to Ruby STI class names. The wire format is the
|
|
4
|
+
# token (`short_text`); the database column stores the class name. Reads
|
|
5
|
+
# translate to the token via `field_type`; writes accept either form.
|
|
6
|
+
# Plugin-defined types fall through to the raw class name until 6.0 when a
|
|
7
|
+
# registration API lands.
|
|
8
|
+
TYPE_TOKENS = {
|
|
9
|
+
'short_text' => 'Spree::Metafields::ShortText',
|
|
10
|
+
'long_text' => 'Spree::Metafields::LongText',
|
|
11
|
+
'rich_text' => 'Spree::Metafields::RichText',
|
|
12
|
+
'number' => 'Spree::Metafields::Number',
|
|
13
|
+
'boolean' => 'Spree::Metafields::Boolean',
|
|
14
|
+
'json' => 'Spree::Metafields::Json'
|
|
15
|
+
}.freeze
|
|
16
|
+
TYPE_CLASS_TO_TOKEN = TYPE_TOKENS.invert.freeze
|
|
17
|
+
|
|
18
|
+
# Array form consumed by serializers via
|
|
19
|
+
# `typelize field_type: Spree::Metafield::FIELD_TYPE_TOKENS`. Typelizer
|
|
20
|
+
# emits a string-literal union in TypeScript and `{type: string, enum: […]}`
|
|
21
|
+
# in OpenAPI (string-array form was added in typelizer 0.10.0).
|
|
22
|
+
FIELD_TYPE_TOKENS = TYPE_TOKENS.keys.freeze
|
|
23
|
+
|
|
3
24
|
has_prefix_id :cf
|
|
4
25
|
|
|
5
26
|
#
|
|
@@ -8,6 +29,21 @@ module Spree
|
|
|
8
29
|
belongs_to :resource, polymorphic: true, touch: true
|
|
9
30
|
belongs_to :metafield_definition, class_name: 'Spree::MetafieldDefinition'
|
|
10
31
|
|
|
32
|
+
#
|
|
33
|
+
# API naming bridge — internal column rename lands in 6.0
|
|
34
|
+
#
|
|
35
|
+
alias_attribute :custom_field_definition_id, :metafield_definition_id
|
|
36
|
+
|
|
37
|
+
# API-facing form of the STI `type` column. Returns the token
|
|
38
|
+
# (`short_text`) when the row's type is a registered built-in; falls
|
|
39
|
+
# through to the raw class name for plugin types.
|
|
40
|
+
#
|
|
41
|
+
# `self[:type]` reads the raw column to bypass AR's STI reader (which
|
|
42
|
+
# returns the resolved class constant, not a string).
|
|
43
|
+
def field_type
|
|
44
|
+
TYPE_CLASS_TO_TOKEN[self[:type]] || self[:type]
|
|
45
|
+
end
|
|
46
|
+
|
|
11
47
|
#
|
|
12
48
|
# Delegations
|
|
13
49
|
#
|
|
@@ -43,6 +79,8 @@ module Spree
|
|
|
43
79
|
private
|
|
44
80
|
|
|
45
81
|
def set_type_from_metafield_definition
|
|
82
|
+
return if metafield_definition.blank?
|
|
83
|
+
|
|
46
84
|
self.type ||= metafield_definition.metafield_type
|
|
47
85
|
end
|
|
48
86
|
|
|
@@ -18,6 +18,7 @@ module Spree
|
|
|
18
18
|
validates :metafield_type, presence: true, inclusion: { in: :valid_available_types }
|
|
19
19
|
validates :resource_type, presence: true, inclusion: { in: :valid_available_resources }
|
|
20
20
|
validates :key, uniqueness: { scope: spree_base_uniqueness_scope + [:resource_type, :namespace] }
|
|
21
|
+
validate :field_type_input_must_be_recognized
|
|
21
22
|
|
|
22
23
|
#
|
|
23
24
|
# Scopes
|
|
@@ -51,14 +52,29 @@ module Spree
|
|
|
51
52
|
self.whitelisted_ransackable_attributes = %w[key namespace name resource_type display_on]
|
|
52
53
|
self.whitelisted_ransackable_scopes = %w[search multi_search]
|
|
53
54
|
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
# API naming bridge — internal columns rename in 6.0. `label` matches
|
|
56
|
+
# OptionType/OptionValue conventions. (`storefront_visible` lives on
|
|
57
|
+
# the `Spree::DisplayOn` concern, shared with PaymentMethod + ShippingMethod —
|
|
58
|
+
# see docs/plans/5.5-6.0-display-on-to-boolean.md.)
|
|
59
|
+
alias_attribute :label, :name
|
|
60
|
+
|
|
61
|
+
# API-facing token for the STI subclass name stored in `metafield_type`.
|
|
62
|
+
# Reader returns the registered token (`short_text`); writer accepts either
|
|
63
|
+
# the token or the legacy class-name form for back-compat.
|
|
64
|
+
def field_type
|
|
65
|
+
Spree::Metafield::TYPE_CLASS_TO_TOKEN[metafield_type] || metafield_type
|
|
58
66
|
end
|
|
59
67
|
|
|
60
|
-
def
|
|
61
|
-
|
|
68
|
+
def field_type=(value)
|
|
69
|
+
v = value.to_s
|
|
70
|
+
mapped = Spree::Metafield::TYPE_TOKENS[v]
|
|
71
|
+
# An input is "recognized" when it's either a known token (mapped to a
|
|
72
|
+
# class) or already a known class name. Anything else gets surfaced as
|
|
73
|
+
# an error on `field_type` so API clients get a token-vocabulary
|
|
74
|
+
# message instead of the raw class-name inclusion error on
|
|
75
|
+
# `metafield_type`.
|
|
76
|
+
@field_type_input_recognized = !mapped.nil? || Spree::Metafield::TYPE_CLASS_TO_TOKEN.key?(v)
|
|
77
|
+
self.metafield_type = mapped || value
|
|
62
78
|
end
|
|
63
79
|
|
|
64
80
|
# Returns the full key with namespace
|
|
@@ -91,6 +107,13 @@ module Spree
|
|
|
91
107
|
self.class.available_types.map(&:to_s)
|
|
92
108
|
end
|
|
93
109
|
|
|
110
|
+
def field_type_input_must_be_recognized
|
|
111
|
+
return if @field_type_input_recognized.nil? || @field_type_input_recognized
|
|
112
|
+
|
|
113
|
+
tokens = Spree::Metafield::TYPE_TOKENS.keys.join(', ')
|
|
114
|
+
errors.add(:field_type, "is not a known custom field type (expected one of: #{tokens})")
|
|
115
|
+
end
|
|
116
|
+
|
|
94
117
|
def valid_available_resources
|
|
95
118
|
self.class.available_resources.map(&:to_s)
|
|
96
119
|
end
|
|
@@ -3,6 +3,16 @@ module Spree
|
|
|
3
3
|
class Json < Spree::Metafield
|
|
4
4
|
validate :value_must_be_valid_json
|
|
5
5
|
|
|
6
|
+
# Accept either a JSON-serialized String (from CSV / Admin UI text
|
|
7
|
+
# input) or a raw Hash / Array (from API callers that ship parsed
|
|
8
|
+
# objects). Non-String inputs get JSON-serialized so the underlying
|
|
9
|
+
# text column always holds canonical JSON.
|
|
10
|
+
#
|
|
11
|
+
# @param raw [String, Hash, Array, nil]
|
|
12
|
+
def value=(raw)
|
|
13
|
+
super(raw.is_a?(Hash) || raw.is_a?(Array) ? raw.to_json : raw)
|
|
14
|
+
end
|
|
15
|
+
|
|
6
16
|
def serialize_value
|
|
7
17
|
JSON.parse(value)
|
|
8
18
|
rescue JSON::ParserError
|
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
has_prefix_id :sub
|
|
4
4
|
|
|
5
5
|
include Spree::Metafields
|
|
6
|
+
include Spree::SingleStoreResource
|
|
6
7
|
|
|
7
8
|
publishes_lifecycle_events
|
|
8
9
|
|
|
@@ -12,6 +13,7 @@ module Spree
|
|
|
12
13
|
# Associations
|
|
13
14
|
#
|
|
14
15
|
belongs_to :user, optional: true, class_name: Spree.user_class&.name
|
|
16
|
+
belongs_to :store, class_name: 'Spree::Store', required: true
|
|
15
17
|
|
|
16
18
|
#
|
|
17
19
|
# Validations
|
|
@@ -19,7 +21,7 @@ module Spree
|
|
|
19
21
|
validates :email,
|
|
20
22
|
presence: true,
|
|
21
23
|
format: { with: URI::MailTo::EMAIL_REGEXP },
|
|
22
|
-
uniqueness: { case_sensitive: false, scope: spree_base_uniqueness_scope }
|
|
24
|
+
uniqueness: { case_sensitive: false, scope: spree_base_uniqueness_scope + [:store_id] }
|
|
23
25
|
|
|
24
26
|
#
|
|
25
27
|
# Scopes
|
|
@@ -30,6 +32,7 @@ module Spree
|
|
|
30
32
|
#
|
|
31
33
|
# Callbacks
|
|
32
34
|
#
|
|
35
|
+
before_validation :set_store, unless: :store_id?
|
|
33
36
|
normalizes :email, with: ->(email) { email.to_s.strip.downcase.presence }
|
|
34
37
|
|
|
35
38
|
#
|
|
@@ -52,8 +55,15 @@ module Spree
|
|
|
52
55
|
Spree::CSV::NewsletterSubscriberPresenter.new(self).call
|
|
53
56
|
end
|
|
54
57
|
|
|
55
|
-
def self.subscribe(email:, user: nil)
|
|
56
|
-
Spree::
|
|
58
|
+
def self.subscribe(email:, user: nil, store: nil, redirect_url: nil)
|
|
59
|
+
store ||= Spree::Current.store
|
|
60
|
+
|
|
61
|
+
Spree::Newsletter::Subscribe.new(
|
|
62
|
+
email: email,
|
|
63
|
+
current_user: user,
|
|
64
|
+
current_store: store,
|
|
65
|
+
redirect_url: redirect_url
|
|
66
|
+
).call
|
|
57
67
|
end
|
|
58
68
|
|
|
59
69
|
def self.verify(token:)
|
|
@@ -61,5 +71,11 @@ module Spree
|
|
|
61
71
|
|
|
62
72
|
Spree::Newsletter::Verify.new(subscriber: subscriber).call
|
|
63
73
|
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def set_store
|
|
78
|
+
self.store ||= Spree::Current.store
|
|
79
|
+
end
|
|
64
80
|
end
|
|
65
81
|
end
|
|
@@ -27,7 +27,14 @@ module Spree
|
|
|
27
27
|
#
|
|
28
28
|
# Associations
|
|
29
29
|
with_options dependent: :destroy, inverse_of: :option_type do
|
|
30
|
-
|
|
30
|
+
# `autosave: true` makes the parent's `save`/`update`:
|
|
31
|
+
# - persist any built / mutated children in one transaction,
|
|
32
|
+
# - collect their validation errors onto `self.errors`,
|
|
33
|
+
# - destroy any child marked via `mark_for_destruction`.
|
|
34
|
+
# The custom `option_values=` writer below leans on this so the v3
|
|
35
|
+
# ResourceController gets `save returning false` + structured errors
|
|
36
|
+
# rather than raised exceptions.
|
|
37
|
+
has_many :option_values, -> { order(:position) }, autosave: true
|
|
31
38
|
has_many :product_option_types
|
|
32
39
|
end
|
|
33
40
|
has_many :products, through: :product_option_types
|
|
@@ -73,12 +80,6 @@ module Spree
|
|
|
73
80
|
after_update :touch_all_products, if: -> { saved_changes.key?(:presentation) }
|
|
74
81
|
after_destroy :touch_all_products
|
|
75
82
|
|
|
76
|
-
# legacy, name itself is now parameterized before saving
|
|
77
|
-
def filter_param
|
|
78
|
-
Spree::Deprecation.warn('Spree::OptionType#filter_param is deprecated and will be removed in Spree 5.5. Please use Spree::OptionType#name instead.')
|
|
79
|
-
name.parameterize
|
|
80
|
-
end
|
|
81
|
-
|
|
82
83
|
def self.color
|
|
83
84
|
colors.first
|
|
84
85
|
end
|
|
@@ -94,6 +95,46 @@ module Spree
|
|
|
94
95
|
color_swatch?
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
# Syncs option values from an array of hashes by mutating the in-memory
|
|
99
|
+
# `option_values` association — built/assigned children get persisted by
|
|
100
|
+
# `autosave: true` when the parent saves, and absent IDs get destroyed
|
|
101
|
+
# via `mark_for_destruction`. The single transaction is owned by the
|
|
102
|
+
# parent's `save`, so validation failures surface as `errors` and the
|
|
103
|
+
# whole thing rolls back together.
|
|
104
|
+
#
|
|
105
|
+
# Falls back to ActiveRecord's collection writer when given OptionValue
|
|
106
|
+
# records (e.g. from `accepts_nested_attributes_for` used by the legacy admin).
|
|
107
|
+
#
|
|
108
|
+
# @param option_values_params [Array<Hash>] array of option value attribute hashes
|
|
109
|
+
# @return [void]
|
|
110
|
+
def option_values=(option_values_params)
|
|
111
|
+
return super if option_values_params.blank? || option_values_params.first.is_a?(Spree::OptionValue)
|
|
112
|
+
|
|
113
|
+
# Load the association into the in-memory collection so subsequent
|
|
114
|
+
# `option_values.build` / `mark_for_destruction` mutations stay on the
|
|
115
|
+
# same instances `autosave` will traverse at parent-save time.
|
|
116
|
+
existing_by_id = option_values.to_a.index_by(&:id)
|
|
117
|
+
retained_ids = []
|
|
118
|
+
|
|
119
|
+
option_values_params.each do |value_data|
|
|
120
|
+
data = value_data.to_h.with_indifferent_access
|
|
121
|
+
value_id = data.delete(:id)
|
|
122
|
+
|
|
123
|
+
record = if value_id.present?
|
|
124
|
+
existing_by_id[Spree::PrefixedId.decode_prefixed_id(value_id) || value_id] ||
|
|
125
|
+
raise(ActiveRecord::RecordNotFound.new("Couldn't find Spree::OptionValue with param=#{value_id}", 'Spree::OptionValue'))
|
|
126
|
+
else
|
|
127
|
+
option_values.build
|
|
128
|
+
end
|
|
129
|
+
record.assign_attributes(data)
|
|
130
|
+
retained_ids << record.id if record.persisted?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
existing_by_id.each_value do |existing|
|
|
134
|
+
existing.mark_for_destruction unless retained_ids.include?(existing.id)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
97
138
|
private
|
|
98
139
|
|
|
99
140
|
def touch_all_products
|
|
@@ -135,7 +135,7 @@ module Spree
|
|
|
135
135
|
def subscribe_to_newsletter
|
|
136
136
|
return unless accept_marketing?
|
|
137
137
|
|
|
138
|
-
Spree::NewsletterSubscriber.subscribe(email: email, user: user)
|
|
138
|
+
Spree::NewsletterSubscriber.subscribe(email: email, user: user, store: store)
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
def self.go_to_state(name, options = {})
|
|
@@ -317,8 +317,8 @@ module Spree
|
|
|
317
317
|
# attributes for a single payment and its source, discarding attributes
|
|
318
318
|
# for payment methods other than the one selected
|
|
319
319
|
#
|
|
320
|
-
#
|
|
321
|
-
#
|
|
320
|
+
# If an existing credit card is provided, build the payment attributes
|
|
321
|
+
# from scratch so the amount can be set. Example payload:
|
|
322
322
|
#
|
|
323
323
|
# {
|
|
324
324
|
# "order": {
|
data/app/models/spree/order.rb
CHANGED
|
@@ -9,6 +9,11 @@ module Spree
|
|
|
9
9
|
class Order < Spree.base_class
|
|
10
10
|
has_prefix_id :or # Stripe: or_
|
|
11
11
|
|
|
12
|
+
# Legacy free-text `channel` column was replaced by the `channel_id` FK
|
|
13
|
+
# (see 6.0-order-routing.md). The string column stays in the DB so the
|
|
14
|
+
# 5.4-to-5.5 backfill rake can read it; AR ignores it everywhere else.
|
|
15
|
+
self.ignored_columns += ['channel']
|
|
16
|
+
|
|
12
17
|
PAYMENT_STATES = %w(balance_due credit_owed failed paid void)
|
|
13
18
|
SHIPMENT_STATES = %w(backorder canceled partial pending ready shipped)
|
|
14
19
|
LINE_ITEM_REMOVABLE_STATES = %w(cart address delivery payment confirm resumed)
|
|
@@ -104,23 +109,42 @@ module Spree
|
|
|
104
109
|
go_to_state :complete
|
|
105
110
|
end
|
|
106
111
|
|
|
107
|
-
self.whitelisted_ransackable_associations = %w[shipments user created_by approver canceler promotions bill_address ship_address line_items store]
|
|
112
|
+
self.whitelisted_ransackable_associations = %w[shipments user created_by approver canceler promotions bill_address ship_address line_items store channel tags]
|
|
108
113
|
self.whitelisted_ransackable_attributes = %w[
|
|
109
|
-
completed_at email number state payment_state shipment_state
|
|
110
|
-
total item_total item_count considered_risky
|
|
114
|
+
completed_at email number state status payment_state shipment_state
|
|
115
|
+
total item_total item_count considered_risky channel_id currency
|
|
111
116
|
]
|
|
112
|
-
self.whitelisted_ransackable_scopes = %w[refunded partially_refunded search multi_search]
|
|
117
|
+
self.whitelisted_ransackable_scopes = %w[complete incomplete refunded partially_refunded search multi_search]
|
|
113
118
|
|
|
114
119
|
attr_reader :coupon_code
|
|
115
120
|
attr_accessor :temporary_address
|
|
116
121
|
|
|
122
|
+
# Set to false on admin-initiated flows to suppress customer-facing emails.
|
|
123
|
+
attr_accessor :notify_customer
|
|
124
|
+
|
|
117
125
|
attribute :state_machine_resumed, :boolean
|
|
118
126
|
|
|
127
|
+
STATUSES = %w[draft placed canceled].freeze
|
|
128
|
+
|
|
129
|
+
attribute :status, :string, default: 'draft'
|
|
130
|
+
validates :status, inclusion: { in: STATUSES }
|
|
131
|
+
|
|
132
|
+
scope :drafts, -> { where(status: 'draft') }
|
|
133
|
+
scope :placed_orders, -> { where(status: 'placed') }
|
|
134
|
+
scope :canceled_orders, -> { where(status: 'canceled') }
|
|
135
|
+
|
|
119
136
|
acts_as_taggable_on :tags
|
|
120
137
|
acts_as_taggable_tenant :store_id
|
|
121
138
|
|
|
139
|
+
def tags=(tags)
|
|
140
|
+
self.tag_list = tags
|
|
141
|
+
end
|
|
142
|
+
|
|
122
143
|
ASSOCIATED_USER_ATTRIBUTES = [:user_id, :email, :bill_address_id, :ship_address_id]
|
|
123
144
|
|
|
145
|
+
# 6.0 forward-compat: User→Customer rename. Column stays user_id in 5.x.
|
|
146
|
+
alias_attribute :customer_id, :user_id
|
|
147
|
+
|
|
124
148
|
belongs_to :user, class_name: "::#{Spree.user_class}", optional: true, autosave: true
|
|
125
149
|
belongs_to :created_by, class_name: "::#{Spree.admin_user_class}", optional: true
|
|
126
150
|
belongs_to :approver, class_name: "::#{Spree.admin_user_class}", optional: true
|
|
@@ -140,6 +164,8 @@ module Spree
|
|
|
140
164
|
|
|
141
165
|
belongs_to :store, class_name: 'Spree::Store'
|
|
142
166
|
belongs_to :market, class_name: 'Spree::Market', optional: true
|
|
167
|
+
belongs_to :channel, class_name: 'Spree::Channel', optional: true
|
|
168
|
+
belongs_to :preferred_stock_location, class_name: 'Spree::StockLocation', optional: true
|
|
143
169
|
|
|
144
170
|
with_options dependent: :destroy do
|
|
145
171
|
has_many :state_changes, as: :stateful, class_name: 'Spree::StateChange'
|
|
@@ -148,11 +174,14 @@ module Spree
|
|
|
148
174
|
has_many :payment_sessions, inverse_of: :order, class_name: 'Spree::PaymentSession'
|
|
149
175
|
has_many :return_authorizations, inverse_of: :order, class_name: 'Spree::ReturnAuthorization'
|
|
150
176
|
has_many :adjustments, -> { order(:created_at) }, as: :adjustable, class_name: 'Spree::Adjustment'
|
|
177
|
+
has_many :cancellations, -> { order(:created_at) }, inverse_of: :order, class_name: 'Spree::OrderCancellation'
|
|
178
|
+
has_many :approvals, -> { order(:created_at) }, inverse_of: :order, class_name: 'Spree::OrderApproval'
|
|
151
179
|
end
|
|
152
180
|
has_many :reimbursements, inverse_of: :order, class_name: 'Spree::Reimbursement'
|
|
153
181
|
has_many :customer_returns, class_name: 'Spree::CustomerReturn', through: :return_authorizations
|
|
154
182
|
has_many :line_item_adjustments, through: :line_items, source: :adjustments
|
|
155
183
|
has_many :inventory_units, inverse_of: :order, class_name: 'Spree::InventoryUnit'
|
|
184
|
+
has_many :stock_reservations, class_name: 'Spree::StockReservation', inverse_of: :order, dependent: :destroy
|
|
156
185
|
has_many :return_items, through: :inventory_units, class_name: 'Spree::ReturnItem'
|
|
157
186
|
has_many :variants, through: :line_items
|
|
158
187
|
has_many :products, through: :variants
|
|
@@ -194,6 +223,7 @@ module Spree
|
|
|
194
223
|
# Needs to happen before save_permalink is called
|
|
195
224
|
before_validation :ensure_store_presence
|
|
196
225
|
before_validation :ensure_market_presence
|
|
226
|
+
before_validation :ensure_channel_presence
|
|
197
227
|
before_validation :ensure_currency_presence
|
|
198
228
|
before_validation :ensure_locale_presence
|
|
199
229
|
before_validation :resolve_market_from_currency, if: -> { persisted? && currency_changed? && !skip_market_resolution }
|
|
@@ -348,6 +378,21 @@ module Spree
|
|
|
348
378
|
completed_at.present?
|
|
349
379
|
end
|
|
350
380
|
|
|
381
|
+
# True when the order is mid-checkout: past the `cart` state but not yet
|
|
382
|
+
# completed or canceled. Used by stock reservation hooks and any flow
|
|
383
|
+
# that should only run during the active checkout phase.
|
|
384
|
+
def in_checkout?
|
|
385
|
+
!cart? && !complete? && !canceled?
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def draft?
|
|
389
|
+
status == 'draft'
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def placed?
|
|
393
|
+
status == 'placed'
|
|
394
|
+
end
|
|
395
|
+
|
|
351
396
|
# Checks if the order is fully refunded
|
|
352
397
|
# @return [Boolean]
|
|
353
398
|
def order_refunded?
|
|
@@ -459,6 +504,12 @@ module Spree
|
|
|
459
504
|
self.market ||= Spree::Current.market || store&.default_market
|
|
460
505
|
end
|
|
461
506
|
|
|
507
|
+
def ensure_channel_presence
|
|
508
|
+
return if channel_id.present?
|
|
509
|
+
|
|
510
|
+
self.channel = store&.default_channel
|
|
511
|
+
end
|
|
512
|
+
|
|
462
513
|
def allow_cancel?
|
|
463
514
|
return false if !completed? || canceled?
|
|
464
515
|
|
|
@@ -591,6 +642,7 @@ module Spree
|
|
|
591
642
|
end
|
|
592
643
|
|
|
593
644
|
updater.update_shipment_state
|
|
645
|
+
self.status = 'placed'
|
|
594
646
|
save!
|
|
595
647
|
updater.run_hooks
|
|
596
648
|
|
|
@@ -719,7 +771,34 @@ module Spree
|
|
|
719
771
|
# and are not returned or shipped should be deleted
|
|
720
772
|
inventory_units.on_hand_or_backordered.delete_all
|
|
721
773
|
|
|
722
|
-
self.shipments =
|
|
774
|
+
self.shipments = order_routing_strategy.for_allocation.map do |package|
|
|
775
|
+
package.to_shipment.tap { |s| s.address_id = ship_address_id }
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# Resolves the routing strategy from the channel override first, then the
|
|
780
|
+
# store default. Only a registered Spree::OrderRouting::Strategy::Base
|
|
781
|
+
# subclass is used; any other value (an unregistered/typo'd class, or a
|
|
782
|
+
# strategy that was unregistered after being persisted) is logged and
|
|
783
|
+
# skipped rather than raised, falling back to the default Rules strategy so
|
|
784
|
+
# a misconfiguration can't take down cart display or checkout.
|
|
785
|
+
#
|
|
786
|
+
# @return [Spree::OrderRouting::Strategy::Base]
|
|
787
|
+
def order_routing_strategy
|
|
788
|
+
klass = valid_order_routing_strategy_class(channel&.preferred_order_routing_strategy) ||
|
|
789
|
+
valid_order_routing_strategy_class(store.preferred_order_routing_strategy) ||
|
|
790
|
+
Spree::OrderRouting::Strategy::Rules
|
|
791
|
+
|
|
792
|
+
klass.new(order: self)
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Cascade for the `preferred_location` rule kind. Channel and B2B sources
|
|
796
|
+
# are layered in by their respective plans.
|
|
797
|
+
#
|
|
798
|
+
# @return [Integer, nil]
|
|
799
|
+
def inferred_preferred_stock_location_id
|
|
800
|
+
preferred_stock_location_id.presence ||
|
|
801
|
+
created_by&.try(:preferred_stock_location_id)
|
|
723
802
|
end
|
|
724
803
|
|
|
725
804
|
# Returns the total weight of the inventory units in the order
|
|
@@ -965,6 +1044,19 @@ module Spree
|
|
|
965
1044
|
|
|
966
1045
|
private
|
|
967
1046
|
|
|
1047
|
+
def valid_order_routing_strategy_class(klass_name)
|
|
1048
|
+
return if klass_name.blank?
|
|
1049
|
+
|
|
1050
|
+
klass = Spree.order_routing.strategies.find { |strategy| strategy.to_s == klass_name.to_s }
|
|
1051
|
+
return klass if klass
|
|
1052
|
+
|
|
1053
|
+
Rails.logger.warn(
|
|
1054
|
+
"[Spree] Ignoring unregistered order routing strategy #{klass_name.inspect} " \
|
|
1055
|
+
"for order #{number.inspect}; falling back to the default strategy."
|
|
1056
|
+
)
|
|
1057
|
+
nil
|
|
1058
|
+
end
|
|
1059
|
+
|
|
968
1060
|
def link_by_email
|
|
969
1061
|
self.email = user.email if user
|
|
970
1062
|
end
|
|
@@ -998,6 +1090,8 @@ module Spree
|
|
|
998
1090
|
end
|
|
999
1091
|
|
|
1000
1092
|
def after_cancel
|
|
1093
|
+
update_column(:status, 'canceled')
|
|
1094
|
+
|
|
1001
1095
|
shipments.each(&:cancel!)
|
|
1002
1096
|
|
|
1003
1097
|
# payments fully covered by gift card won't be refunded
|
|
@@ -1015,6 +1109,8 @@ module Spree
|
|
|
1015
1109
|
end
|
|
1016
1110
|
|
|
1017
1111
|
def after_resume
|
|
1112
|
+
update_column(:status, 'placed')
|
|
1113
|
+
|
|
1018
1114
|
shipments.each(&:resume!)
|
|
1019
1115
|
consider_risk
|
|
1020
1116
|
send_order_resumed_webhook
|
|
@@ -1089,7 +1185,7 @@ module Spree
|
|
|
1089
1185
|
end
|
|
1090
1186
|
|
|
1091
1187
|
def publish_order_completed_event
|
|
1092
|
-
publish_event('order.completed')
|
|
1188
|
+
publish_event('order.completed', event_payload.merge(notify_customer: notify_customer))
|
|
1093
1189
|
end
|
|
1094
1190
|
|
|
1095
1191
|
def publish_order_resumed_event
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class OrderApproval < Spree.base_class
|
|
3
|
+
has_prefix_id :appr
|
|
4
|
+
|
|
5
|
+
STATUSES = %w[pending approved rejected].freeze
|
|
6
|
+
|
|
7
|
+
attribute :metadata, default: -> { {} }
|
|
8
|
+
|
|
9
|
+
belongs_to :order, class_name: 'Spree::Order', inverse_of: :approvals
|
|
10
|
+
belongs_to :approver, polymorphic: true, optional: true
|
|
11
|
+
|
|
12
|
+
validates :order, presence: true
|
|
13
|
+
validates :status, presence: true, inclusion: { in: STATUSES }
|
|
14
|
+
|
|
15
|
+
scope :approved, -> { where(status: 'approved') }
|
|
16
|
+
scope :pending, -> { where(status: 'pending') }
|
|
17
|
+
scope :rejected, -> { where(status: 'rejected') }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class OrderCancellation < Spree.base_class
|
|
3
|
+
has_prefix_id :cncl
|
|
4
|
+
|
|
5
|
+
REASONS = %w[customer declined fraud inventory staff other expired].freeze
|
|
6
|
+
|
|
7
|
+
attribute :restock_items, :boolean, default: false
|
|
8
|
+
attribute :refund_payments, :boolean, default: false
|
|
9
|
+
attribute :notify_customer, :boolean, default: false
|
|
10
|
+
attribute :metadata, default: -> { {} }
|
|
11
|
+
|
|
12
|
+
belongs_to :order, class_name: 'Spree::Order', inverse_of: :cancellations
|
|
13
|
+
belongs_to :canceled_by, polymorphic: true, optional: true
|
|
14
|
+
|
|
15
|
+
validates :order, presence: true
|
|
16
|
+
validates :reason, presence: true, inclusion: { in: REASONS }
|
|
17
|
+
validates :refund_amount, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -17,13 +17,23 @@ module Spree
|
|
|
17
17
|
# In case shipment is passed the stock location should only unstock or
|
|
18
18
|
# restock items if the order is completed. That is so because stock items
|
|
19
19
|
# are always unstocked when the order is completed through +shipment.finalize+
|
|
20
|
-
def verify(shipment = nil, is_updated: false)
|
|
20
|
+
def verify(shipment = nil, is_updated: false, removing: false)
|
|
21
21
|
return unless order.completed? || shipment.present?
|
|
22
22
|
|
|
23
23
|
units_count = inventory_units.reload.sum(&:quantity)
|
|
24
24
|
line_item_changed = is_updated ? !line_item.saved_changes? : !line_item.changed?
|
|
25
25
|
|
|
26
|
-
if
|
|
26
|
+
if removing
|
|
27
|
+
# When the line item is being destroyed, only remove existing inventory.
|
|
28
|
+
# Adding here would create units that the LineItem `dependent: :destroy`
|
|
29
|
+
# cascade can't see (set_up_inventory writes through shipment.inventory_units,
|
|
30
|
+
# leaving line_item.inventory_units stale), producing an orphaned unit.
|
|
31
|
+
#
|
|
32
|
+
# Bypass `remove` because it routes through `set_quantity_to_remove` which
|
|
33
|
+
# assumes a quantity-change scenario; here we want to drain everything tied
|
|
34
|
+
# to this line item regardless of `line_item.quantity`.
|
|
35
|
+
remove_all_units(units_count, shipment) if units_count.positive?
|
|
36
|
+
elsif units_count < line_item.quantity
|
|
27
37
|
quantity = line_item.quantity - units_count
|
|
28
38
|
|
|
29
39
|
shipment ||= determine_target_shipment
|
|
@@ -49,6 +59,18 @@ module Spree
|
|
|
49
59
|
end
|
|
50
60
|
end
|
|
51
61
|
|
|
62
|
+
def remove_all_units(quantity, target_shipment = nil)
|
|
63
|
+
if target_shipment.present?
|
|
64
|
+
remove_from_shipment(target_shipment, quantity)
|
|
65
|
+
else
|
|
66
|
+
order.shipments.each do |shipment|
|
|
67
|
+
break if quantity.zero?
|
|
68
|
+
|
|
69
|
+
quantity -= remove_from_shipment(shipment, quantity)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
52
74
|
def set_quantity_to_remove(units_count)
|
|
53
75
|
if (units_count - line_item.quantity).zero?
|
|
54
76
|
line_item.quantity
|