spree_core 6.0.0.beta1 → 6.0.0.beta2
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/sample_data/load_job.rb +13 -0
- 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/purchase/payment_processing.rb +1 -1
- data/app/models/spree/api_key.rb +5 -3
- data/app/models/spree/catalog.rb +50 -13
- data/app/models/spree/claim.rb +2 -1
- data/app/models/spree/current.rb +41 -13
- data/app/models/spree/exchange.rb +2 -1
- data/app/models/spree/line_item.rb +1 -1
- data/app/models/spree/order.rb +54 -6
- data/app/models/spree/pricing_provider/internal.rb +37 -15
- data/app/models/spree/refund.rb +2 -1
- data/app/models/spree/return.rb +2 -1
- data/app/models/spree/stock_receipt.rb +7 -2
- data/app/models/spree/store.rb +4 -0
- data/app/services/spree/orders/approve.rb +3 -2
- data/app/workflows/spree/orders/cancel.rb +3 -2
- data/app/workflows/spree/refunds/create.rb +1 -1
- data/app/workflows/spree/returns/approve.rb +1 -1
- data/app/workflows/spree/returns/create.rb +2 -2
- data/app/workflows/spree/returns/refund.rb +1 -1
- data/config/locales/en.yml +1 -0
- data/db/migrate/20260916000001_order_operation_actors_polymorphic.rb +30 -0
- data/lib/generators/spree/api_resource/api_resource_generator.rb +63 -5
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +5 -0
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +3 -0
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +4 -1
- 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/spree/core/configuration.rb +25 -25
- data/lib/spree/core/engine.rb +27 -0
- 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/testing_support/factories/user_factory.rb +0 -1
- data/lib/tasks/backfill_actor_types.rake +35 -0
- data/lib/tasks/store_settings.rake +1 -0
- metadata +14 -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
|
@@ -17,7 +17,7 @@ module Spree
|
|
|
17
17
|
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant'
|
|
18
18
|
end
|
|
19
19
|
belongs_to :tax_category, -> { with_deleted }, class_name: 'Spree::TaxCategory', optional: true
|
|
20
|
-
belongs_to :price_list, class_name: 'Spree::PriceList', optional: true
|
|
20
|
+
belongs_to :price_list, -> { with_deleted }, class_name: 'Spree::PriceList', optional: true
|
|
21
21
|
# Snapshotted from the variant when the line is added — see copy_seller.
|
|
22
22
|
# Nil is the operator's own first-party item.
|
|
23
23
|
belongs_to :seller, class_name: 'Spree::Seller', optional: true
|
data/app/models/spree/order.rb
CHANGED
|
@@ -23,6 +23,7 @@ module Spree
|
|
|
23
23
|
|
|
24
24
|
extend Spree::DisplayMoney
|
|
25
25
|
|
|
26
|
+
include Spree::ActedBy
|
|
26
27
|
include Spree::SingleStoreResource
|
|
27
28
|
include Spree::SanitizableRichText
|
|
28
29
|
include Spree::Purchase::Channel
|
|
@@ -91,6 +92,54 @@ module Spree
|
|
|
91
92
|
order
|
|
92
93
|
end
|
|
93
94
|
|
|
95
|
+
# Checkout-step introspection belongs to Spree::Cart
|
|
96
|
+
# ({Spree::Purchase::CheckoutSteps}) since 6.0 — an order is past checkout
|
|
97
|
+
# by definition. These bridges answer for an upgrader reaching for the
|
|
98
|
+
# Spree 5 API on an order, rather than raising NoMethodError.
|
|
99
|
+
#
|
|
100
|
+
# The step list is data-driven, so it stays accurate here: the registry
|
|
101
|
+
# computes it from the record's own predicates, all of which Order carries.
|
|
102
|
+
# Where the checkout stands within that list is not, which is why the
|
|
103
|
+
# readers below report a finished checkout.
|
|
104
|
+
|
|
105
|
+
# @deprecated Checkout steps belong to Spree::Cart; removed in 6.1.
|
|
106
|
+
def checkout_steps
|
|
107
|
+
Spree::Deprecation.warn('Spree::Order#checkout_steps is deprecated and will be removed in Spree 6.1. Checkout steps belong to Spree::Cart — ask the cart before it is completed.')
|
|
108
|
+
Spree::Checkout::Registry.step_names_for(self)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @deprecated See {#checkout_steps}; removed in 6.1.
|
|
112
|
+
def has_checkout_step?(step)
|
|
113
|
+
Spree::Deprecation.warn('Spree::Order#has_checkout_step? is deprecated and will be removed in Spree 6.1. Checkout steps belong to Spree::Cart — ask the cart before it is completed.')
|
|
114
|
+
step.present? && Spree::Checkout::Registry.step_names_for(self).include?(step.to_s)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @deprecated See {#checkout_steps}; removed in 6.1.
|
|
118
|
+
def checkout_step_index(step)
|
|
119
|
+
Spree::Deprecation.warn('Spree::Order#checkout_step_index is deprecated and will be removed in Spree 6.1. Checkout steps belong to Spree::Cart — ask the cart before it is completed.')
|
|
120
|
+
Spree::Checkout::Registry.step_names_for(self).index(step).to_i
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @deprecated See {#checkout_steps}; removed in 6.1. An order has no
|
|
124
|
+
# outstanding step, so this is always 'complete'.
|
|
125
|
+
def current_checkout_step
|
|
126
|
+
Spree::Deprecation.warn("Spree::Order#current_checkout_step is deprecated and will be removed in Spree 6.1. An order is past checkout, so this is always 'complete'.")
|
|
127
|
+
'complete'
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @deprecated See {#checkout_steps}; removed in 6.1.
|
|
131
|
+
def final_checkout_step
|
|
132
|
+
Spree::Deprecation.warn('Spree::Order#final_checkout_step is deprecated and will be removed in Spree 6.1. Checkout steps belong to Spree::Cart — ask the cart before it is completed.')
|
|
133
|
+
Spree::Checkout::Registry.step_names_for(self).reject { |step| step == 'complete' }.last || 'address'
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @deprecated See {#checkout_steps}; removed in 6.1. Every step is behind
|
|
137
|
+
# an order, so this is the whole list bar 'complete'.
|
|
138
|
+
def completed_checkout_steps
|
|
139
|
+
Spree::Deprecation.warn("Spree::Order#completed_checkout_steps is deprecated and will be removed in Spree 6.1. An order is past checkout, so every step bar 'complete' is behind it.")
|
|
140
|
+
Spree::Checkout::Registry.step_names_for(self).reject { |step| step == 'complete' }
|
|
141
|
+
end
|
|
142
|
+
|
|
94
143
|
# Standardized column names (renamed in 6.0); legacy readers stay as
|
|
95
144
|
# aliases one release.
|
|
96
145
|
alias_attribute :promo_total, :discount_total
|
|
@@ -169,9 +218,7 @@ module Spree
|
|
|
169
218
|
# Whose sale this is. Nil on the operator's own goods, including the
|
|
170
219
|
# first-party child of a mixed marketplace checkout.
|
|
171
220
|
belongs_to :seller, class_name: 'Spree::Seller', optional: true
|
|
172
|
-
|
|
173
|
-
belongs_to :approver, class_name: "::#{Spree.admin_user_class}", optional: true
|
|
174
|
-
belongs_to :canceler, class_name: "::#{Spree.admin_user_class}", optional: true
|
|
221
|
+
acted_by :created_by, :approver, :canceler
|
|
175
222
|
belongs_to :cancel_reason, class_name: 'Spree::OrderCancellationReason', optional: true, inverse_of: :orders
|
|
176
223
|
|
|
177
224
|
belongs_to :preferred_stock_location, class_name: 'Spree::StockLocation', optional: true
|
|
@@ -1089,10 +1136,11 @@ module Spree
|
|
|
1089
1136
|
# Approves the order and records the approver.
|
|
1090
1137
|
# Delegates to {Spree::Orders::Approve} service.
|
|
1091
1138
|
#
|
|
1092
|
-
# @param
|
|
1139
|
+
# @param actor [Object, nil] who approved it — an admin user or an API
|
|
1140
|
+
# key (see Spree.actor_classes)
|
|
1093
1141
|
# @return [Spree::ServiceModule::Result]
|
|
1094
|
-
def approved_by(
|
|
1095
|
-
Spree.order_approve_service.call(order: self, approver:
|
|
1142
|
+
def approved_by(actor = nil)
|
|
1143
|
+
Spree.order_approve_service.call(order: self, approver: actor)
|
|
1096
1144
|
end
|
|
1097
1145
|
|
|
1098
1146
|
def approved?
|
|
@@ -45,7 +45,12 @@ module Spree
|
|
|
45
45
|
# back to base.
|
|
46
46
|
# @return [Spree::Price]
|
|
47
47
|
def find_price_from_lists
|
|
48
|
-
|
|
48
|
+
catalog_price_list_groups.each do |price_lists|
|
|
49
|
+
price = best_price_among(price_lists)
|
|
50
|
+
return price if price
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
applicable_price_lists.each do |price_list|
|
|
49
54
|
price = find_price_for_list(price_list)
|
|
50
55
|
return price if price
|
|
51
56
|
end
|
|
@@ -53,12 +58,25 @@ module Spree
|
|
|
53
58
|
nil
|
|
54
59
|
end
|
|
55
60
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
61
|
+
# The cheapest price one node's lists give, nil when none prices this
|
|
62
|
+
# variant. Catalogs sharing a node rank equally, so taking the first to
|
|
63
|
+
# answer charged a company from whichever sorted first rather than the
|
|
64
|
+
# cheaper (docs/plans/6.0-b2b-companies-and-catalogs.md). Equal amounts
|
|
65
|
+
# keep list order, so the answer is stable.
|
|
66
|
+
#
|
|
67
|
+
# @param price_lists [Array<Spree::PriceList>]
|
|
68
|
+
# @return [Spree::Price, nil]
|
|
69
|
+
def best_price_among(price_lists)
|
|
70
|
+
price_lists.filter_map { |price_list| find_price_for_list(price_list) }.min_by(&:amount)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Price lists reached through the buyer's effective catalogs, grouped
|
|
74
|
+
# by the node that assigns them and ordered nearest node first — the
|
|
75
|
+
# company subtree, or failing that the customer's groups, or failing
|
|
76
|
+
# that the channel's default catalog. The same chain visibility uses,
|
|
77
|
+
# and its steps are alternatives: a company buyer is priced by their
|
|
78
|
+
# company's agreement and never also by their customer group's
|
|
79
|
+
# ({Spree::Catalog.groups_for_context}).
|
|
62
80
|
#
|
|
63
81
|
# A catalog-attached list applies because the catalog applies: its
|
|
64
82
|
# audience rules are not consulted, since the assignment already
|
|
@@ -68,10 +86,13 @@ module Spree
|
|
|
68
86
|
# volume pricing is (docs/plans/6.0-price-list-automatic-pricing.md).
|
|
69
87
|
# Status and dates gate the list as before.
|
|
70
88
|
#
|
|
71
|
-
# @return [Array<Spree::PriceList
|
|
72
|
-
def
|
|
73
|
-
@
|
|
74
|
-
price_list.
|
|
89
|
+
# @return [Array<Array<Spree::PriceList>>]
|
|
90
|
+
def catalog_price_list_groups
|
|
91
|
+
@catalog_price_list_groups ||= catalog_groups_for_context.filter_map do |catalogs|
|
|
92
|
+
lists = catalogs.filter_map(&:price_list).select do |price_list|
|
|
93
|
+
price_list.currently_active? && price_list.contextual_rules_applicable?(context)
|
|
94
|
+
end
|
|
95
|
+
lists.presence
|
|
75
96
|
end
|
|
76
97
|
end
|
|
77
98
|
|
|
@@ -89,11 +110,12 @@ module Spree
|
|
|
89
110
|
# Catalogs that apply to this buyer, through the one entry point
|
|
90
111
|
# visibility and quantity terms also use — what a buyer is shown, what
|
|
91
112
|
# they pay and how much they must take have to come from the same
|
|
92
|
-
# agreement.
|
|
113
|
+
# agreement. Grouped by assigning node, since that is where precedence
|
|
114
|
+
# lives. Reuses the request-scoped set for the current store, so a
|
|
93
115
|
# listing does not re-resolve the same buyer per variant.
|
|
94
|
-
# @return [Array<Spree::Catalog
|
|
95
|
-
def
|
|
96
|
-
Spree::Catalog.
|
|
116
|
+
# @return [Array<Array<Spree::Catalog>>]
|
|
117
|
+
def catalog_groups_for_context
|
|
118
|
+
Spree::Catalog.groups_for_buyer(
|
|
97
119
|
store: context.store, customer: context.user,
|
|
98
120
|
company: context.company, channel: context.channel
|
|
99
121
|
)
|
data/app/models/spree/refund.rb
CHANGED
|
@@ -2,6 +2,7 @@ module Spree
|
|
|
2
2
|
class Refund < Spree.base_class
|
|
3
3
|
has_prefix_id :re # Stripe: re_
|
|
4
4
|
|
|
5
|
+
include Spree::ActedBy
|
|
5
6
|
include Spree::HasCustomFields
|
|
6
7
|
include Spree::Metadata
|
|
7
8
|
include Spree::InstrumentsGatewayCalls
|
|
@@ -18,7 +19,7 @@ module Spree
|
|
|
18
19
|
# payment covers several and only one of them is being refunded.
|
|
19
20
|
belongs_to :order, class_name: 'Spree::Order', optional: true, inverse_of: :refunds
|
|
20
21
|
belongs_to :reason, class_name: 'Spree::RefundReason', foreign_key: :refund_reason_id
|
|
21
|
-
|
|
22
|
+
acted_by :refunder
|
|
22
23
|
# What triggered this refund — a Spree::Return today, later an Exchange
|
|
23
24
|
# or Claim; nil for a manual refund. Deliberately polymorphic: the set is
|
|
24
25
|
# small and closed, and refunds are never bulk-queried in a hot path.
|
data/app/models/spree/return.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Spree
|
|
|
14
14
|
has_prefix_id :ret
|
|
15
15
|
|
|
16
16
|
has_spree_number prefix: 'RET'
|
|
17
|
+
include Spree::ActedBy
|
|
17
18
|
include Spree::NumberIdentifier
|
|
18
19
|
include Spree::SingleStoreResource
|
|
19
20
|
include Spree::HasStatus
|
|
@@ -31,7 +32,7 @@ module Spree
|
|
|
31
32
|
belongs_to :reason, class_name: 'Spree::ReturnReason', optional: true, inverse_of: :returns
|
|
32
33
|
# Staff only. Customer-initiated returns leave this nil — the requester
|
|
33
34
|
# is always order.customer, so no second association is needed.
|
|
34
|
-
|
|
35
|
+
acted_by :created_by
|
|
35
36
|
|
|
36
37
|
has_many :return_line_items, class_name: 'Spree::ReturnLineItem',
|
|
37
38
|
dependent: :destroy, inverse_of: :return
|
|
@@ -9,6 +9,7 @@ module Spree
|
|
|
9
9
|
class StockReceipt < Spree.base_class
|
|
10
10
|
has_prefix_id :sr
|
|
11
11
|
|
|
12
|
+
include Spree::ActedBy
|
|
12
13
|
include Spree::SingleStoreResource
|
|
13
14
|
has_spree_number prefix: 'SR'
|
|
14
15
|
include Spree::NumberIdentifier
|
|
@@ -17,7 +18,7 @@ module Spree
|
|
|
17
18
|
publishes_lifecycle_events
|
|
18
19
|
|
|
19
20
|
belongs_to :receivable, polymorphic: true, inverse_of: :stock_receipts
|
|
20
|
-
|
|
21
|
+
acted_by :received_by
|
|
21
22
|
|
|
22
23
|
has_many :items, class_name: 'Spree::StockReceiptItem', inverse_of: :stock_receipt,
|
|
23
24
|
dependent: :destroy
|
|
@@ -29,8 +30,12 @@ module Spree
|
|
|
29
30
|
|
|
30
31
|
normalizes :reference, with: ->(value) { value.strip.presence }
|
|
31
32
|
|
|
33
|
+
# `received_by_type` sits beside its id because ids are per-table: filtering
|
|
34
|
+
# on the id alone would match the admin user and the API key that happen to
|
|
35
|
+
# share that number.
|
|
32
36
|
self.whitelisted_ransackable_attributes = %w[number reference received_at receivable_type
|
|
33
|
-
receivable_id received_by_id
|
|
37
|
+
receivable_id received_by_id received_by_type
|
|
38
|
+
created_at]
|
|
34
39
|
|
|
35
40
|
# @return [Integer] units this delivery put on the shelf
|
|
36
41
|
def quantity_accepted_total
|
data/app/models/spree/store.rb
CHANGED
|
@@ -95,6 +95,10 @@ module Spree
|
|
|
95
95
|
# e.g. "https://myshop.com" — see #storefront_url for the fallback chain.
|
|
96
96
|
preference :storefront_url, :string
|
|
97
97
|
preference :special_instructions_enabled, :boolean, default: false
|
|
98
|
+
# Advertises a confirm/review step for every checkout, even when no
|
|
99
|
+
# payment method asks for one. Payment methods that require confirmation
|
|
100
|
+
# get it regardless of this setting.
|
|
101
|
+
preference :always_include_confirm_step, :boolean, default: false
|
|
98
102
|
preference :stock_reservation_ttl_minutes, :integer, default: 10
|
|
99
103
|
# Store-wide default for when a customer is charged rather than only
|
|
100
104
|
# authorized. A payment method's own capture_method wins when set.
|
|
@@ -6,7 +6,8 @@ module Spree
|
|
|
6
6
|
# Approves an order: clears the risk flag and records who approved it and when.
|
|
7
7
|
#
|
|
8
8
|
# @param order [Spree::Order]
|
|
9
|
-
# @param approver [Object, nil]
|
|
9
|
+
# @param approver [Object, nil] who approved it — an admin user or an
|
|
10
|
+
# API key (see Spree.actor_classes)
|
|
10
11
|
# @param level [String, nil] deprecated and ignored — removed in Spree 6.1
|
|
11
12
|
# @param note [String, nil] deprecated and ignored — removed in Spree 6.1
|
|
12
13
|
# @return [Spree::ServiceModule::Result]
|
|
@@ -16,7 +17,7 @@ module Spree
|
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
changes = { considered_risky: false, approved_at: Time.current }
|
|
19
|
-
changes
|
|
20
|
+
changes.merge!(Spree::ActedBy.columns_for(:approver, approver)) if approver.present?
|
|
20
21
|
order.update_columns(changes)
|
|
21
22
|
|
|
22
23
|
order.publish_event('order.approved')
|
|
@@ -11,7 +11,8 @@ module Spree
|
|
|
11
11
|
hooks :before_cancel, :after_cancel
|
|
12
12
|
|
|
13
13
|
# @param order [Spree::Order]
|
|
14
|
-
# @param canceler [Object, nil]
|
|
14
|
+
# @param canceler [Object, nil] who initiated it — an admin user or an
|
|
15
|
+
# API key (see Spree.actor_classes)
|
|
15
16
|
# @param canceled_at [Time, nil] timestamp (defaults to Time.current)
|
|
16
17
|
# @param reason [Spree::OrderCancellationReason, nil] the merchant's own
|
|
17
18
|
# vocabulary; must belong to the order's store
|
|
@@ -102,7 +103,7 @@ module Spree
|
|
|
102
103
|
|
|
103
104
|
def mark_canceled
|
|
104
105
|
changes = { status: 'canceled', canceled_at: @decided_at, cancel_reason_id: reason&.id, cancel_note: note }
|
|
105
|
-
changes
|
|
106
|
+
changes.merge!(Spree::ActedBy.columns_for(:canceler, canceler)) if canceler.present?
|
|
106
107
|
order.update_columns(changes)
|
|
107
108
|
end
|
|
108
109
|
|
|
@@ -19,7 +19,7 @@ module Spree
|
|
|
19
19
|
# creditable balance
|
|
20
20
|
# @param reason [Spree::RefundReason, nil] defaults to the store's
|
|
21
21
|
# return-processing reason
|
|
22
|
-
# @param refunder [Object, nil]
|
|
22
|
+
# @param refunder [Object, nil] who is issuing it (see Spree.actor_classes)
|
|
23
23
|
# @param originator [Object, nil] what triggered the refund — a
|
|
24
24
|
# Spree::Return, Exchange or Claim; nil for a manual refund
|
|
25
25
|
# @param order [Spree::Order, nil] which order is being put right.
|
|
@@ -13,7 +13,7 @@ module Spree
|
|
|
13
13
|
hooks :validate, :after_approve
|
|
14
14
|
|
|
15
15
|
# @param return_record [Spree::Return]
|
|
16
|
-
# @param approver [Object, nil]
|
|
16
|
+
# @param approver [Object, nil] who is approving it (see Spree.actor_classes)
|
|
17
17
|
def perform(return_record:, approver: nil)
|
|
18
18
|
super
|
|
19
19
|
|
|
@@ -18,8 +18,8 @@ module Spree
|
|
|
18
18
|
# back to; defaults to the location that shipped them
|
|
19
19
|
# @param reason [Spree::ReturnReason, nil]
|
|
20
20
|
# @param memo [String, nil] customer- or staff-supplied note
|
|
21
|
-
# @param created_by [Object, nil]
|
|
22
|
-
# customer self-service
|
|
21
|
+
# @param created_by [Object, nil] who is opening it (see
|
|
22
|
+
# Spree.actor_classes); nil for customer self-service
|
|
23
23
|
def perform(order:, items:, stock_location: nil, reason: nil, memo: nil, created_by: nil)
|
|
24
24
|
super
|
|
25
25
|
|
|
@@ -19,7 +19,7 @@ module Spree
|
|
|
19
19
|
# @param amount [BigDecimal, Numeric, nil] defaults to what the return
|
|
20
20
|
# is still owed
|
|
21
21
|
# @param refund_method [String] 'original_payment' or 'store_credit'
|
|
22
|
-
# @param refunder [Object, nil]
|
|
22
|
+
# @param refunder [Object, nil] who is issuing it (see Spree.actor_classes)
|
|
23
23
|
def perform(return_record:, amount: nil, refund_method: 'original_payment', refunder: nil)
|
|
24
24
|
super
|
|
25
25
|
|
data/config/locales/en.yml
CHANGED
|
@@ -1102,6 +1102,7 @@ en:
|
|
|
1102
1102
|
store_is_already_set: Store is already set
|
|
1103
1103
|
unknown_custom_field_type: 'is not a known custom field type (expected one of: %{types})'
|
|
1104
1104
|
unknown_scopes: 'contains unknown scopes: %{scopes}'
|
|
1105
|
+
unregistered_actor: is not a registered actor class
|
|
1105
1106
|
variant_product_mismatch: must belong to the same product as the variant
|
|
1106
1107
|
services:
|
|
1107
1108
|
get_shipping_rates:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Makes the order-operation "who did this" associations polymorphic, so a
|
|
2
|
+
# write made with a secret API key records the key rather than nobody.
|
|
3
|
+
# See docs/plans/6.0-action-actors.md.
|
|
4
|
+
class OrderOperationActorsPolymorphic < ActiveRecord::Migration[8.1]
|
|
5
|
+
ACTORS = {
|
|
6
|
+
spree_orders: %i[created_by approver canceler],
|
|
7
|
+
spree_refunds: %i[refunder],
|
|
8
|
+
spree_returns: %i[created_by],
|
|
9
|
+
spree_exchanges: %i[created_by],
|
|
10
|
+
spree_claims: %i[created_by],
|
|
11
|
+
spree_stock_receipts: %i[received_by]
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def change
|
|
15
|
+
ACTORS.each do |table, names|
|
|
16
|
+
names.each do |name|
|
|
17
|
+
# Nullable with no default: the type is data, filled by
|
|
18
|
+
# `rake spree:upgrade:backfill_actor_types`. Until it runs, the
|
|
19
|
+
# transitional reader resolves these rows through the admin user
|
|
20
|
+
# class — the only thing they could ever have pointed at.
|
|
21
|
+
add_column table, :"#{name}_type", :string
|
|
22
|
+
|
|
23
|
+
# Named for the pair so it reads apart from the `_id` index that
|
|
24
|
+
# already exists on most of these columns.
|
|
25
|
+
add_index table, [:"#{name}_type", :"#{name}_id"],
|
|
26
|
+
name: "index_#{table}_on_#{name}_actor"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -76,7 +76,7 @@ module Spree
|
|
|
76
76
|
# the second run with "Spree::Brand is already used in your application".
|
|
77
77
|
class_option :skip_collision_check, type: :boolean, default: true, desc: false
|
|
78
78
|
|
|
79
|
-
desc '
|
|
79
|
+
desc 'Creates a new Spree API resource'
|
|
80
80
|
|
|
81
81
|
# --- Owned-once gating ---
|
|
82
82
|
#
|
|
@@ -171,8 +171,10 @@ module Spree
|
|
|
171
171
|
|
|
172
172
|
routes_file = api_routes_path
|
|
173
173
|
|
|
174
|
+
# An installed gem is read-only, which is the normal case: a host app
|
|
175
|
+
# declares its own routes instead, through the engine's route hook.
|
|
174
176
|
unless File.exist?(routes_file) && File.writable?(routes_file)
|
|
175
|
-
|
|
177
|
+
draw_app_routes
|
|
176
178
|
return
|
|
177
179
|
end
|
|
178
180
|
|
|
@@ -180,11 +182,67 @@ module Spree
|
|
|
180
182
|
inject_route_for(:admin, admin_route_line) if options[:admin]
|
|
181
183
|
end
|
|
182
184
|
|
|
185
|
+
no_tasks do
|
|
186
|
+
# A host app declares its API routes in config/routes.rb, inside the
|
|
187
|
+
# engine's route hook that spree-starter ships ready to fill in. The
|
|
188
|
+
# block must be registered before the engine draws its routes, which is
|
|
189
|
+
# why it lives in routes.rb rather than an initializer.
|
|
190
|
+
def draw_app_routes
|
|
191
|
+
path = 'config/routes.rb'
|
|
192
|
+
full = File.join(destination_root, path)
|
|
193
|
+
|
|
194
|
+
unless File.exist?(full)
|
|
195
|
+
say_status :skip, "#{path} not found — add the routes by hand", :yellow
|
|
196
|
+
return
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
unless File.read(full).include?('Spree::Core::Engine.add_routes')
|
|
200
|
+
say_status :skip, "#{path} has no `Spree::Core::Engine.add_routes` block — add the routes by hand", :yellow
|
|
201
|
+
return
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
append_app_route(path, :store, store_route_line.strip) if options[:store]
|
|
205
|
+
append_app_route(path, :admin, admin_route_line.strip) if options[:admin]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Inserts one `resources` line into the named namespace. Idempotent: a
|
|
209
|
+
# resource already declared there is left alone, so re-runs are safe.
|
|
210
|
+
def append_app_route(path, namespace, route_line)
|
|
211
|
+
full = File.join(destination_root, path)
|
|
212
|
+
content = File.read(full)
|
|
213
|
+
|
|
214
|
+
# Confined to the engine's route hook: an application may well have its
|
|
215
|
+
# own `namespace :admin` earlier in the file, and a route inserted there
|
|
216
|
+
# would never be drawn on the engine.
|
|
217
|
+
hook = content[/^(\s*)Spree::Core::Engine\.add_routes do\n.*?^\1end\n/m]
|
|
218
|
+
unless hook
|
|
219
|
+
say_status :skip, "#{path} (no `Spree::Core::Engine.add_routes` block — add `#{route_line}` by hand)", :yellow
|
|
220
|
+
return
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
match = hook.match(/^(\s*)namespace :#{namespace} do\n/)
|
|
224
|
+
unless match
|
|
225
|
+
say_status :skip, "#{path} (no :#{namespace} namespace — add `#{route_line}` by hand)", :yellow
|
|
226
|
+
return
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
block = hook[match.end(0)..].to_s[/\A.*?^#{match[1]}end$/m].to_s
|
|
230
|
+
if block.include?(route_line)
|
|
231
|
+
say_status :identical, "#{path} (#{namespace}: #{route_line})", :blue
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
updated_hook = hook.sub(match[0], "#{match[0]}#{match[1]} #{route_line}\n")
|
|
236
|
+
File.write(full, content.sub(hook, updated_hook))
|
|
237
|
+
say_status :route, "#{path} (#{namespace}: #{route_line})", :green
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
183
241
|
def print_summary
|
|
184
242
|
say ''
|
|
185
243
|
say "✓ Generated Spree::#{bare_class_name} API resource", :green
|
|
186
244
|
say ''
|
|
187
|
-
say " Prefixed ID: #{id_prefix}_xxxxxxxxxx
|
|
245
|
+
say " Prefixed ID: #{id_prefix}_xxxxxxxxxx"
|
|
188
246
|
if store_external_name != bare_class_name
|
|
189
247
|
say " Store API: /api/v3/store/#{store_external_plural} (aliased from #{bare_class_name})"
|
|
190
248
|
elsif options[:store]
|
|
@@ -196,9 +254,9 @@ module Spree
|
|
|
196
254
|
say ' 1. Review the generated model — add validations, scopes, callbacks'
|
|
197
255
|
say ' 2. Apply the migration: pnpm exec spree migrate'
|
|
198
256
|
say ' 3. Set up authorization (CanCanCan ability) for the resource'
|
|
199
|
-
say
|
|
257
|
+
say " 4. Add `has_many :#{plural_name}` to Spree::Store" if store_scoped?
|
|
200
258
|
if options[:store] || options[:admin]
|
|
201
|
-
say
|
|
259
|
+
say " #{store_scoped? ? 5 : 4}. Run the specs: pnpm exec spree rspec spec/controllers/spree/api/v3/"
|
|
202
260
|
end
|
|
203
261
|
say ''
|
|
204
262
|
end
|
|
@@ -3,6 +3,11 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Admin
|
|
5
5
|
class <%= plural_name.camelize %>Controller < ResourceController
|
|
6
|
+
# Which API key scope authorizes this endpoint — `read_<%= plural_name %>`
|
|
7
|
+
# and `write_<%= plural_name %>`. Point it at an existing resource name
|
|
8
|
+
# instead if this endpoint belongs under one.
|
|
9
|
+
scoped_resource :<%= plural_name %>
|
|
10
|
+
|
|
6
11
|
protected
|
|
7
12
|
|
|
8
13
|
def model_class
|
|
@@ -2,6 +2,9 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
RSpec.describe Spree::Api::V3::Admin::<%= plural_name.camelize %>Controller, type: :controller do
|
|
4
4
|
render_views
|
|
5
|
+
# The controller is mounted by the engine, so its paths resolve
|
|
6
|
+
# through the engine's router rather than the application's.
|
|
7
|
+
routes { Spree::Core::Engine.routes }
|
|
5
8
|
|
|
6
9
|
include_context 'API v3 Admin authenticated'
|
|
7
10
|
|
|
@@ -2,8 +2,11 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
RSpec.describe Spree::Api::V3::Store::<%= plural_name.camelize %>Controller, type: :controller do
|
|
4
4
|
render_views
|
|
5
|
+
# The controller is mounted by the engine, so its paths resolve
|
|
6
|
+
# through the engine's router rather than the application's.
|
|
7
|
+
routes { Spree::Core::Engine.routes }
|
|
5
8
|
|
|
6
|
-
include_context 'API v3 Store'
|
|
9
|
+
include_context 'API v3 Store guest'
|
|
7
10
|
|
|
8
11
|
let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
|
|
9
12
|
|
|
@@ -25,7 +25,24 @@ module Spree
|
|
|
25
25
|
default: false,
|
|
26
26
|
desc: 'Include Spree::HasCustomFields and Spree::Metadata concerns'
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# On by default so every new resource is observable from the moment it
|
|
29
|
+
# exists — subscribers and webhooks both hang off these events, and a
|
|
30
|
+
# model that publishes nothing is invisible to integrations.
|
|
31
|
+
class_option :lifecycle_events,
|
|
32
|
+
type: :boolean,
|
|
33
|
+
default: true,
|
|
34
|
+
desc: 'Publish created/updated/deleted events'
|
|
35
|
+
|
|
36
|
+
# Store scoping is the default because commerce records belong to a store:
|
|
37
|
+
# catalog, configuration and orders are all per-store. Opt out with
|
|
38
|
+
# --no-store-scoped only for genuinely global reference data (countries,
|
|
39
|
+
# states, roles), which is rare enough to be the explicit case.
|
|
40
|
+
class_option :store_scoped,
|
|
41
|
+
type: :boolean,
|
|
42
|
+
default: true,
|
|
43
|
+
desc: 'Scope the model to a store'
|
|
44
|
+
|
|
45
|
+
desc 'Creates a new Spree model'
|
|
29
46
|
|
|
30
47
|
def create_module_file
|
|
31
48
|
return
|
|
@@ -44,6 +61,28 @@ module Spree
|
|
|
44
61
|
options[:custom_fields]
|
|
45
62
|
end
|
|
46
63
|
|
|
64
|
+
def lifecycle_events?
|
|
65
|
+
options[:lifecycle_events]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Whether the record belongs to a store, however that reference got
|
|
69
|
+
# there: the concern below, or a `store:references` the caller passed.
|
|
70
|
+
# Uniqueness and indexes key off this, so a value stays unique within a
|
|
71
|
+
# store rather than across the whole installation.
|
|
72
|
+
def store_scoped?
|
|
73
|
+
options[:store_scoped] || declares_store_reference?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The concern is what adds `belongs_to :store`, so a model that already
|
|
77
|
+
# declares one must not include it as well.
|
|
78
|
+
def includes_store_concern?
|
|
79
|
+
options[:store_scoped] && !declares_store_reference?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def declares_store_reference?
|
|
83
|
+
attributes.any? { |attribute| attribute.name == 'store' }
|
|
84
|
+
end
|
|
85
|
+
|
|
47
86
|
def class_path
|
|
48
87
|
['spree']
|
|
49
88
|
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
2
|
def change
|
|
3
3
|
create_table :<%= table_name %><%= primary_key_type %> do |t|
|
|
4
|
+
<% if includes_store_concern? -%>
|
|
5
|
+
t.references :store, null: false, index: true, foreign_key: false
|
|
6
|
+
|
|
7
|
+
<% end -%>
|
|
4
8
|
<% attributes.reject(&:reference?).reject(&:virtual?).each do |attribute| -%>
|
|
5
9
|
<% if attribute.password_digest? -%>
|
|
6
10
|
t.string :password_digest, null: false
|
|
@@ -27,9 +31,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
|
|
|
27
31
|
name: 'index_<%= table_name %>_on_<%= attribute.name %>'
|
|
28
32
|
<% end -%>
|
|
29
33
|
<% attributes.select(&:has_uniq_index?).reject(&:token?).each do |attribute| -%>
|
|
34
|
+
<% if store_scoped? -%>
|
|
35
|
+
add_index :<%= table_name %>, [:store_id, :<%= attribute.name %>], unique: true,
|
|
36
|
+
name: 'index_<%= table_name %>_on_store_id_and_<%= attribute.name %>'
|
|
37
|
+
<% else -%>
|
|
30
38
|
add_index :<%= table_name %>, :<%= attribute.name %>, unique: true,
|
|
31
39
|
name: 'index_<%= table_name %>_on_<%= attribute.name %>'
|
|
32
40
|
<% end -%>
|
|
41
|
+
<% end -%>
|
|
33
42
|
<% attributes.select { |a| a.has_index? && !a.has_uniq_index? && !a.reference? && !a.token? }.each do |attribute| -%>
|
|
34
43
|
add_index :<%= table_name %>, :<%= attribute.name %>
|
|
35
44
|
<% end -%>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
class <%= class_name.demodulize %> < <%= parent_class_name %>
|
|
3
|
+
<% if includes_store_concern? -%>
|
|
4
|
+
include Spree::SingleStoreResource
|
|
5
|
+
|
|
6
|
+
<% end -%>
|
|
3
7
|
<% if custom_fields? -%>
|
|
4
8
|
include Spree::HasCustomFields
|
|
5
9
|
include Spree::Metadata
|
|
@@ -10,7 +14,9 @@ module Spree
|
|
|
10
14
|
|
|
11
15
|
<% end -%>
|
|
12
16
|
has_prefix_id :<%= id_prefix %>
|
|
13
|
-
|
|
17
|
+
<% if lifecycle_events? -%>
|
|
18
|
+
publishes_lifecycle_events
|
|
19
|
+
<% end -%>
|
|
14
20
|
<% attributes.select(&:reference?).each do |attribute| -%>
|
|
15
21
|
<% if attribute.polymorphic? -%>
|
|
16
22
|
belongs_to :<%= attribute.name %>, polymorphic: true
|
|
@@ -38,10 +44,10 @@ module Spree
|
|
|
38
44
|
has_secure_password
|
|
39
45
|
<% end -%>
|
|
40
46
|
<% required_attrs = attributes.reject { |a| a.reference? || a.type == :boolean || a.token? || a.attachment? || a.attachments? || a.rich_text? || a.password_digest? } -%>
|
|
41
|
-
<% if required_attrs.any?
|
|
42
|
-
<% required_attrs.each do |attr| -%>
|
|
43
|
-
validates :<%= attr.name %>, presence: true<% if attr.has_uniq_index? %>, uniqueness: { scope: spree_base_uniqueness_scope }<% end %>
|
|
47
|
+
<% if required_attrs.any? -%>
|
|
44
48
|
|
|
49
|
+
<% required_attrs.each do |attr| -%>
|
|
50
|
+
validates :<%= attr.name %>, presence: true<% if attr.has_uniq_index? %>, uniqueness: { scope: [<%= ':store_id, ' if store_scoped? %>*spree_base_uniqueness_scope] }<% end %>
|
|
45
51
|
<% end -%>
|
|
46
52
|
<% end -%>
|
|
47
53
|
<% ransackable = attributes.reject { |a| a.reference? || a.attachment? || a.attachments? || a.rich_text? || a.token? || a.password_digest? }.map(&:name) -%>
|