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
|
@@ -1,42 +1,64 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Newsletter
|
|
3
3
|
class Subscribe
|
|
4
|
-
def initialize(email:, current_user: nil)
|
|
4
|
+
def initialize(email:, current_user: nil, current_store: nil, redirect_url: nil)
|
|
5
5
|
@email = email
|
|
6
6
|
@current_user = current_user
|
|
7
|
+
@current_store = current_store || Spree::Store.current
|
|
8
|
+
@redirect_url = redirect_url
|
|
7
9
|
end
|
|
8
10
|
|
|
9
11
|
def call
|
|
10
|
-
|
|
12
|
+
if existed_subscription.present?
|
|
13
|
+
Spree::Newsletter::LinkUser.new(subscriber: existed_subscription, user: known_user).call
|
|
14
|
+
return existed_subscription
|
|
15
|
+
end
|
|
11
16
|
|
|
12
17
|
ActiveRecord::Base.transaction do
|
|
13
18
|
upsert_subscriber
|
|
14
19
|
return subscriber if subscriber.errors.any?
|
|
15
20
|
|
|
21
|
+
Spree::Newsletter::LinkUser.new(subscriber: subscriber, user: known_user).call
|
|
22
|
+
|
|
16
23
|
if subscriber.email == current_user&.email
|
|
17
|
-
#
|
|
24
|
+
# User's email is already verified by login — skip the double opt-in.
|
|
18
25
|
Spree::Newsletter::Verify.new(subscriber: subscriber).call
|
|
19
26
|
end
|
|
20
27
|
end
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
subscriber.publish_event('newsletter_subscriber.subscribed') unless subscriber.verified?
|
|
29
|
+
subscriber.publish_event('newsletter_subscriber.subscription_requested', subscription_requested_payload) unless subscriber.verified?
|
|
24
30
|
subscriber
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
private
|
|
28
34
|
|
|
29
|
-
attr_reader :email, :current_user
|
|
35
|
+
attr_reader :email, :current_user, :current_store, :redirect_url
|
|
36
|
+
|
|
37
|
+
def subscription_requested_payload
|
|
38
|
+
payload = {
|
|
39
|
+
id: subscriber.prefixed_id,
|
|
40
|
+
email: subscriber.email,
|
|
41
|
+
verification_token: subscriber.verification_token,
|
|
42
|
+
store_id: current_store.prefixed_id,
|
|
43
|
+
customer_id: subscriber.user&.prefixed_id
|
|
44
|
+
}
|
|
45
|
+
payload[:redirect_url] = redirect_url if redirect_url.present?
|
|
46
|
+
payload
|
|
47
|
+
end
|
|
30
48
|
|
|
31
49
|
def upsert_subscriber
|
|
32
|
-
@upsert_subscriber ||= Spree::NewsletterSubscriber.find_or_create_by(email: email) do |new_record|
|
|
33
|
-
new_record.user =
|
|
50
|
+
@upsert_subscriber ||= Spree::NewsletterSubscriber.find_or_create_by(email: email, store: current_store) do |new_record|
|
|
51
|
+
new_record.user = known_user
|
|
34
52
|
end
|
|
35
53
|
end
|
|
36
54
|
alias_method :subscriber, :upsert_subscriber
|
|
37
55
|
|
|
38
56
|
def existed_subscription
|
|
39
|
-
@existed_subscription ||= Spree::NewsletterSubscriber.verified.find_by(email: email)
|
|
57
|
+
@existed_subscription ||= Spree::NewsletterSubscriber.verified.find_by(email: email, store: current_store)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def known_user
|
|
61
|
+
@known_user ||= current_user || Spree.user_class.find_by(email: email)
|
|
40
62
|
end
|
|
41
63
|
end
|
|
42
64
|
end
|
|
@@ -3,16 +3,37 @@ module Spree
|
|
|
3
3
|
class Approve
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
# Approves an order and records a Spree::OrderApproval history record.
|
|
7
|
+
#
|
|
8
|
+
# The legacy keyword `approver:` remains valid; new keywords (`level:`, `note:`)
|
|
9
|
+
# are additive and stored on the approval record.
|
|
10
|
+
#
|
|
11
|
+
# @param order [Spree::Order]
|
|
12
|
+
# @param approver [Object, nil] the user/admin who approved
|
|
13
|
+
# @param level [String, nil] approval level (used by 6.0 multi-level B2B flow)
|
|
14
|
+
# @param note [String, nil] staff-facing note
|
|
15
|
+
# @return [Spree::ServiceModule::Result]
|
|
16
|
+
def call(order:, approver: nil, level: nil, note: nil)
|
|
17
|
+
decided_at = Time.current
|
|
18
|
+
|
|
19
|
+
order.transaction do
|
|
20
|
+
order.approvals.create!(
|
|
21
|
+
status: 'approved',
|
|
22
|
+
level: level,
|
|
23
|
+
note: note,
|
|
24
|
+
approver: approver,
|
|
25
|
+
decided_at: decided_at,
|
|
26
|
+
created_at: decided_at
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
changes = { considered_risky: false, approved_at: decided_at }
|
|
30
|
+
changes[:approver_id] = approver.id if approver.present?
|
|
31
|
+
order.update_columns(changes)
|
|
10
32
|
end
|
|
11
|
-
order.update_columns(changes)
|
|
12
33
|
|
|
13
34
|
order.publish_event('order.approved')
|
|
14
35
|
success(order.reload)
|
|
15
|
-
rescue ActiveRecord::
|
|
36
|
+
rescue ActiveRecord::RecordInvalid, StateMachines::InvalidTransition
|
|
16
37
|
failure(order)
|
|
17
38
|
end
|
|
18
39
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Orders
|
|
3
|
+
# Shared shipment-building step for admin order Create / Update.
|
|
4
|
+
#
|
|
5
|
+
# Rebuilds shipments from scratch (Stock::Coordinator), then layers in
|
|
6
|
+
# tax, costs, and free-shipping promotions so totals reflect delivery
|
|
7
|
+
# before payment is requested. Without this, draft orders would expose
|
|
8
|
+
# delivery_total: 0.0 until completion is attempted — which is too late.
|
|
9
|
+
#
|
|
10
|
+
# No-op when the order has no shipping address, no line items, or does
|
|
11
|
+
# not require delivery (digital orders, etc.).
|
|
12
|
+
class BuildShipments
|
|
13
|
+
prepend Spree::ServiceModule::Base
|
|
14
|
+
|
|
15
|
+
def call(order:)
|
|
16
|
+
return success(order) unless order.ship_address_id.present?
|
|
17
|
+
return success(order) unless order.line_items.any?
|
|
18
|
+
return success(order) unless order.delivery_required?
|
|
19
|
+
|
|
20
|
+
order.create_proposed_shipments
|
|
21
|
+
order.create_shipment_tax_charge!
|
|
22
|
+
order.set_shipments_cost
|
|
23
|
+
order.apply_free_shipping_promotions
|
|
24
|
+
|
|
25
|
+
success(order)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -3,19 +3,50 @@ module Spree
|
|
|
3
3
|
class Cancel
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
DEFAULT_REASON = 'other'.freeze
|
|
7
|
+
|
|
8
|
+
# Cancels an order and records a Spree::OrderCancellation history record.
|
|
9
|
+
# Legacy `canceler:` and `canceled_at:` remain valid; new keywords are additive.
|
|
10
|
+
#
|
|
11
|
+
# @param order [Spree::Order]
|
|
12
|
+
# @param canceler [Object, nil] the user/admin who initiated the cancellation
|
|
13
|
+
# @param canceled_at [Time, nil] timestamp (defaults to Time.current)
|
|
14
|
+
# @param reason [String] one of Spree::OrderCancellation::REASONS
|
|
15
|
+
# @param note [String, nil] staff-facing note
|
|
16
|
+
# @param restock_items [Boolean] whether to return inventory
|
|
17
|
+
# @param refund_payments [Boolean] whether to refund captured payments
|
|
18
|
+
# @param refund_amount [BigDecimal, Numeric, nil] amount to refund;
|
|
19
|
+
# when refund_payments is true and this is nil, defaults to order.payment_total
|
|
20
|
+
# @param notify_customer [Boolean] hint for subscribers
|
|
21
|
+
# @return [Spree::ServiceModule::Result]
|
|
22
|
+
def call(order:, canceler: nil, canceled_at: nil,
|
|
23
|
+
reason: DEFAULT_REASON, note: nil,
|
|
24
|
+
restock_items: false, refund_payments: false, refund_amount: nil,
|
|
25
|
+
notify_customer: false)
|
|
7
26
|
canceled_at ||= Time.current
|
|
27
|
+
refund_amount ||= order.payment_total if refund_payments
|
|
8
28
|
|
|
9
29
|
order.transaction do
|
|
30
|
+
order.cancellations.create!(
|
|
31
|
+
reason: reason,
|
|
32
|
+
note: note,
|
|
33
|
+
restock_items: restock_items,
|
|
34
|
+
refund_payments: refund_payments,
|
|
35
|
+
refund_amount: refund_amount,
|
|
36
|
+
notify_customer: notify_customer,
|
|
37
|
+
canceled_by: canceler,
|
|
38
|
+
created_at: canceled_at
|
|
39
|
+
)
|
|
40
|
+
|
|
10
41
|
changes = { canceled_at: canceled_at }
|
|
11
42
|
changes[:canceler_id] = canceler.id if canceler.present?
|
|
12
43
|
order.update_columns(changes)
|
|
13
44
|
order.cancel!
|
|
14
45
|
end
|
|
15
46
|
|
|
16
|
-
order.publish_event('order.canceled')
|
|
47
|
+
order.publish_event('order.canceled', order.event_payload.merge(notify_customer: notify_customer))
|
|
17
48
|
success(order.reload)
|
|
18
|
-
rescue ActiveRecord::
|
|
49
|
+
rescue ActiveRecord::RecordInvalid, StateMachines::InvalidTransition
|
|
19
50
|
failure(order)
|
|
20
51
|
end
|
|
21
52
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Orders
|
|
3
|
+
# Admin-side order completion.
|
|
4
|
+
#
|
|
5
|
+
# Distinct from Spree::Carts::Complete (storefront checkout). Callers must
|
|
6
|
+
# wrap invocation in Spree::Api::V3::OrderLock#with_order_lock — this
|
|
7
|
+
# service does not lock the row itself.
|
|
8
|
+
#
|
|
9
|
+
# @param order [Spree::Order]
|
|
10
|
+
# @param payment_pending [Boolean] if true, completes the order without
|
|
11
|
+
# processing payments. Order is placed but `payment_status` may be
|
|
12
|
+
# 'balance_due'. Useful for B2B / invoice-later flows.
|
|
13
|
+
# @param notify_customer [Boolean] if true, the customer receives the
|
|
14
|
+
# standard order confirmation email. Defaults to false — admin orders
|
|
15
|
+
# complete silently unless explicitly opted in.
|
|
16
|
+
# @return [Spree::ServiceModule::Result]
|
|
17
|
+
class Complete
|
|
18
|
+
prepend Spree::ServiceModule::Base
|
|
19
|
+
|
|
20
|
+
def call(order:, payment_pending: false, notify_customer: false)
|
|
21
|
+
order.notify_customer = notify_customer
|
|
22
|
+
|
|
23
|
+
return success(order) if order.completed?
|
|
24
|
+
return failure(order, 'Order is canceled') if order.canceled?
|
|
25
|
+
|
|
26
|
+
process_payments!(order) if order.payment_required? && !payment_pending
|
|
27
|
+
|
|
28
|
+
return failure(order, order.errors.full_messages.to_sentence) if order.errors.any?
|
|
29
|
+
|
|
30
|
+
advance_to_complete!(order)
|
|
31
|
+
|
|
32
|
+
if order.reload.complete?
|
|
33
|
+
success(order)
|
|
34
|
+
else
|
|
35
|
+
failure(order, order.errors.full_messages.to_sentence.presence || 'Could not complete order')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def process_payments!(order)
|
|
42
|
+
return if order.payment_total >= order.total
|
|
43
|
+
return if order.payments.valid.any?(&:completed?) && order.unprocessed_payments.empty?
|
|
44
|
+
|
|
45
|
+
order.process_payments!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def advance_to_complete!(order)
|
|
49
|
+
order.next until order.complete? || order.errors.present?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Orders
|
|
3
|
+
# Admin-side order creation. One-shot: customer, items, addresses, currency,
|
|
4
|
+
# market, locale, notes, metadata, and a coupon code in a single call.
|
|
5
|
+
# Invalid coupons are non-fatal — the order is created and `result.value`
|
|
6
|
+
# carries `discount_application_errors`.
|
|
7
|
+
#
|
|
8
|
+
# Standalone from Spree::Carts::Create (storefront). Admin-created orders
|
|
9
|
+
# are first-class Spree::Order records (status: 'draft') in 5.x and remain
|
|
10
|
+
# so in 6.0 — Spree::Cart in 6.0 is storefront-only.
|
|
11
|
+
class Create
|
|
12
|
+
prepend Spree::ServiceModule::Base
|
|
13
|
+
|
|
14
|
+
attr_reader :discount_application_errors
|
|
15
|
+
|
|
16
|
+
# @param store [Spree::Store]
|
|
17
|
+
# @param user [Object, nil] resolved customer (Spree.user_class instance)
|
|
18
|
+
# @param params [Hash] order params (see admin API docs)
|
|
19
|
+
# @return [Spree::ServiceModule::Result]
|
|
20
|
+
def call(store:, user: nil, params: {})
|
|
21
|
+
@store = store
|
|
22
|
+
@user = user
|
|
23
|
+
@params = params.to_h.deep_symbolize_keys
|
|
24
|
+
@discount_application_errors = []
|
|
25
|
+
|
|
26
|
+
return failure(:store_is_required) if store.nil?
|
|
27
|
+
|
|
28
|
+
order = nil
|
|
29
|
+
ApplicationRecord.transaction do
|
|
30
|
+
order = build_order
|
|
31
|
+
assign_addresses(order)
|
|
32
|
+
order.tags = @params[:tags] if @params[:tags]
|
|
33
|
+
order.save!
|
|
34
|
+
|
|
35
|
+
add_items(order) if @params[:items].present?
|
|
36
|
+
build_shipments(order)
|
|
37
|
+
apply_coupon(order) if @params[:coupon_code].present?
|
|
38
|
+
order.update_with_updater!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
success(order.reload)
|
|
42
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
43
|
+
failure(e.record, e.record.errors.full_messages.to_sentence)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_order
|
|
49
|
+
attrs = {
|
|
50
|
+
user: @user,
|
|
51
|
+
email: @params[:email] || @user&.email,
|
|
52
|
+
currency: @params[:currency].presence&.upcase || @store.default_currency,
|
|
53
|
+
locale: @params[:locale] || Spree::Current.locale,
|
|
54
|
+
customer_note: @params[:customer_note],
|
|
55
|
+
internal_note: @params[:internal_note],
|
|
56
|
+
metadata: @params[:metadata].to_h,
|
|
57
|
+
token: Spree::GenerateToken.new.call(Spree::Order),
|
|
58
|
+
status: 'draft'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
attrs[:market] = resolve_market if @params[:market_id].present?
|
|
62
|
+
attrs[:channel] = resolve_channel if @params[:channel_id].present?
|
|
63
|
+
attrs[:preferred_stock_location] = resolve_preferred_stock_location if @params[:preferred_stock_location_id].present?
|
|
64
|
+
attrs.compact_blank!
|
|
65
|
+
|
|
66
|
+
@store.orders.new(attrs)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def resolve_market
|
|
70
|
+
@store.markets.find_by_param!(@params[:market_id])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def resolve_channel
|
|
74
|
+
@store.channels.find_by_param!(@params[:channel_id])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def resolve_preferred_stock_location
|
|
78
|
+
Spree::StockLocation.for_store(@store).find_by_param!(@params[:preferred_stock_location_id])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def assign_addresses(order)
|
|
82
|
+
if @params[:use_customer_default_address] && @user
|
|
83
|
+
@user.association(:bill_address).load_target
|
|
84
|
+
@user.association(:ship_address).load_target
|
|
85
|
+
order.bill_address = @user.bill_address&.dup
|
|
86
|
+
order.ship_address = @user.ship_address&.dup
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
assign_address(order, :ship_address, @params[:shipping_address_id], @params[:shipping_address])
|
|
90
|
+
assign_address(order, :bill_address, @params[:billing_address_id], @params[:billing_address])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def assign_address(order, association, address_id, address_attrs)
|
|
94
|
+
if address_id.present?
|
|
95
|
+
address = resolve_user_address(address_id)
|
|
96
|
+
order.public_send(:"#{association}_id=", address.id) if address
|
|
97
|
+
elsif address_attrs.present?
|
|
98
|
+
order.public_send(:"#{association}_attributes=", address_attrs)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def resolve_user_address(address_id)
|
|
103
|
+
return unless @user
|
|
104
|
+
|
|
105
|
+
@user.addresses.find_by_param(address_id)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def add_items(order)
|
|
109
|
+
result = Spree::Orders::UpsertItems.call(order: order, items: @params[:items])
|
|
110
|
+
return if result.success?
|
|
111
|
+
|
|
112
|
+
propagate_step_failure!(order, result, fallback: 'Failed to add items to order')
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def build_shipments(order)
|
|
116
|
+
result = Spree::Orders::BuildShipments.call(order: order)
|
|
117
|
+
return if result.success?
|
|
118
|
+
|
|
119
|
+
propagate_step_failure!(order, result, fallback: 'Failed to build shipments')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Surface the failing record's errors on the order so the API response
|
|
123
|
+
# carries an actionable message instead of an empty +processing_error+.
|
|
124
|
+
# Falls back to a static message when neither the record nor the result
|
|
125
|
+
# carry one — better than raising +RecordInvalid+ with an empty errors
|
|
126
|
+
# collection.
|
|
127
|
+
def propagate_step_failure!(order, result, fallback:)
|
|
128
|
+
record = result.value
|
|
129
|
+
if record.respond_to?(:errors) && record.errors.any?
|
|
130
|
+
record.errors.full_messages.each { |msg| order.errors.add(:base, msg) }
|
|
131
|
+
elsif result.error.to_s.present?
|
|
132
|
+
order.errors.add(:base, result.error.to_s)
|
|
133
|
+
else
|
|
134
|
+
order.errors.add(:base, fallback)
|
|
135
|
+
end
|
|
136
|
+
raise ActiveRecord::RecordInvalid, order
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def apply_coupon(order)
|
|
140
|
+
order.coupon_code = @params[:coupon_code]
|
|
141
|
+
handler = Spree::PromotionHandler::Coupon.new(order).apply
|
|
142
|
+
|
|
143
|
+
if handler.successful?
|
|
144
|
+
order.save!
|
|
145
|
+
else
|
|
146
|
+
@discount_application_errors << {
|
|
147
|
+
code: handler.status_code,
|
|
148
|
+
message: handler.error,
|
|
149
|
+
coupon_code: @params[:coupon_code]
|
|
150
|
+
}
|
|
151
|
+
order.coupon_code = nil
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Orders
|
|
3
|
+
# Admin-side order update.
|
|
4
|
+
#
|
|
5
|
+
# Updates Order attributes plus optional line items via a flat `items: [...]`
|
|
6
|
+
# array (matches POST shape and Store API convention). Standalone from
|
|
7
|
+
# Spree::Carts::Update (storefront).
|
|
8
|
+
class Update
|
|
9
|
+
prepend Spree::ServiceModule::Base
|
|
10
|
+
|
|
11
|
+
def call(order:, params: {})
|
|
12
|
+
@order = order
|
|
13
|
+
@params = params.to_h.deep_symbolize_keys
|
|
14
|
+
|
|
15
|
+
items_param = @params.delete(:items)
|
|
16
|
+
|
|
17
|
+
ApplicationRecord.transaction do
|
|
18
|
+
ship_address_id_before = @order.ship_address_id
|
|
19
|
+
|
|
20
|
+
if @order.update(@params)
|
|
21
|
+
process_items(items_param) if items_param
|
|
22
|
+
else
|
|
23
|
+
return failure(@order, @order.errors.full_messages.to_sentence)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if items_param || @order.ship_address_id != ship_address_id_before
|
|
27
|
+
build_shipments
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@order.update_with_updater!
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
success(@order.reload)
|
|
34
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
35
|
+
failure(e.record, e.record.errors.full_messages.to_sentence)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def process_items(items)
|
|
41
|
+
result = Spree::Orders::UpsertItems.call(order: @order, items: items)
|
|
42
|
+
raise ActiveRecord::RecordInvalid, @order if result.failure?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_shipments
|
|
46
|
+
result = Spree::Orders::BuildShipments.call(order: @order)
|
|
47
|
+
raise ActiveRecord::RecordInvalid, @order if result.failure?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Orders
|
|
3
|
+
# Bulk upsert line items on an order. Mirrors Spree::Carts::UpsertItems
|
|
4
|
+
# but is admin/order-side (separate from the cart pipeline per the 6.0
|
|
5
|
+
# cart/order split — see docs/plans/6.0-cart-order-split.md).
|
|
6
|
+
#
|
|
7
|
+
# For each entry in +items+:
|
|
8
|
+
# - If a line item for the variant already exists -> sets its quantity
|
|
9
|
+
# - If no line item exists -> creates one with the given quantity
|
|
10
|
+
#
|
|
11
|
+
# Order totals are NOT recalculated here. Callers (Spree::Orders::Create
|
|
12
|
+
# and Spree::Orders::Update) are responsible for running shipment
|
|
13
|
+
# rebuilding and a final `order.update_with_updater!` once their
|
|
14
|
+
# full pipeline (items, shipments, coupons) has run.
|
|
15
|
+
class UpsertItems
|
|
16
|
+
prepend Spree::ServiceModule::Base
|
|
17
|
+
|
|
18
|
+
def call(order:, items:)
|
|
19
|
+
items = Array(items)
|
|
20
|
+
return success(order) if items.empty?
|
|
21
|
+
|
|
22
|
+
store = order.store || Spree::Current.store
|
|
23
|
+
|
|
24
|
+
ApplicationRecord.transaction do
|
|
25
|
+
items.each do |item_params|
|
|
26
|
+
item_params = item_params.to_h.deep_symbolize_keys
|
|
27
|
+
variant = resolve_variant(store, item_params[:variant_id])
|
|
28
|
+
next unless variant
|
|
29
|
+
|
|
30
|
+
quantity = (item_params[:quantity] || 1).to_i
|
|
31
|
+
next if quantity <= 0
|
|
32
|
+
|
|
33
|
+
return failure(variant, "#{variant.name} is not available in #{order.currency}") if variant.amount_in(order.currency).nil?
|
|
34
|
+
|
|
35
|
+
line_item = Spree.line_item_by_variant_finder.new.execute(order: order, variant: variant)
|
|
36
|
+
|
|
37
|
+
if line_item
|
|
38
|
+
line_item.quantity = quantity
|
|
39
|
+
line_item.metadata = line_item.metadata.merge(item_params[:metadata].to_h) if item_params[:metadata].present?
|
|
40
|
+
else
|
|
41
|
+
line_item = order.line_items.new(quantity: quantity, variant: variant, options: { currency: order.currency })
|
|
42
|
+
line_item.metadata = item_params[:metadata].to_h if item_params[:metadata].present?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
return failure(line_item) unless line_item.save
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
success(order)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def resolve_variant(store, variant_id)
|
|
55
|
+
return nil if variant_id.blank?
|
|
56
|
+
|
|
57
|
+
variant = store.variants.find_by_param(variant_id)
|
|
58
|
+
|
|
59
|
+
raise ActiveRecord::RecordNotFound.new(
|
|
60
|
+
"Variant '#{variant_id}' not found in this store",
|
|
61
|
+
'Spree::Variant',
|
|
62
|
+
'id',
|
|
63
|
+
variant_id
|
|
64
|
+
) unless variant
|
|
65
|
+
|
|
66
|
+
variant
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|