spree_core 6.0.0.rc2 → 6.0.0.rc4
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/mailers/spree/email_template_mailer.rb +5 -2
- data/app/models/concerns/spree/has_external_references.rb +7 -6
- data/app/models/concerns/spree/preference_schema/json_schema.rb +8 -1
- data/app/models/concerns/spree/purchase/taxation.rb +44 -3
- data/app/models/concerns/spree/translatable_resource.rb +49 -0
- data/app/models/spree/cart.rb +1 -0
- data/app/models/spree/delivery_rate.rb +12 -4
- data/app/models/spree/delivery_rate_provider/base.rb +1 -1
- data/app/models/spree/delivery_rate_provider/estimate.rb +5 -4
- data/app/models/spree/fulfillment.rb +1 -0
- data/app/models/spree/gateway/bogus.rb +7 -0
- data/app/models/spree/line_item.rb +3 -8
- data/app/models/spree/oauth_access_grant.rb +11 -0
- data/app/models/spree/oauth_access_token.rb +16 -0
- data/app/models/spree/oauth_application.rb +97 -0
- data/app/models/spree/oauth_consent_request.rb +23 -0
- data/app/models/spree/oauth_protected_resource.rb +21 -0
- data/app/models/spree/oauth_redirect.rb +12 -0
- data/app/models/spree/option_type.rb +5 -0
- data/app/models/spree/option_value.rb +5 -0
- data/app/models/spree/order.rb +5 -1
- data/app/models/spree/payment.rb +14 -2
- data/app/models/spree/payment_capture_event.rb +2 -0
- data/app/models/spree/policy.rb +6 -0
- data/app/models/spree/price.rb +16 -33
- data/app/models/spree/price_rule.rb +3 -16
- data/app/models/spree/price_rules/market_rule.rb +0 -6
- data/app/models/spree/pricing_provider/base.rb +1 -1
- data/app/models/spree/product.rb +1 -1
- data/app/models/spree/purchase_order_item.rb +1 -1
- data/app/models/spree/return.rb +1 -0
- data/app/models/spree/search_provider/database.rb +1 -1
- data/app/models/spree/stock/estimator.rb +4 -20
- data/app/models/spree/store.rb +8 -0
- data/app/models/spree/tax_exemption/item_override.rb +14 -0
- data/app/models/spree/tax_exemption.rb +26 -0
- data/app/models/spree/tax_exemption_certificate.rb +13 -2
- data/app/models/spree/tax_exemption_reason.rb +62 -0
- data/app/models/spree/tax_provider/base.rb +24 -4
- data/app/models/spree/tax_provider/internal.rb +36 -29
- data/app/models/spree/tax_rate.rb +9 -35
- data/app/models/spree/translations/batch.rb +14 -4
- data/app/services/spree/carts/price_items.rb +2 -8
- data/app/services/spree/carts/split_by_seller.rb +6 -1
- data/app/services/spree/carts/update.rb +34 -7
- data/app/services/spree/email_templates/preview.rb +24 -3
- data/app/services/spree/seeds/store_resources.rb +1 -0
- data/app/services/spree/seeds/tax_exemption_reasons.rb +28 -0
- data/app/services/spree/tax/resolve_exemptions.rb +11 -2
- data/app/services/spree/update_check.rb +8 -1
- data/app/tools/spree/agent_tools/create_export.rb +116 -0
- data/app/tools/spree/agent_tools/create_import.rb +117 -0
- data/app/tools/spree/agent_tools/create_media.rb +81 -0
- data/app/tools/spree/agent_tools/create_resource.rb +36 -0
- data/app/tools/spree/agent_tools/curate_products.rb +90 -0
- data/app/tools/spree/agent_tools/delete_resource.rb +28 -0
- data/app/tools/spree/agent_tools/describe_import_mapping.rb +73 -0
- data/app/tools/spree/agent_tools/describe_reporting.rb +39 -0
- data/app/tools/spree/agent_tools/describe_resource.rb +81 -0
- data/app/tools/spree/agent_tools/get_import_status.rb +71 -0
- data/app/tools/spree/agent_tools/get_resource.rb +97 -0
- data/app/tools/spree/agent_tools/import_tool.rb +80 -0
- data/app/tools/spree/agent_tools/propose_import_mapping.rb +138 -0
- data/app/tools/spree/agent_tools/query_report.rb +124 -0
- data/app/tools/spree/agent_tools/resource_write.rb +99 -0
- data/app/tools/spree/agent_tools/search_resources.rb +155 -0
- data/app/tools/spree/agent_tools/set_stock.rb +52 -0
- data/app/tools/spree/agent_tools/update_resource.rb +42 -0
- data/app/tools/spree/agent_tools/upload_file.rb +104 -0
- data/app/workflows/spree/carts/complete.rb +115 -2
- data/app/workflows/spree/catalogs/set_price_list.rb +2 -2
- data/app/workflows/spree/orders/cancel.rb +8 -0
- data/app/workflows/spree/orders/complete.rb +8 -0
- data/app/{services → workflows}/spree/orders/create.rb +9 -9
- data/app/workflows/spree/purchase_orders/create.rb +3 -0
- data/app/workflows/spree/returns/refund.rb +4 -0
- data/app/{services → workflows}/spree/stock_levels/bulk_upsert.rb +49 -7
- data/app/workflows/spree/variants/create.rb +7 -4
- data/config/locales/en.yml +14 -0
- data/db/migrate/20250902143122_fix_policies_store_association.rb +6 -4
- data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +11 -2
- data/db/migrate/20260904100001_add_applied_tax_exemptions_to_spree_orders.rb +16 -0
- data/db/migrate/20260914230000_add_store_to_spree_option_types.rb +12 -2
- data/db/migrate/20261005120000_create_spree_oauth_tables.rb +67 -0
- data/db/migrate/20261016000001_remove_default_from_spree_tax_rates_included_in_price.rb +9 -0
- data/db/migrate/20261016100001_create_spree_tax_exemption_reasons.rb +21 -0
- data/db/migrate/20261017100001_add_tax_exemption_reason_to_spree_tax_exemption_certificates.rb +9 -0
- data/db/sample_data/orders.rb +1 -1
- data/lib/spree/agent_tool.rb +188 -0
- data/lib/spree/agent_tools/api_dispatch.rb +204 -0
- data/lib/spree/agent_tools/context.rb +191 -0
- data/lib/spree/agent_tools/default_catalog.rb +180 -0
- data/lib/spree/agent_tools/membership_map.rb +61 -0
- data/lib/spree/agent_tools/record_summary.rb +219 -0
- data/lib/spree/agent_tools/registry.rb +131 -0
- data/lib/spree/agent_tools/resource_map.rb +352 -0
- data/lib/spree/agent_tools/workflow_endpoints.rb +88 -0
- data/lib/spree/agent_tools/workflow_schema.rb +279 -0
- data/lib/spree/agent_tools/workflow_tool.rb +654 -0
- data/lib/spree/agent_tools.rb +24 -0
- data/lib/spree/core/configuration.rb +1 -1
- data/lib/spree/core/controller_helpers/store.rb +3 -13
- data/lib/spree/core/dependencies.rb +9 -0
- data/lib/spree/core/engine.rb +35 -1
- data/lib/spree/core/model_extensions.rb +111 -0
- data/lib/spree/core/permissions/default_catalog.rb +6 -1
- data/lib/spree/core/pricing/price_resolution.rb +4 -4
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +27 -2
- data/lib/spree/service_module.rb +11 -0
- data/lib/spree/testing_support/factories/tax_exemption_certificate_factory.rb +13 -1
- data/lib/spree/testing_support/factories/tax_exemption_reason_factory.rb +8 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +38 -0
- data/lib/tasks/markets.rake +47 -0
- data/lib/tasks/tax_exemption_reasons.rake +48 -0
- metadata +67 -7
- data/app/models/concerns/spree/vat_price_calculation.rb +0 -99
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module AgentTools
|
|
3
|
+
# Reads a workflow's argument contract and turns it into a JSON parameter
|
|
4
|
+
# schema an agent can fill in.
|
|
5
|
+
#
|
|
6
|
+
# Nothing here is hand-maintained. `Method#parameters` on `perform` names
|
|
7
|
+
# every keyword and says which are required; the YARD `@param` block above
|
|
8
|
+
# it gives each one a type and a sentence of prose. Both already exist on
|
|
9
|
+
# every workflow, because the house style requires them — so a workflow
|
|
10
|
+
# gains a parameter and the tool's schema gains it in the same commit.
|
|
11
|
+
class WorkflowSchema
|
|
12
|
+
# A workflow that cannot be exposed as written. Raised at derivation time
|
|
13
|
+
# so the contract spec fails in CI rather than an agent meeting a broken
|
|
14
|
+
# tool in production.
|
|
15
|
+
class UndocumentedParameterError < StandardError
|
|
16
|
+
def initialize(workflow_class, names)
|
|
17
|
+
super("#{workflow_class} exposes #{names.join(', ')} without a YARD @param — " \
|
|
18
|
+
'every parameter of an exposed workflow needs a documented type')
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Tenancy, which comes from the credential and never from the caller.
|
|
23
|
+
# A workflow that takes `store:` is given the context's store; offering
|
|
24
|
+
# it would invite a model to name another merchant's store, and the
|
|
25
|
+
# answer to "which store" is the API key or the signed-in admin.
|
|
26
|
+
CONTEXT_PARAMETERS = %i[store].freeze
|
|
27
|
+
|
|
28
|
+
# Actor keywords a workflow takes whose association has not been
|
|
29
|
+
# converted to `acted_by` yet — the 6.1 half of
|
|
30
|
+
# docs/plans/6.0-action-actors.md. They are principals by the same
|
|
31
|
+
# reasoning as the converted ones, so they are injected too; each drops
|
|
32
|
+
# off this list as its association converts, and the registry below then
|
|
33
|
+
# carries it.
|
|
34
|
+
UNCONVERTED_PRINCIPAL_PARAMETERS = %i[
|
|
35
|
+
reviewer reviewed_by rejected_by suspended_by verified_by reopened_by
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
# YARD type to JSON type. A model type is handled separately — it becomes
|
|
39
|
+
# a prefixed-id string resolved through the resource map.
|
|
40
|
+
PRIMITIVES = {
|
|
41
|
+
'String' => 'string',
|
|
42
|
+
'Symbol' => 'string',
|
|
43
|
+
'Time' => 'string',
|
|
44
|
+
'DateTime' => 'string',
|
|
45
|
+
'Date' => 'string',
|
|
46
|
+
'Boolean' => 'boolean',
|
|
47
|
+
'TrueClass' => 'boolean',
|
|
48
|
+
'FalseClass' => 'boolean',
|
|
49
|
+
'Integer' => 'integer',
|
|
50
|
+
'Numeric' => 'number',
|
|
51
|
+
'Float' => 'number',
|
|
52
|
+
'BigDecimal' => 'number',
|
|
53
|
+
'Hash' => 'object',
|
|
54
|
+
'Array' => 'array'
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
# @return [Class] the workflow this schema describes
|
|
58
|
+
attr_reader :workflow_class
|
|
59
|
+
|
|
60
|
+
# @param workflow_class [Class]
|
|
61
|
+
# @param except [Array<Symbol>] parameters to leave out of the schema
|
|
62
|
+
def initialize(workflow_class, except: [])
|
|
63
|
+
@workflow_class = workflow_class
|
|
64
|
+
@except = except.map(&:to_sym)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Every parameter the caller may supply, in `perform` order.
|
|
68
|
+
#
|
|
69
|
+
# @return [Array<Hash>] `{name:, required:, json_type:, model_name:, description:}`
|
|
70
|
+
def parameters
|
|
71
|
+
@parameters ||= begin
|
|
72
|
+
documented = docs
|
|
73
|
+
undocumented = keywords.keys - documented.keys - injected_parameters - @except
|
|
74
|
+
raise UndocumentedParameterError.new(workflow_class, undocumented) if undocumented.any?
|
|
75
|
+
|
|
76
|
+
keywords.filter_map do |name, required|
|
|
77
|
+
next if @except.include?(name) || injected_parameters.include?(name)
|
|
78
|
+
|
|
79
|
+
build_parameter(name, required, documented.fetch(name))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The principal keywords this workflow accepts, so the adapter knows
|
|
85
|
+
# which to inject.
|
|
86
|
+
#
|
|
87
|
+
# @return [Array<Symbol>]
|
|
88
|
+
def principal_parameters
|
|
89
|
+
keywords.keys & self.class.principal_parameter_names
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The tenancy keywords this workflow accepts.
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<Symbol>]
|
|
95
|
+
def context_parameters
|
|
96
|
+
keywords.keys & CONTEXT_PARAMETERS
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Everything filled from the context rather than by the caller.
|
|
100
|
+
#
|
|
101
|
+
# @return [Array<Symbol>]
|
|
102
|
+
def injected_parameters
|
|
103
|
+
self.class.principal_parameter_names + CONTEXT_PARAMETERS
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# `perform`'s keywords, mapped to whether they are required.
|
|
109
|
+
#
|
|
110
|
+
# @return [Hash{Symbol => Boolean}]
|
|
111
|
+
def keywords
|
|
112
|
+
@keywords ||= workflow_class.instance_method(:perform).parameters.filter_map do |kind, name|
|
|
113
|
+
[name, kind == :keyreq] if %i[key keyreq].include?(kind)
|
|
114
|
+
end.to_h
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_parameter(name, required, doc)
|
|
118
|
+
types = doc[:types]
|
|
119
|
+
model_name = types.filter_map { |type| model_name_for(type) }.first
|
|
120
|
+
|
|
121
|
+
json_type = model_name ? 'string' : json_type_for(types)
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
name: name,
|
|
125
|
+
required: required,
|
|
126
|
+
model_name: model_name,
|
|
127
|
+
json_type: json_type,
|
|
128
|
+
item_type: json_type == 'array' ? item_type_for(types) : nil,
|
|
129
|
+
description: description_for(doc[:description], model_name)
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def description_for(text, model_name)
|
|
134
|
+
return text if model_name.blank?
|
|
135
|
+
|
|
136
|
+
prefix = "Prefixed id of the #{model_name.demodulize.underscore.humanize.downcase}"
|
|
137
|
+
text.present? ? "#{prefix} — #{text}" : prefix
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# A Spree model, addressed by prefixed id, as its canonical class name.
|
|
141
|
+
# A YARD block may name a deprecated alias (`Spree::ShippingMethod` for
|
|
142
|
+
# `Spree::DeliveryMethod`), and the alias is a constant pointing at the
|
|
143
|
+
# same class — so resolve the constant and ask it for its own name
|
|
144
|
+
# rather than trusting the spelling.
|
|
145
|
+
#
|
|
146
|
+
# `Spree.admin_user_class` and the bare `Object` of a principal
|
|
147
|
+
# parameter never reach here: those parameters are stripped first.
|
|
148
|
+
#
|
|
149
|
+
# @return [String, nil]
|
|
150
|
+
def model_name_for(type)
|
|
151
|
+
return unless type.start_with?('Spree::')
|
|
152
|
+
|
|
153
|
+
constant = type.safe_constantize
|
|
154
|
+
return unless constant.is_a?(Class) && constant < ActiveRecord::Base
|
|
155
|
+
|
|
156
|
+
constant.name
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# What an array holds, read from the documented type: `Array<Hash>` holds
|
|
160
|
+
# objects, and anything else is treated as strings — the common case for
|
|
161
|
+
# a list of names or codes.
|
|
162
|
+
def item_type_for(types)
|
|
163
|
+
inner = types.filter_map { |type| type[/\AArray<(.+)>\z/, 1] }.first
|
|
164
|
+
|
|
165
|
+
PRIMITIVES[inner] || 'string'
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def json_type_for(types)
|
|
169
|
+
concrete = types.reject { |type| type == 'nil' }
|
|
170
|
+
# `Array<Hash>` and friends — the outer container is what JSON sees.
|
|
171
|
+
concrete = concrete.map { |type| type.split('<').first }
|
|
172
|
+
concrete.filter_map { |type| PRIMITIVES[type] }.first || 'string'
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The YARD block above `perform`, parsed from the workflow's own source.
|
|
176
|
+
#
|
|
177
|
+
# Read from source rather than through the YARD gem: the gem is a
|
|
178
|
+
# development dependency, and this has to work in production, where the
|
|
179
|
+
# tool list is built on every request.
|
|
180
|
+
#
|
|
181
|
+
# @return [Hash{Symbol => Hash}]
|
|
182
|
+
def docs
|
|
183
|
+
@docs ||= self.class.parse_docs(workflow_class)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class << self
|
|
187
|
+
# Every keyword that names who acted, so none is ever offered to a
|
|
188
|
+
# caller as a free parameter.
|
|
189
|
+
#
|
|
190
|
+
# Read from {Spree::ActedBy} rather than listed here: the macro already
|
|
191
|
+
# records each actor association per model, exactly so a caller
|
|
192
|
+
# sweeping them walks all of them without being handed a list. A model
|
|
193
|
+
# that gains an actor gains it here in the same commit, and a name
|
|
194
|
+
# nobody declares cannot linger.
|
|
195
|
+
#
|
|
196
|
+
# @return [Array<Symbol>]
|
|
197
|
+
def principal_parameter_names
|
|
198
|
+
declared = if defined?(Spree::ActedBy)
|
|
199
|
+
Spree::ActedBy.models.flat_map(&:acted_by_associations).map(&:to_sym)
|
|
200
|
+
else
|
|
201
|
+
[]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
(declared | UNCONVERTED_PRINCIPAL_PARAMETERS).uniq
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Parsed once per workflow class and cached, because a tool list is
|
|
208
|
+
# built for every MCP request and the source never changes at runtime.
|
|
209
|
+
#
|
|
210
|
+
# @param workflow_class [Class]
|
|
211
|
+
# @return [Hash{Symbol => Hash}] `{types: [String], description: String}`
|
|
212
|
+
def parse_docs(workflow_class)
|
|
213
|
+
file, = workflow_class.instance_method(:perform).source_location
|
|
214
|
+
key = [workflow_class.name, file, (File.mtime(file) if file && File.exist?(file))]
|
|
215
|
+
|
|
216
|
+
cache[key] ||= parse_source(workflow_class)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @return [void]
|
|
220
|
+
def clear_cache!
|
|
221
|
+
@cache = {}
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def cache
|
|
227
|
+
@cache ||= {}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def parse_source(workflow_class)
|
|
231
|
+
source = comment_above_perform(workflow_class)
|
|
232
|
+
return {} if source.blank?
|
|
233
|
+
|
|
234
|
+
parse_param_tags(source)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# The comment block immediately above `def perform`, joined so a
|
|
238
|
+
# `@param` whose prose wraps onto the next line keeps it.
|
|
239
|
+
def comment_above_perform(workflow_class)
|
|
240
|
+
location = workflow_class.instance_method(:perform).source_location
|
|
241
|
+
return if location.nil?
|
|
242
|
+
|
|
243
|
+
file, line = location
|
|
244
|
+
return unless File.exist?(file)
|
|
245
|
+
|
|
246
|
+
lines = File.readlines(file)
|
|
247
|
+
comment = []
|
|
248
|
+
index = line - 2
|
|
249
|
+
while index >= 0 && lines[index].strip.start_with?('#')
|
|
250
|
+
comment.unshift(lines[index].strip.delete_prefix('#').strip)
|
|
251
|
+
index -= 1
|
|
252
|
+
end
|
|
253
|
+
comment
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def parse_param_tags(comment_lines)
|
|
257
|
+
tags = {}
|
|
258
|
+
current = nil
|
|
259
|
+
|
|
260
|
+
comment_lines.each do |line|
|
|
261
|
+
if (match = line.match(/\A@param\s+(\w+)\s+\[([^\]]*)\]\s*(.*)\z/))
|
|
262
|
+
current = match[1].to_sym
|
|
263
|
+
tags[current] = {
|
|
264
|
+
types: match[2].split(',').map(&:strip).reject(&:empty?),
|
|
265
|
+
description: match[3].strip
|
|
266
|
+
}
|
|
267
|
+
elsif line.start_with?('@')
|
|
268
|
+
current = nil
|
|
269
|
+
elsif current && line.present?
|
|
270
|
+
tags[current][:description] = [tags[current][:description], line].reject(&:blank?).join(' ')
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
tags
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|