spree_core 6.0.0.beta1 → 6.0.0.beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/jobs/spree/payments/handle_webhook_job.rb +1 -0
- data/app/jobs/spree/sample_data/load_job.rb +18 -0
- data/app/jobs/spree/seller_transfers/execute_pending_job.rb +1 -0
- data/app/mailers/spree/admin_user_mailer.rb +2 -2
- data/app/models/concerns/spree/acted_by.rb +210 -0
- data/app/models/concerns/spree/actor.rb +51 -0
- data/app/models/concerns/spree/admin_user_methods.rb +48 -17
- data/app/models/concerns/spree/customer_methods.rb +17 -1
- data/app/models/concerns/spree/has_number.rb +3 -2
- data/app/models/concerns/spree/prefixed_id.rb +4 -1
- data/app/models/concerns/spree/purchase/payment_processing.rb +1 -1
- data/app/models/concerns/spree/purchase/totals.rb +3 -1
- data/app/models/concerns/spree/ransackable_attributes.rb +38 -6
- data/app/models/concerns/spree/translatable_resource.rb +6 -7
- data/app/models/concerns/spree/webhook_payload_redaction.rb +132 -21
- data/app/models/spree/adjusters/promotion.rb +1 -1
- data/app/models/spree/api_key.rb +5 -3
- data/app/models/spree/authentication/lockout.rb +34 -0
- data/app/models/spree/authentication/strategies/email_password_strategy.rb +4 -12
- data/app/models/spree/authentication/strategies/oidc_strategy.rb +1 -1
- data/app/models/spree/catalog.rb +50 -13
- data/app/models/spree/claim.rb +3 -1
- data/app/models/spree/commission_line.rb +4 -2
- data/app/models/spree/commission_rule.rb +1 -1
- data/app/models/spree/coupon_code.rb +14 -2
- data/app/models/spree/current.rb +56 -13
- data/app/models/spree/exchange.rb +3 -1
- data/app/models/spree/export.rb +8 -5
- data/app/models/spree/exports/customers.rb +2 -2
- data/app/models/spree/fulfillment_group.rb +127 -0
- data/app/models/spree/gift_card.rb +9 -0
- data/app/models/spree/invitation.rb +18 -5
- data/app/models/spree/line_item.rb +1 -1
- data/app/models/spree/number_generators/sequential.rb +2 -2
- data/app/models/spree/option_value.rb +7 -0
- data/app/models/spree/order.rb +89 -18
- data/app/models/spree/order_group.rb +78 -6
- data/app/models/spree/payment_source.rb +1 -1
- data/app/models/spree/payment_split.rb +24 -0
- data/app/models/spree/price.rb +74 -0
- data/app/models/spree/pricing_provider/internal.rb +37 -15
- data/app/models/spree/product.rb +4 -0
- data/app/models/spree/product_option_type.rb +11 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +92 -30
- data/app/models/spree/promotion/rules/category.rb +14 -9
- data/app/models/spree/promotion/rules/product.rb +17 -1
- data/app/models/spree/promotion.rb +1 -1
- data/app/models/spree/promotion_action.rb +12 -1
- data/app/models/spree/promotion_handler/coupon.rb +50 -5
- data/app/models/spree/promotion_rule.rb +14 -0
- data/app/models/spree/refund.rb +2 -1
- data/app/models/spree/return.rb +18 -7
- data/app/models/spree/return_line_item.rb +19 -0
- data/app/models/spree/search_provider/database.rb +10 -2
- data/app/models/spree/seller_payout.rb +3 -1
- data/app/models/spree/seller_transfer.rb +3 -1
- data/app/models/spree/stock_receipt.rb +7 -2
- data/app/models/spree/store.rb +4 -0
- data/app/models/spree/store_credit.rb +4 -0
- data/app/models/spree/translations/batch.rb +4 -2
- data/app/models/spree/user_identity.rb +10 -6
- data/app/models/spree/variant.rb +8 -3
- data/app/models/spree/webhook_delivery.rb +28 -0
- data/app/models/spree/webhook_endpoint.rb +14 -0
- data/app/presenters/spree/csv/order_line_item_presenter.rb +2 -2
- data/app/services/spree/carts/split_by_seller.rb +22 -12
- data/app/services/spree/companies/add_member.rb +8 -2
- data/app/services/spree/imports/row_processors/price_list_price.rb +6 -0
- data/app/services/spree/imports/row_processors/product_translation.rb +1 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +17 -11
- data/app/services/spree/order_groups/allocate_payments.rb +3 -0
- data/app/services/spree/orders/approve.rb +3 -2
- data/app/services/spree/orders/build_fulfillments.rb +6 -0
- data/app/services/spree/orders/update_statuses.rb +8 -2
- data/app/services/spree/prices/bulk_upsert.rb +186 -47
- data/app/services/spree/sample_data/import_builder.rb +1 -1
- data/app/services/spree/sample_data/loader.rb +54 -40
- data/app/services/spree/seeds/all.rb +1 -26
- data/app/services/spree/seeds/allowed_origins.rb +5 -4
- data/app/services/spree/seeds/api_keys.rb +10 -9
- data/app/services/spree/seeds/channels.rb +9 -8
- data/app/services/spree/seeds/commission_rates.rb +21 -20
- data/app/services/spree/seeds/customer_groups.rb +5 -4
- data/app/services/spree/seeds/digital_delivery.rb +7 -8
- data/app/services/spree/seeds/payment_methods.rb +12 -11
- data/app/services/spree/seeds/product_types.rb +7 -8
- data/app/services/spree/seeds/returns_environment.rb +12 -11
- data/app/services/spree/seeds/roles.rb +8 -7
- data/app/services/spree/seeds/saved_reports.rb +19 -20
- data/app/services/spree/seeds/seller_requirements.rb +5 -4
- data/app/services/spree/seeds/store_resources.rb +41 -0
- data/app/services/spree/seeds/store_scoped.rb +15 -0
- data/app/services/spree/seeds/tax_categories.rb +10 -9
- data/app/services/spree/stock_locations/stock_items/create.rb +1 -1
- data/app/services/spree/stock_locations/stock_levels/create.rb +19 -3
- data/app/subscribers/spree/order_status_subscriber.rb +15 -7
- data/app/workflows/spree/carts/add_item.rb +5 -7
- data/app/workflows/spree/carts/complete.rb +20 -64
- data/app/workflows/spree/carts/recalculate_totals.rb +3 -1
- data/app/workflows/spree/claims/create.rb +31 -1
- data/app/workflows/spree/claims/resolve.rb +7 -11
- data/app/workflows/spree/exchanges/create.rb +11 -0
- data/app/workflows/spree/exchanges/fulfill.rb +21 -13
- data/app/workflows/spree/orders/cancel.rb +3 -2
- data/app/workflows/spree/orders/complete.rb +99 -2
- data/app/workflows/spree/price_lists/update.rb +17 -0
- data/app/workflows/spree/refunds/create.rb +1 -1
- data/app/workflows/spree/refunds/order_payments.rb +55 -1
- data/app/workflows/spree/returns/approve.rb +1 -1
- data/app/workflows/spree/returns/create.rb +11 -17
- data/app/workflows/spree/returns/refund.rb +16 -25
- data/app/workflows/spree/returns/returnable_quantity.rb +53 -0
- data/app/workflows/spree/sellers/invite.rb +1 -1
- data/config/locales/en.yml +8 -119
- data/db/migrate/20260916000001_add_email_delivery_flags_to_spree_order_groups.rb +14 -0
- data/db/migrate/20260916000001_order_operation_actors_polymorphic.rb +30 -0
- data/db/migrate/20260922000001_add_refunded_order_to_spree_store_credits.rb +5 -0
- data/db/migrate/20260923000001_change_spree_user_identities_tokens_to_text.rb +10 -0
- data/db/sample_data/channels.rb +1 -1
- data/db/sample_data/collections.rb +1 -1
- data/db/sample_data/custom_field_definitions.rb +1 -1
- data/db/sample_data/digital_products.rb +1 -1
- data/db/sample_data/fulfillment.rb +1 -1
- data/db/sample_data/options.rb +4 -2
- data/db/sample_data/orders.rb +9 -10
- data/db/sample_data/payment_methods.rb +18 -18
- data/db/sample_data/product_type_categories.rb +1 -1
- data/db/sample_data/product_types.rb +2 -2
- data/db/sample_data/promotions.rb +2 -4
- data/db/sample_data/wholesale.rb +1 -1
- data/lib/generators/spree/api_resource/api_resource_generator.rb +121 -6
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +8 -2
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +3 -0
- data/lib/generators/spree/api_resource/templates/permissions.en.yml.tt +6 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +2 -2
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +4 -1
- data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
- data/lib/generators/spree/model/model_generator.rb +40 -1
- data/lib/generators/spree/model/templates/create_table_migration.rb.tt +9 -0
- data/lib/generators/spree/model/templates/model.rb.tt +10 -4
- data/lib/generators/spree/subscriber/subscriber_generator.rb +3 -3
- data/lib/spree/core/configuration.rb +25 -25
- data/lib/spree/core/engine.rb +35 -8
- data/lib/spree/core/preferences/runtime_configuration.rb +76 -2
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +27 -0
- data/lib/spree/iso_data.rb +23 -4
- data/lib/spree/store_scope_guard.rb +16 -1
- data/lib/spree/testing_support/factories/invitation_factory.rb +1 -0
- data/lib/spree/testing_support/factories/order_group_factory.rb +49 -0
- data/lib/spree/testing_support/factories/payment_factory.rb +7 -2
- data/lib/spree/testing_support/factories/user_factory.rb +0 -1
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
- data/lib/tasks/backfill_actor_types.rake +35 -0
- data/lib/tasks/sample_data.rake +8 -2
- data/lib/tasks/store_settings.rake +1 -0
- data/lib/tasks/upgrade.rake +6 -21
- metadata +23 -9
- /data/{app/models → lib}/spree/checkout/default_requirements.rb +0 -0
- /data/{app/models → lib}/spree/checkout/registry.rb +0 -0
- /data/{app/models → lib}/spree/checkout/requirement.rb +0 -0
- /data/{app/models → lib}/spree/checkout/requirements.rb +0 -0
- /data/{app/models → lib}/spree/checkout/step.rb +0 -0
|
@@ -2,16 +2,11 @@ module Spree
|
|
|
2
2
|
module Seeds
|
|
3
3
|
class DigitalDelivery
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
|
-
|
|
6
|
-
def call
|
|
7
|
-
Spree::Store.all.find_each do |store|
|
|
8
|
-
create_for(store)
|
|
9
|
-
end
|
|
10
|
-
end
|
|
5
|
+
include StoreScoped
|
|
11
6
|
|
|
12
7
|
private
|
|
13
8
|
|
|
14
|
-
def
|
|
9
|
+
def seed(store)
|
|
15
10
|
# Digital goods are a distinct product set, so they get their own
|
|
16
11
|
# profile; assigning a product to it is what makes it digital.
|
|
17
12
|
profile = Spree::DeliveryProfiles::Digital.find_by(store: store) ||
|
|
@@ -22,7 +17,11 @@ module Spree
|
|
|
22
17
|
digital_delivery_method.delivery_profile = profile
|
|
23
18
|
digital_delivery_method.storefront_visible = true
|
|
24
19
|
digital_delivery_method.fulfillment_provider = 'Spree::FulfillmentProvider::Digital'
|
|
25
|
-
|
|
20
|
+
# Explicit, because the calculator's own default is the default
|
|
21
|
+
# store's currency rather than this store's.
|
|
22
|
+
digital_delivery_method.calculator ||= Spree::Calculator::Shipping::DigitalDelivery.create!(
|
|
23
|
+
preferred_currency: store.default_currency
|
|
24
|
+
)
|
|
26
25
|
digital_delivery_method.save!
|
|
27
26
|
end
|
|
28
27
|
end
|
|
@@ -2,19 +2,20 @@ module Spree
|
|
|
2
2
|
module Seeds
|
|
3
3
|
class PaymentMethods
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
|
+
include StoreScoped
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
Spree::Store.all.find_each do |store|
|
|
8
|
-
payment_method = store.payment_methods.find_or_initialize_by(
|
|
9
|
-
type: 'Spree::PaymentMethod::StoreCredit'
|
|
10
|
-
)
|
|
11
|
-
next if payment_method.persisted?
|
|
7
|
+
private
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
def seed(store)
|
|
10
|
+
payment_method = store.payment_methods.find_or_initialize_by(
|
|
11
|
+
type: 'Spree::PaymentMethod::StoreCredit'
|
|
12
|
+
)
|
|
13
|
+
return if payment_method.persisted?
|
|
14
|
+
|
|
15
|
+
payment_method.name = Spree.t(:store_credit_name)
|
|
16
|
+
payment_method.description = Spree.t(:store_credit_name)
|
|
17
|
+
payment_method.active = true
|
|
18
|
+
payment_method.save!
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
end
|
|
@@ -2,18 +2,17 @@ module Spree
|
|
|
2
2
|
module Seeds
|
|
3
3
|
class ProductTypes
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
|
+
include StoreScoped
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def seed(store)
|
|
10
|
+
Spree::ProductType.where(store: store).find_or_create_by!(name: I18n.t('spree.seed.product_types.default'))
|
|
11
|
+
Spree::ProductType.where(store: store).find_or_create_by!(name: I18n.t('spree.seed.product_types.digital')) do |product_type|
|
|
12
|
+
product_type.delivery_profile = digital_profile_for(store)
|
|
12
13
|
end
|
|
13
14
|
end
|
|
14
15
|
|
|
15
|
-
private
|
|
16
|
-
|
|
17
16
|
def digital_profile_for(store)
|
|
18
17
|
Spree::DeliveryProfiles::Digital.find_by(store: store) ||
|
|
19
18
|
Spree::DeliveryProfiles::Digital.create!(store: store, name: I18n.t('spree.seed.delivery_profiles.digital'))
|
|
@@ -2,6 +2,7 @@ module Spree
|
|
|
2
2
|
module Seeds
|
|
3
3
|
class ReturnsEnvironment
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
|
+
include StoreScoped
|
|
5
6
|
|
|
6
7
|
RETURN_REASONS = [
|
|
7
8
|
'Better price available',
|
|
@@ -37,18 +38,18 @@ module Spree
|
|
|
37
38
|
'Item not as described'
|
|
38
39
|
].freeze
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
# Every vocabulary is store-owned, so every store gets its own.
|
|
42
|
-
Spree::Store.all.find_each do |store|
|
|
43
|
-
# Through the model helper, not a literal — the workflows look this
|
|
44
|
-
# row up by the same constant, so a hard-coded string here would
|
|
45
|
-
# silently create a second, unfindable reason if the name ever moves.
|
|
46
|
-
Spree::RefundReason.return_processing_reason(store)
|
|
41
|
+
private
|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
# Every vocabulary is store-owned, so every store gets its own.
|
|
44
|
+
def seed(store)
|
|
45
|
+
# Through the model helper, not a literal — the workflows look this
|
|
46
|
+
# row up by the same constant, so a hard-coded string here would
|
|
47
|
+
# silently create a second, unfindable reason if the name ever moves.
|
|
48
|
+
Spree::RefundReason.return_processing_reason(store)
|
|
49
|
+
|
|
50
|
+
RETURN_REASONS.each { |name| Spree::ReturnReason.where(store: store).find_or_create_by!(name: name) }
|
|
51
|
+
CLAIM_REASONS.each { |name| Spree::ClaimReason.where(store: store).find_or_create_by!(name: name) }
|
|
52
|
+
ORDER_CANCELLATION_REASONS.each { |name| Spree::OrderCancellationReason.where(store: store).find_or_create_by!(name: name) }
|
|
52
53
|
end
|
|
53
54
|
end
|
|
54
55
|
end
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Seeds
|
|
3
|
+
# A role belongs to what it governs, so every store gets its own
|
|
4
|
+
# immutable admin.
|
|
3
5
|
class Roles
|
|
4
6
|
prepend Spree::ServiceModule::Base
|
|
7
|
+
include StoreScoped
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def
|
|
9
|
-
Spree::
|
|
10
|
-
|
|
11
|
-
admin.update_column(:mutable, false) if admin.read_attribute(:mutable)
|
|
12
|
-
end
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def seed(store)
|
|
12
|
+
admin = Spree::Role.default_admin_role(store)
|
|
13
|
+
admin.update_column(:mutable, false) if admin.read_attribute(:mutable)
|
|
13
14
|
end
|
|
14
15
|
end
|
|
15
16
|
end
|
|
@@ -6,6 +6,7 @@ module Spree
|
|
|
6
6
|
# only never-created ones are added.
|
|
7
7
|
class SavedReports
|
|
8
8
|
prepend Spree::ServiceModule::Base
|
|
9
|
+
include StoreScoped
|
|
9
10
|
|
|
10
11
|
REPORTS = [
|
|
11
12
|
{ key: 'sales_over_time',
|
|
@@ -70,31 +71,29 @@ module Spree
|
|
|
70
71
|
'time_range' => { 'preset' => 'last_4_weeks' } } }
|
|
71
72
|
].freeze
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def seed(store)
|
|
77
|
+
# Compared case-insensitively like the model's uniqueness rule, so a
|
|
78
|
+
# merchant's own "top products" never makes a re-seed raise.
|
|
79
|
+
existing = store.saved_reports.pluck(:name).map(&:downcase).to_set
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
REPORTS.each do |report|
|
|
82
|
+
# Every locale's name for this report, not just the current one:
|
|
83
|
+
# the name is translated, so a store seeded in one language and
|
|
84
|
+
# re-seeded in another would otherwise recognise none of its own
|
|
85
|
+
# built-ins and create a second full set.
|
|
86
|
+
next if known_names(report[:key]).intersect?(existing)
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
end
|
|
88
|
+
store.saved_reports.create!(
|
|
89
|
+
name: Spree.t("reporting.seeds.#{report[:key]}.name"),
|
|
90
|
+
description: Spree.t("reporting.seeds.#{report[:key]}.description"),
|
|
91
|
+
query: report[:query],
|
|
92
|
+
seeded: true
|
|
93
|
+
)
|
|
93
94
|
end
|
|
94
95
|
end
|
|
95
96
|
|
|
96
|
-
private
|
|
97
|
-
|
|
98
97
|
# The downcased name this report carries in every locale core ships.
|
|
99
98
|
def known_names(key)
|
|
100
99
|
@known_names ||= {}
|
|
@@ -8,11 +8,12 @@ module Spree
|
|
|
8
8
|
# a licence) is theirs to add, and there is no sensible default for it.
|
|
9
9
|
class SellerRequirements
|
|
10
10
|
prepend Spree::ServiceModule::Base
|
|
11
|
+
include StoreScoped
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def seed(store)
|
|
16
|
+
Spree::SellerRequirement.provision_defaults(store)
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Seeds
|
|
3
|
+
# Seeds what a single store needs to trade, without touching any other
|
|
4
|
+
# store.
|
|
5
|
+
#
|
|
6
|
+
# Country-shaped defaults (market, warehouse, delivery zones, pickup) are
|
|
7
|
+
# not part of it: the caller runs Spree::Stores::ProvisionDefaults
|
|
8
|
+
# afterwards with the merchant's answers.
|
|
9
|
+
class StoreResources
|
|
10
|
+
prepend Spree::ServiceModule::Base
|
|
11
|
+
|
|
12
|
+
# @param store [Spree::Store] the store to seed, already persisted
|
|
13
|
+
# @return [Spree::ServiceModule::Result]
|
|
14
|
+
def call(store:)
|
|
15
|
+
# A nil store would fall through to every seed's all-stores mode.
|
|
16
|
+
raise ArgumentError, 'store is required' if store.nil?
|
|
17
|
+
|
|
18
|
+
Spree::Events.disable do
|
|
19
|
+
ActiveRecord::Base.no_touching do
|
|
20
|
+
TaxCategories.call(store: store)
|
|
21
|
+
Channels.call(store: store)
|
|
22
|
+
Roles.call(store: store)
|
|
23
|
+
DigitalDelivery.call(store: store)
|
|
24
|
+
PaymentMethods.call(store: store)
|
|
25
|
+
ProductTypes.call(store: store)
|
|
26
|
+
CustomerGroups.call(store: store)
|
|
27
|
+
ReturnsEnvironment.call(store: store)
|
|
28
|
+
# The marketplace's catch-all rate, at the bottom of the list so
|
|
29
|
+
# narrower rates added later resolve ahead of it.
|
|
30
|
+
CommissionRates.call(store: store)
|
|
31
|
+
SellerRequirements.call(store: store)
|
|
32
|
+
# Binds to the wholesale channel that Channels creates above.
|
|
33
|
+
ApiKeys.call(store: store)
|
|
34
|
+
SavedReports.call(store: store)
|
|
35
|
+
AllowedOrigins.call(store: store)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Seeds
|
|
3
|
+
# Shared entry point for seeds whose rows belong to a store. Including
|
|
4
|
+
# classes implement a private `seed(store)` that seeds exactly one store
|
|
5
|
+
# and is safe to re-run.
|
|
6
|
+
module StoreScoped
|
|
7
|
+
# @param store [Spree::Store, nil] the store to seed; every store when nil
|
|
8
|
+
def call(store: nil)
|
|
9
|
+
return seed(store) if store
|
|
10
|
+
|
|
11
|
+
Spree::Store.find_each { |each_store| seed(each_store) }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -2,17 +2,18 @@ module Spree
|
|
|
2
2
|
module Seeds
|
|
3
3
|
class TaxCategories
|
|
4
4
|
prepend Spree::ServiceModule::Base
|
|
5
|
+
include StoreScoped
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
Spree::Store.find_each do |store|
|
|
8
|
-
# The block form only runs on create, so a store that already had a
|
|
9
|
-
# "Default" category with the flag unset kept no default at all — and
|
|
10
|
-
# the default is what taxes an item carrying no category of its own.
|
|
11
|
-
default = store.tax_categories.find_or_create_by!(name: 'Default')
|
|
12
|
-
default.update!(is_default: true) unless default.is_default?
|
|
7
|
+
private
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
def seed(store)
|
|
10
|
+
# The block form only runs on create, so a store that already had a
|
|
11
|
+
# "Default" category with the flag unset kept no default at all — and
|
|
12
|
+
# the default is what taxes an item carrying no category of its own.
|
|
13
|
+
default = store.tax_categories.find_or_create_by!(name: 'Default')
|
|
14
|
+
default.update!(is_default: true) unless default.is_default?
|
|
15
|
+
|
|
16
|
+
store.tax_categories.find_or_create_by!(name: 'Non-taxable')
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -10,7 +10,7 @@ module Spree
|
|
|
10
10
|
# @param stock_location [Spree::StockLocation]
|
|
11
11
|
# @param variants_scope [ActiveRecord::Relation]
|
|
12
12
|
# @return [Spree::ServiceModule::Base::Result]
|
|
13
|
-
def call(stock_location:, variants_scope:
|
|
13
|
+
def call(stock_location:, variants_scope: nil)
|
|
14
14
|
Spree::Deprecation.warn('Spree::StockLocations::StockItems::Create is deprecated and will be removed in Spree 6.1. Use Spree::StockLocations::StockLevels::Create instead.')
|
|
15
15
|
Spree::StockLocations::StockLevels::Create.call(stock_location: stock_location, variants_scope: variants_scope)
|
|
16
16
|
end
|
|
@@ -4,8 +4,13 @@ module Spree
|
|
|
4
4
|
class Create
|
|
5
5
|
prepend Spree::ServiceModule::Base
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# @param stock_location [Spree::StockLocation]
|
|
8
|
+
# @param variants_scope [ActiveRecord::Relation, nil] defaults to what the
|
|
9
|
+
# location may stock; always narrowed to it, so no caller can seed rows
|
|
10
|
+
# for another store's or another seller's variants
|
|
11
|
+
def call(stock_location:, variants_scope: nil)
|
|
12
|
+
variant_ids = propagatable_variants(stock_location, variants_scope).ids
|
|
13
|
+
prepared_stock_levels = variant_ids.map do |variant_id|
|
|
9
14
|
Hash[
|
|
10
15
|
'stock_location_id', stock_location.id,
|
|
11
16
|
'variant_id', variant_id,
|
|
@@ -16,9 +21,20 @@ module Spree
|
|
|
16
21
|
end
|
|
17
22
|
if prepared_stock_levels.any?
|
|
18
23
|
stock_location.stock_levels.insert_all(prepared_stock_levels)
|
|
19
|
-
|
|
24
|
+
# By id: MySQL refuses an UPDATE whose own table appears in a subquery.
|
|
25
|
+
Spree::Variant.where(id: variant_ids).touch_all
|
|
20
26
|
end
|
|
21
27
|
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# A location stocks its own store's variants, and a seller's location
|
|
32
|
+
# only the variants that seller sells.
|
|
33
|
+
def propagatable_variants(stock_location, variants_scope)
|
|
34
|
+
scope = Spree::Variant.where(id: (variants_scope || Spree::Variant).select(:id))
|
|
35
|
+
scope = scope.for_seller(stock_location.seller_id) if stock_location.seller_id.present?
|
|
36
|
+
scope.joins(:product).where(Spree::Product.table_name => { store_id: stock_location.store_id })
|
|
37
|
+
end
|
|
22
38
|
end
|
|
23
39
|
end
|
|
24
40
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
# The trigger side of derive-then-persist statuses: any payment, refund,
|
|
3
|
-
# fulfillment or return change recomputes the owning order's
|
|
3
|
+
# store credit, fulfillment or return change recomputes the owning order's
|
|
4
4
|
# payment_status / fulfillment_status through the single writer
|
|
5
5
|
# (Spree::Orders::UpdateStatuses). Synchronous so API responses right
|
|
6
6
|
# after a capture/void/fulfill already carry the fresh status. Cart-owned
|
|
@@ -15,12 +15,15 @@ module Spree
|
|
|
15
15
|
subscribes_to 'payment.created', 'payment.updated', 'payment.deleted',
|
|
16
16
|
'payment.completed', 'payment.captured', 'payment.voided',
|
|
17
17
|
'refund.created', 'refund.updated',
|
|
18
|
+
'store_credit.created', 'store_credit.updated', 'store_credit.deleted',
|
|
18
19
|
'fulfillment.created', 'fulfillment.updated', 'fulfillment.deleted',
|
|
19
20
|
'return.received', 'return.refunded', 'return.canceled',
|
|
20
21
|
async: false
|
|
21
22
|
|
|
22
23
|
# Events whose payload changes what the customer has actually paid, so
|
|
23
24
|
# payment_total has to be re-summed before statuses derive from it.
|
|
25
|
+
# Store credit is not one of them: the money stayed with the store, and
|
|
26
|
+
# payment_total is what came in through payments.
|
|
24
27
|
MONEY_EVENTS = %w[
|
|
25
28
|
payment.created payment.updated payment.deleted payment.completed
|
|
26
29
|
payment.voided refund.created refund.updated
|
|
@@ -63,19 +66,24 @@ module Spree
|
|
|
63
66
|
resource_class = {
|
|
64
67
|
'payment' => Spree::Payment,
|
|
65
68
|
'refund' => Spree::Refund,
|
|
69
|
+
'store_credit' => Spree::StoreCredit,
|
|
66
70
|
'fulfillment' => Spree::Fulfillment,
|
|
67
71
|
'return' => Spree::Return
|
|
68
72
|
}[event.resource_type]
|
|
69
73
|
return if resource_class.nil?
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
# A paranoid model's .deleted event leaves the row in place but out of
|
|
76
|
+
# default scope, and a credit that has been withdrawn is exactly the
|
|
77
|
+
# kind of change the order has to learn about.
|
|
78
|
+
record = (resource_class.try(:with_deleted) || resource_class).
|
|
79
|
+
find_by_prefix_id(event.payload['id'])
|
|
80
|
+
# Neither Payment nor Fulfillment is paranoid, so their .deleted events
|
|
81
|
+
# arrive with the row already gone and a payload carrying no owner —
|
|
82
|
+
# nothing can lead back to the order. The models whose deletion changes
|
|
83
|
+
# order money recalculate from their own after_destroy instead.
|
|
76
84
|
return if record.nil?
|
|
77
85
|
|
|
78
|
-
record.try(:owner) || record.try(:order) ||
|
|
86
|
+
record.try(:owner) || record.try(:order) || record.try(:refunded_order) ||
|
|
79
87
|
record.try(:payment)&.try(:owner)
|
|
80
88
|
end
|
|
81
89
|
end
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Carts
|
|
3
3
|
class AddItem < Spree::Workflow
|
|
4
|
-
# Line item attributes a caller may set through `options`.
|
|
5
|
-
ITEM_OPTIONS = [:id, :variant_id, :quantity].freeze
|
|
6
|
-
|
|
7
4
|
hooks :validate, :after_item_added
|
|
8
5
|
|
|
9
6
|
# Hook handlers read these plus the argument readers. Both are nil
|
|
@@ -110,10 +107,11 @@ module Spree
|
|
|
110
107
|
|
|
111
108
|
if @line_item.nil?
|
|
112
109
|
# `LineItem#options=` mass-assigns, so the caller's hash is narrowed
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
# to the attributes extensions declare. The variant and quantity are
|
|
111
|
+
# never among them: both were checked and priced above, and an
|
|
112
|
+
# `options` value replacing them would sell something else.
|
|
113
|
+
writable = ::Spree::LineItem.additional_permitted_attributes.flat_map { |a| a.is_a?(Hash) ? a.keys : a }.
|
|
114
|
+
map(&:to_sym) - [:id, :variant_id, :quantity]
|
|
117
115
|
|
|
118
116
|
opts = item_options.symbolize_keys.slice(*writable).
|
|
119
117
|
merge(currency: cart.currency).
|
|
@@ -207,6 +207,13 @@ module Spree
|
|
|
207
207
|
# typed money lines re-pointed, fulfillments + selected rates, address
|
|
208
208
|
# copies); money records (payments, sessions, reservations, coupon
|
|
209
209
|
# codes) re-point — external transaction references must never fork.
|
|
210
|
+
#
|
|
211
|
+
# The cart's metadata is copied whole: storefronts and checkout
|
|
212
|
+
# requirements keep what they collected about the purchase there, and it
|
|
213
|
+
# has to outlive the cart. The order is new at this point, so there is
|
|
214
|
+
# nothing on it to merge with — the cart's hash simply becomes the
|
|
215
|
+
# order's. A deep copy, so a later edit to either record can never reach
|
|
216
|
+
# the other through a shared nested hash.
|
|
210
217
|
def create_draft_order!(cart)
|
|
211
218
|
order = nil
|
|
212
219
|
ApplicationRecord.transaction do
|
|
@@ -228,6 +235,7 @@ module Spree
|
|
|
228
235
|
po_number: cart.po_number,
|
|
229
236
|
gift_card: cart.gift_card,
|
|
230
237
|
last_ip_address: cart.last_ip_address,
|
|
238
|
+
metadata: cart.metadata.to_h.deep_dup,
|
|
231
239
|
ship_address: cart.ship_address&.snapshot,
|
|
232
240
|
bill_address: cart.bill_address&.snapshot
|
|
233
241
|
)
|
|
@@ -407,82 +415,30 @@ module Spree
|
|
|
407
415
|
# resumes like any other finalize failure.
|
|
408
416
|
def finalize!(cart, order)
|
|
409
417
|
@order = order
|
|
410
|
-
step :split_by_seller
|
|
411
|
-
step :allocate_payment_splits
|
|
412
418
|
step :complete_orders
|
|
413
419
|
step :complete_cart
|
|
414
420
|
step :mark_coupon_codes_used
|
|
415
421
|
external_step :commit_tax
|
|
416
422
|
end
|
|
417
423
|
|
|
418
|
-
#
|
|
419
|
-
#
|
|
420
|
-
#
|
|
421
|
-
#
|
|
424
|
+
# Places what the checkout produced, through the order-side workflow
|
|
425
|
+
# that owns placement — including dividing the basket between sellers,
|
|
426
|
+
# which an order raised from the admin needs just as much and which only
|
|
427
|
+
# that workflow can give both of them.
|
|
422
428
|
#
|
|
423
|
-
#
|
|
424
|
-
#
|
|
425
|
-
# the
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
@order_group = cart.order_group
|
|
430
|
-
return if order_group.present?
|
|
431
|
-
return if order.placed?
|
|
432
|
-
|
|
433
|
-
partitions = Spree::Carts::PartitionBySeller.call(purchase: order).value
|
|
434
|
-
|
|
435
|
-
if partitions.one?
|
|
436
|
-
# Nothing to divide, but the sale still belongs to whoever made it:
|
|
437
|
-
# a basket entirely from one seller is that seller's order, and the
|
|
438
|
-
# column is what their own order list reads.
|
|
439
|
-
order.update_columns(seller_id: partitions.first.seller_id) if order.seller_id != partitions.first.seller_id
|
|
440
|
-
return
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
return if partitions.empty?
|
|
429
|
+
# It answers with the group when the basket divided, and the first of
|
|
430
|
+
# its children is the order this checkout carries on with. No child holds
|
|
431
|
+
# the confirmation: the customer is confirmed from the group.
|
|
432
|
+
def complete_orders
|
|
433
|
+
result = Spree.order_complete_workflow.call(order: order, payment_pending: payment_pending)
|
|
434
|
+
failure(cart, code: 'completion_failed', message: result.error) if result.failure?
|
|
444
435
|
|
|
445
|
-
|
|
446
|
-
failure(cart, code: 'split_failed', message: result.error) if result.failure?
|
|
436
|
+
return unless result.value.is_a?(Spree::OrderGroup)
|
|
447
437
|
|
|
448
438
|
@order_group = result.value
|
|
449
|
-
# Ordered, because which child comes first decides which one carries
|
|
450
|
-
# the confirmation email — that must not depend on how the rows come
|
|
451
|
-
# back.
|
|
452
439
|
@order = order_group.orders.order(:id).first
|
|
453
440
|
end
|
|
454
441
|
|
|
455
|
-
def allocate_payment_splits
|
|
456
|
-
return if order_group.nil?
|
|
457
|
-
|
|
458
|
-
result = Spree::OrderGroups::AllocatePayments.call(group: order_group)
|
|
459
|
-
failure(cart, code: 'split_failed', message: result.error) if result.failure?
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
# Places what the checkout produced — the children of a split, or the one
|
|
463
|
-
# order otherwise. Each child publishes its own order.placed, which is
|
|
464
|
-
# what gets every seller their own commission lines with no marketplace
|
|
465
|
-
# code in the commission engine.
|
|
466
|
-
#
|
|
467
|
-
# A split checkout's payments were already taken against the whole basket
|
|
468
|
-
# and moved onto the group, so the children place without processing them
|
|
469
|
-
# again. One purchase means one confirmation email: the first child
|
|
470
|
-
# carries the notification and its siblings place silently.
|
|
471
|
-
def complete_orders
|
|
472
|
-
split = order_group.present?
|
|
473
|
-
|
|
474
|
-
placed_orders.each_with_index do |child, index|
|
|
475
|
-
result = Spree.order_complete_workflow.call(
|
|
476
|
-
order: child,
|
|
477
|
-
payment_pending: split,
|
|
478
|
-
notify_customer: split && !index.zero? ? false : nil
|
|
479
|
-
)
|
|
480
|
-
failure(cart, code: 'completion_failed', message: result.error) if result.failure?
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
order_group&.publish_event('order_group.completed')
|
|
484
|
-
end
|
|
485
|
-
|
|
486
442
|
# Tells the tax engine the sale is final. A no-op for the built-in
|
|
487
443
|
# provider, whose rows are already the record; external engines file the
|
|
488
444
|
# document that a tax return is later built from. Outside the
|
|
@@ -499,7 +455,7 @@ module Spree
|
|
|
499
455
|
# otherwise. The single answer to "which orders came out of here", so
|
|
500
456
|
# placement and tax filing can never disagree about the set.
|
|
501
457
|
def placed_orders
|
|
502
|
-
order_group.present? ? order_group.orders.to_a : [order]
|
|
458
|
+
order_group.present? ? order_group.orders.to_a.sort_by(&:id) : [order]
|
|
503
459
|
end
|
|
504
460
|
|
|
505
461
|
def complete_cart
|
|
@@ -137,7 +137,9 @@ module Spree
|
|
|
137
137
|
# @return [BigDecimal]
|
|
138
138
|
def paid_so_far
|
|
139
139
|
if cart.is_a?(Spree::Order) && cart.grouped?
|
|
140
|
-
|
|
140
|
+
# Reloaded: a share moved since this instance loaded them would be
|
|
141
|
+
# re-summed at its old figure and persisted back over the correction.
|
|
142
|
+
return cart.payment_splits.reload.to_a.sum(&:net_captured_amount)
|
|
141
143
|
end
|
|
142
144
|
|
|
143
145
|
cart.payments.completed.includes(:refunds).inject(0) do |sum, payment|
|
|
@@ -39,6 +39,9 @@ module Spree
|
|
|
39
39
|
failure(order, :no_items_to_claim) if items.blank?
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Bounded by what earlier claims left: each claim can pay out what was
|
|
43
|
+
# paid for its units, so claiming the same units again would pay twice.
|
|
44
|
+
# A denied or canceled claim settled nothing and frees its units.
|
|
42
45
|
def normalize_items
|
|
43
46
|
@normalized_items = items.map do |item|
|
|
44
47
|
line_item = item[:line_item]
|
|
@@ -46,13 +49,40 @@ module Spree
|
|
|
46
49
|
|
|
47
50
|
failure(order, :invalid_quantity) unless quantity.positive?
|
|
48
51
|
failure(order, :item_not_on_order) unless line_item&.order_id == order.id
|
|
49
|
-
failure(order, :invalid_quantity) if quantity > line_item.quantity.to_i
|
|
50
52
|
|
|
51
53
|
item.merge(line_item: line_item, quantity: quantity)
|
|
52
54
|
end
|
|
55
|
+
|
|
56
|
+
ensure_claimable_quantities
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ensure_claimable_quantities
|
|
60
|
+
requested = Hash.new(0)
|
|
61
|
+
|
|
62
|
+
@normalized_items.each do |item|
|
|
63
|
+
line_item = item[:line_item]
|
|
64
|
+
failure(order, :invalid_quantity) if item[:quantity] > claimable_quantity_for(line_item) - requested[line_item.id]
|
|
65
|
+
|
|
66
|
+
requested[line_item.id] += item[:quantity]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def claimable_quantity_for(line_item)
|
|
71
|
+
claimed = Spree::ClaimLineItem.
|
|
72
|
+
joins(:claim).
|
|
73
|
+
where(line_item_id: line_item.id).
|
|
74
|
+
where.not(Spree::Claim.table_name => { status: %w[denied canceled] }).
|
|
75
|
+
sum(:quantity)
|
|
76
|
+
|
|
77
|
+
line_item.quantity.to_i - claimed
|
|
53
78
|
end
|
|
54
79
|
|
|
55
80
|
def build_claim
|
|
81
|
+
# Again under the order's row lock, so two requests racing for the same
|
|
82
|
+
# units cannot both pass the check above.
|
|
83
|
+
Spree::Order.lock.find(order.id)
|
|
84
|
+
ensure_claimable_quantities
|
|
85
|
+
|
|
56
86
|
@claim = order.claims.new(
|
|
57
87
|
store: order.store,
|
|
58
88
|
reason: reason,
|