spree_api 5.5.3 → 5.6.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/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +3 -1
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +1 -1
- data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +4 -2
- data/app/controllers/concerns/spree/api/v3/locale_and_currency.rb +4 -1
- data/app/controllers/concerns/spree/api/v3/storefront_gating.rb +65 -0
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/categories/products_controller.rb +98 -0
- data/app/controllers/spree/api/v3/admin/categories_controller.rb +102 -2
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +8 -2
- data/app/controllers/spree/api/v3/admin/exports_controller.rb +3 -1
- data/app/controllers/spree/api/v3/admin/import_rows_controller.rb +49 -0
- data/app/controllers/spree/api/v3/admin/imports_controller.rb +274 -0
- data/app/controllers/spree/api/v3/admin/locales_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +39 -9
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +62 -2
- data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +105 -0
- data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +1 -8
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/products_controller.rb +8 -7
- data/app/controllers/spree/api/v3/admin/promotions_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/resource_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/store_controller.rb +3 -0
- data/app/controllers/spree/api/v3/admin/translatable_resources_controller.rb +38 -0
- data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +113 -0
- data/app/controllers/spree/api/v3/admin/translations_controller.rb +106 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +0 -2
- data/app/controllers/spree/api/v3/store/auth_controller.rb +3 -1
- data/app/controllers/spree/api/v3/store/base_controller.rb +5 -0
- data/app/controllers/spree/api/v3/store/carts/fulfillments_controller.rb +1 -3
- data/app/controllers/spree/api/v3/store/carts_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/countries_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/currencies_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +5 -1
- data/app/controllers/spree/api/v3/store/customers_controller.rb +3 -1
- data/app/controllers/spree/api/v3/store/digitals_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/locales_controller.rb +3 -8
- data/app/controllers/spree/api/v3/store/markets/countries_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/markets_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +5 -2
- data/app/controllers/spree/api/v3/store/products_controller.rb +1 -1
- data/app/controllers/spree/api/v3/store/resource_controller.rb +1 -0
- data/app/serializers/concerns/spree/api/v3/admin/translatable.rb +38 -0
- data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +7 -1
- data/app/serializers/spree/api/v3/admin/category_serializer.rb +5 -3
- data/app/serializers/spree/api/v3/admin/channel_serializer.rb +5 -1
- data/app/serializers/spree/api/v3/admin/customer_serializer.rb +7 -2
- data/app/serializers/spree/api/v3/admin/discount_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +9 -0
- data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +7 -0
- data/app/serializers/spree/api/v3/admin/import_mapping_serializer.rb +20 -0
- data/app/serializers/spree/api/v3/admin/import_row_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/admin/import_serializer.rb +70 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +11 -0
- data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +15 -1
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/product_serializer.rb +1 -0
- data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +8 -0
- data/app/serializers/spree/api/v3/admin/resource_translations_serializer.rb +76 -0
- data/app/serializers/spree/api/v3/admin/setup_task_serializer.rb +18 -0
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +27 -1
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +8 -1
- data/app/serializers/spree/api/v3/base_serializer.rb +13 -0
- data/app/serializers/spree/api/v3/cart_serializer.rb +25 -22
- data/app/serializers/spree/api/v3/discount_serializer.rb +6 -2
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +16 -13
- data/app/serializers/spree/api/v3/gift_card_serializer.rb +15 -11
- data/app/serializers/spree/api/v3/import_row_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/line_item_serializer.rb +35 -17
- data/app/serializers/spree/api/v3/locale_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/order_serializer.rb +25 -21
- data/app/serializers/spree/api/v3/payment_serializer.rb +6 -2
- data/app/serializers/spree/api/v3/product_serializer.rb +9 -1
- data/app/serializers/spree/api/v3/variant_serializer.rb +12 -1
- data/app/services/spree/api/v3/filters_aggregator.rb +4 -4
- data/app/subscribers/spree/webhook_event_subscriber.rb +6 -0
- data/config/initializers/typelizer.rb +10 -0
- data/config/locales/en.yml +4 -0
- data/config/routes.rb +54 -4
- data/lib/spree/api/dependencies.rb +5 -0
- data/lib/spree/api/openapi/schema_helper.rb +29 -0
- metadata +21 -6
|
@@ -39,6 +39,9 @@ module Spree
|
|
|
39
39
|
:preferred_timezone,
|
|
40
40
|
:preferred_weight_unit,
|
|
41
41
|
:preferred_unit_system,
|
|
42
|
+
:preferred_storefront_access,
|
|
43
|
+
:preferred_storefront_url,
|
|
44
|
+
:preferred_guest_checkout,
|
|
42
45
|
:mail_from_address,
|
|
43
46
|
:customer_support_email,
|
|
44
47
|
:new_order_notifications_email,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Self-describing discovery of every translatable resource and its
|
|
6
|
+
# translatable fields (the Spree.translatable_resources registry made
|
|
7
|
+
# public). Lets the dashboard render translation editors and the
|
|
8
|
+
# centralized translations grid generically, with no per-model code.
|
|
9
|
+
class TranslatableResourcesController < Admin::BaseController
|
|
10
|
+
scoped_resource :settings
|
|
11
|
+
|
|
12
|
+
# Public resource types with a dedicated nested read route
|
|
13
|
+
# (`…/:id/translations`). Other registered types are writable via the
|
|
14
|
+
# batch endpoint and readable inline (e.g. option values are returned
|
|
15
|
+
# as children of an option type), so the dashboard must not GET a
|
|
16
|
+
# standalone matrix for them. Keep in sync with the `:translatable`
|
|
17
|
+
# route concern mounts in `spree/api/config/routes.rb`.
|
|
18
|
+
READABLE_RESOURCE_TYPES = %w[product category option_type].freeze
|
|
19
|
+
|
|
20
|
+
# GET /api/v3/admin/translatable_resources
|
|
21
|
+
def index
|
|
22
|
+
data = Spree::Translations.registry.map do |entry|
|
|
23
|
+
entry.merge('readable' => READABLE_RESOURCE_TYPES.include?(entry['resource_type']))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
render json: { data: data }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def action_kind
|
|
32
|
+
'read'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
module Translations
|
|
6
|
+
# Atomically upserts translations across MANY records of (possibly)
|
|
7
|
+
# different translatable resource types in a single request. Powers
|
|
8
|
+
# the dashboard's combined editors (e.g. an option type + all its
|
|
9
|
+
# option values saved in one go).
|
|
10
|
+
#
|
|
11
|
+
# The body is a flat list of independent registry writes — NOT a
|
|
12
|
+
# nested/parent-owns-children payload. Each entry names its own
|
|
13
|
+
# resource_type + resource_id, so the controller stays free of
|
|
14
|
+
# per-model branching.
|
|
15
|
+
#
|
|
16
|
+
# POST /api/v3/admin/translations/batch
|
|
17
|
+
# Body:
|
|
18
|
+
# { "translations": [
|
|
19
|
+
# { "resource_type": "option_type", "resource_id": "opt_…",
|
|
20
|
+
# "values": { "de": { "label": "Größe" } } },
|
|
21
|
+
# { "resource_type": "option_value", "resource_id": "optval_…",
|
|
22
|
+
# "values": { "de": { "label": "Klein" } } }
|
|
23
|
+
# ] }
|
|
24
|
+
#
|
|
25
|
+
# All entries succeed or none do — a validation failure on any entry
|
|
26
|
+
# rolls back the whole transaction and returns per-entry detail.
|
|
27
|
+
# The orchestration (resolve → upsert → rollback) lives in
|
|
28
|
+
# Spree::Translations::Batch; the controller only handles params,
|
|
29
|
+
# authorization, and rendering.
|
|
30
|
+
class BatchesController < Admin::BaseController
|
|
31
|
+
# The batch spans heterogeneous resource types, so a single static
|
|
32
|
+
# scope can't gate it. We verify the API key holds the matching
|
|
33
|
+
# write_<resource> scope for EVERY entry (see require_batch_scopes!),
|
|
34
|
+
# which is stricter than the static check and keeps the audit model.
|
|
35
|
+
skip_scope_check!
|
|
36
|
+
|
|
37
|
+
# POST /api/v3/admin/translations/batch
|
|
38
|
+
def create
|
|
39
|
+
raw = params[:translations]
|
|
40
|
+
return render_empty_batch_error unless raw.is_a?(Array) && raw.any?
|
|
41
|
+
|
|
42
|
+
batch = Spree::Translations::Batch.new(batch_params)
|
|
43
|
+
return unless require_batch_scopes!(batch)
|
|
44
|
+
|
|
45
|
+
records = batch.process! { |record| authorize!(:update, record) }
|
|
46
|
+
render json: { data: records.map { |record| serialize_translations(record) } }
|
|
47
|
+
rescue Spree::Translations::Batch::EntryError => e
|
|
48
|
+
render_error(
|
|
49
|
+
code: ERROR_CODES[:validation_error],
|
|
50
|
+
message: e.message,
|
|
51
|
+
status: :unprocessable_content,
|
|
52
|
+
details: { translations: { e.index.to_s => [e.message] } }
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# For API-key callers, require write_<resource> for every distinct
|
|
59
|
+
# resource type in the batch. JWT callers bypass (current_api_key is
|
|
60
|
+
# nil) and rely on the per-record authorize!(:update, record) above.
|
|
61
|
+
# Returns false (and renders 403) when a scope is missing.
|
|
62
|
+
def require_batch_scopes!(batch)
|
|
63
|
+
return true unless current_api_key
|
|
64
|
+
|
|
65
|
+
missing = batch.required_scopes.reject { |scope| current_api_key.has_scope?(scope) }
|
|
66
|
+
return true if missing.empty?
|
|
67
|
+
|
|
68
|
+
render_error(
|
|
69
|
+
code: ERROR_CODES[:access_denied],
|
|
70
|
+
message: "API key lacks scope(s): #{missing.join(', ')}",
|
|
71
|
+
status: :forbidden,
|
|
72
|
+
details: { required_scopes: missing }
|
|
73
|
+
)
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# [{ resource_type:, resource_id:, values: { locale => { field => value } } }]
|
|
78
|
+
def batch_params
|
|
79
|
+
Array(params[:translations]).map do |entry|
|
|
80
|
+
permitted = entry.respond_to?(:permit) ? entry.permit(:resource_type, :resource_id, values: {}) : entry
|
|
81
|
+
values = permitted[:values]
|
|
82
|
+
{
|
|
83
|
+
resource_type: permitted[:resource_type].to_s,
|
|
84
|
+
resource_id: permitted[:resource_id].to_s,
|
|
85
|
+
values: values.respond_to?(:to_unsafe_h) ? values.to_unsafe_h : (values || {}).to_h
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def render_empty_batch_error
|
|
91
|
+
render_error(
|
|
92
|
+
code: ERROR_CODES[:validation_error],
|
|
93
|
+
message: 'translations must be a non-empty array',
|
|
94
|
+
status: :unprocessable_content
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Write echo: matrix + locale envelope, no discovery fields/children.
|
|
99
|
+
def serialize_translations(record)
|
|
100
|
+
Spree.api.admin_resource_translations_serializer.new(
|
|
101
|
+
record, params: serializer_params.merge(fields: false, envelope: true)
|
|
102
|
+
).to_h
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def action_kind
|
|
106
|
+
'write'
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Read-only translation matrix for any parent in the
|
|
6
|
+
# +Spree.translatable_resources+ registry. Mounted via the
|
|
7
|
+
# +:translatable+ route concern; the parent class is inferred from
|
|
8
|
+
# whichever +<segment>_id+ route param matches a registered translatable
|
|
9
|
+
# resource. One controller serves every model.
|
|
10
|
+
#
|
|
11
|
+
# Writes go through the batch endpoint (+POST /admin/translations/batch+)
|
|
12
|
+
# — a single atomic surface that also handles multi-record saves (an
|
|
13
|
+
# option type plus all its option values). There is no per-resource
|
|
14
|
+
# write here.
|
|
15
|
+
class TranslationsController < ResourceController
|
|
16
|
+
# This controller acts on the @parent (set by set_parent), never on a
|
|
17
|
+
# @resource of its own, so the base set_resource lookup (which needs a
|
|
18
|
+
# model_class) must not run.
|
|
19
|
+
skip_before_action :set_resource
|
|
20
|
+
before_action :ensure_translatable!
|
|
21
|
+
|
|
22
|
+
# GET /api/v3/admin/<parent>/<parent_id>/translations
|
|
23
|
+
# Returns the matrix + discovery fields, with nested translatable
|
|
24
|
+
# children (e.g. an option type's values) so an editor fetches the
|
|
25
|
+
# whole tree in one read.
|
|
26
|
+
def index
|
|
27
|
+
render json: { data: serialize_translations(@parent) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
def set_parent
|
|
33
|
+
raise ActiveRecord::RecordNotFound, 'Parent resource not found' unless parent_lookup
|
|
34
|
+
|
|
35
|
+
@parent = parent_relation.find_by_prefix_id!(parent_lookup.value)
|
|
36
|
+
authorize!(:show, @parent)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Resolves the parent within the current store so a token for one
|
|
40
|
+
# store can't read or write another store's translations. Preloads
|
|
41
|
+
# translatable children + their translation rows so the nested matrix
|
|
42
|
+
# read (e.g. an option type's values) avoids an N+1 over the child
|
|
43
|
+
# translation tables.
|
|
44
|
+
def parent_relation
|
|
45
|
+
klass = parent_lookup.klass
|
|
46
|
+
relation = klass.respond_to?(:for_store) ? klass.for_store(current_store) : klass
|
|
47
|
+
|
|
48
|
+
children = klass.try(:translatable_children)
|
|
49
|
+
children ? relation.includes(children => :translations) : relation
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Per-parent scope check: reading a product's translations needs
|
|
53
|
+
# `read_products`, a category's `read_categories`, etc.
|
|
54
|
+
def scoped_resource_name
|
|
55
|
+
parent_lookup&.segment&.pluralize&.to_sym
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Translatable by class hierarchy, not strict equality, so a subclass
|
|
59
|
+
# instance (e.g. Spree::Category < Spree::Taxon) passes when its base
|
|
60
|
+
# is registered.
|
|
61
|
+
def ensure_translatable!
|
|
62
|
+
# Compare by class NAME, not object identity: in dev the registry is
|
|
63
|
+
# populated at boot, but the model constants are reloaded on edits,
|
|
64
|
+
# so `is_a?(registered_class)` fails against a stale class object even
|
|
65
|
+
# when @parent's live class (or an ancestor) matches by name. Walk the
|
|
66
|
+
# ancestor names so a subclass (Spree::Category < Spree::Taxon) passes.
|
|
67
|
+
registered = Spree.translatable_resources.map(&:name).to_set
|
|
68
|
+
translatable = @parent.class.ancestors.any? { |ancestor| registered.include?(ancestor.name) }
|
|
69
|
+
raise ActiveRecord::RecordNotFound unless translatable
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
ParentLookup = Struct.new(:klass, :value, :segment)
|
|
73
|
+
|
|
74
|
+
# Maps route segment ('product', 'category', …) to model class name.
|
|
75
|
+
# Class names (not objects) survive dev-mode reloads. Aliases
|
|
76
|
+
# 'category' because routes expose taxons as categories (5.5 rename)
|
|
77
|
+
# while the model element is still 'taxon'.
|
|
78
|
+
def parent_route_map
|
|
79
|
+
@parent_route_map ||= Spree.translatable_resources.each_with_object({}) do |klass, m|
|
|
80
|
+
m[klass.model_name.element.to_s] = klass.name
|
|
81
|
+
end.merge('category' => 'Spree::Taxon')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def parent_lookup
|
|
85
|
+
return @parent_lookup if defined?(@parent_lookup)
|
|
86
|
+
|
|
87
|
+
match = parent_route_map.find { |segment, _| params[:"#{segment}_id"].present? }
|
|
88
|
+
@parent_lookup =
|
|
89
|
+
if match
|
|
90
|
+
segment, klass_name = match
|
|
91
|
+
ParentLookup.new(klass_name.constantize, params[:"#{segment}_id"], segment)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Full read shape: matrix + discovery fields + nested children +
|
|
96
|
+
# the locale envelope.
|
|
97
|
+
def serialize_translations(record)
|
|
98
|
+
Spree.api.admin_resource_translations_serializer.new(
|
|
99
|
+
record, params: serializer_params.merge(fields: true, envelope: true)
|
|
100
|
+
).to_h
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -114,8 +114,6 @@ module Spree
|
|
|
114
114
|
model_class.new(permitted_params)
|
|
115
115
|
end
|
|
116
116
|
resource.store = current_store if resource.respond_to?(:store_id) && resource.store_id.blank?
|
|
117
|
-
# very ugly code we need to still support for promotion/payment_method until we migrate them into single store in spree 6.0
|
|
118
|
-
resource.store_ids = [current_store.id] if resource.respond_to?(:store_ids) && resource.store_ids.blank? && !resource.respond_to?(:store_id)
|
|
119
117
|
resource.created_by = try_spree_current_user if resource.respond_to?(:created_by_id)
|
|
120
118
|
resource
|
|
121
119
|
end
|
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Store
|
|
5
5
|
class AuthController < Store::BaseController
|
|
6
|
+
allow_guest_storefront_access!
|
|
6
7
|
# Tighter rate limits for auth endpoints (per IP to prevent brute force)
|
|
7
8
|
rate_limit to: Spree::Api::Config[:rate_limit_login], within: Spree::Api::Config[:rate_limit_window].seconds, store: Rails.cache, only: :create, with: RATE_LIMIT_RESPONSE
|
|
8
9
|
rate_limit to: Spree::Api::Config[:rate_limit_refresh], within: Spree::Api::Config[:rate_limit_window].seconds, store: Rails.cache, only: :refresh, with: RATE_LIMIT_RESPONSE
|
|
@@ -87,7 +88,8 @@ module Spree
|
|
|
87
88
|
locale: current_locale,
|
|
88
89
|
currency: current_currency,
|
|
89
90
|
user: current_user,
|
|
90
|
-
includes: []
|
|
91
|
+
includes: [],
|
|
92
|
+
hide_prices: hide_prices?
|
|
91
93
|
}
|
|
92
94
|
end
|
|
93
95
|
|
|
@@ -8,6 +8,11 @@ module Spree
|
|
|
8
8
|
# this here keeps the +X-Spree-Channel+ header from accidentally
|
|
9
9
|
# narrowing admin queries.
|
|
10
10
|
include Spree::Api::V3::ChannelResolution
|
|
11
|
+
# Mirrors Store::ResourceController. Both branches must carry the gate so
|
|
12
|
+
# login_required / prices_hidden apply to every Store endpoint, not just
|
|
13
|
+
# ResourceController ones. Public endpoints opt out with
|
|
14
|
+
# +allow_guest_storefront_access!+.
|
|
15
|
+
include Spree::Api::V3::StorefrontGating
|
|
11
16
|
|
|
12
17
|
# Require publishable API key for all Store API requests
|
|
13
18
|
before_action :authenticate_api_key!
|
|
@@ -25,9 +25,7 @@ module Spree
|
|
|
25
25
|
fulfillment = @cart.shipments.find_by_prefix_id!(params[:id])
|
|
26
26
|
|
|
27
27
|
if permitted_params[:selected_delivery_rate_id].present?
|
|
28
|
-
|
|
29
|
-
fulfillment.selected_shipping_rate_id = delivery_rate.id
|
|
30
|
-
fulfillment.save!
|
|
28
|
+
fulfillment.selected_delivery_rate_id = permitted_params[:selected_delivery_rate_id]
|
|
31
29
|
end
|
|
32
30
|
|
|
33
31
|
# Auto-advance (e.g. delivery → payment) after rate selection.
|
|
@@ -104,6 +104,10 @@ module Spree
|
|
|
104
104
|
def complete
|
|
105
105
|
find_cart!
|
|
106
106
|
|
|
107
|
+
if @cart.guest_checkout_disallowed?
|
|
108
|
+
return render_authentication_required('api.errors.guest_checkout_not_allowed', 'You must be signed in to complete checkout')
|
|
109
|
+
end
|
|
110
|
+
|
|
107
111
|
result = Spree::Dependencies.carts_complete_service.constantize.call(cart: @cart)
|
|
108
112
|
|
|
109
113
|
if result.success?
|
|
@@ -6,6 +6,7 @@ module Spree
|
|
|
6
6
|
module Store
|
|
7
7
|
module Customer
|
|
8
8
|
class PasswordResetsController < Store::BaseController
|
|
9
|
+
allow_guest_storefront_access!
|
|
9
10
|
rate_limit to: Spree::Api::Config[:rate_limit_password_reset],
|
|
10
11
|
within: Spree::Api::Config[:rate_limit_window].seconds,
|
|
11
12
|
store: Rails.cache,
|
|
@@ -32,7 +33,7 @@ module Spree
|
|
|
32
33
|
|
|
33
34
|
if user
|
|
34
35
|
token = user.generate_token_for(:password_reset)
|
|
35
|
-
event_payload = { reset_token: token, email: user.email }
|
|
36
|
+
event_payload = { reset_token: token, email: user.email, store_id: current_store.prefixed_id }
|
|
36
37
|
event_payload[:redirect_url] = redirect_url if redirect_url.present?
|
|
37
38
|
user.publish_event('customer.password_reset_requested', event_payload)
|
|
38
39
|
end
|
|
@@ -55,6 +56,9 @@ module Spree
|
|
|
55
56
|
|
|
56
57
|
if user.update(password: params[:password], password_confirmation: params[:password_confirmation])
|
|
57
58
|
jwt = generate_jwt(user)
|
|
59
|
+
# Kill every existing session before minting the new one —
|
|
60
|
+
# a stolen refresh token must not survive a password reset.
|
|
61
|
+
Spree::RefreshToken.revoke_all_for(user)
|
|
58
62
|
refresh_token = Spree::RefreshToken.create_for(user, request_env: { ip_address: request.remote_ip, user_agent: request.user_agent&.truncate(255) })
|
|
59
63
|
user.publish_event('customer.password_reset')
|
|
60
64
|
|
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Store
|
|
5
5
|
class CustomersController < Store::BaseController
|
|
6
|
+
allow_guest_storefront_access!
|
|
6
7
|
rate_limit to: Spree::Api::Config[:rate_limit_register], within: Spree::Api::Config[:rate_limit_window].seconds, store: Rails.cache, only: :create, with: RATE_LIMIT_RESPONSE
|
|
7
8
|
|
|
8
9
|
skip_before_action :authenticate_user, only: [:create]
|
|
@@ -64,7 +65,8 @@ module Spree
|
|
|
64
65
|
locale: current_locale,
|
|
65
66
|
currency: current_currency,
|
|
66
67
|
user: current_user,
|
|
67
|
-
includes: []
|
|
68
|
+
includes: [],
|
|
69
|
+
hide_prices: hide_prices?
|
|
68
70
|
}
|
|
69
71
|
end
|
|
70
72
|
|
|
@@ -3,24 +3,19 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Store
|
|
5
5
|
class LocalesController < Store::BaseController
|
|
6
|
+
allow_guest_storefront_access!
|
|
6
7
|
include Spree::Api::V3::HttpCaching
|
|
7
8
|
|
|
8
9
|
# GET /api/v3/store/locales
|
|
9
10
|
def index
|
|
10
|
-
locales = current_store.
|
|
11
|
+
locales = current_store.supported_locales
|
|
11
12
|
|
|
12
13
|
return unless cache_collection(locales)
|
|
13
14
|
|
|
14
15
|
render json: {
|
|
15
|
-
data: locales.map { |
|
|
16
|
+
data: locales.map { |locale| Spree.api.locale_serializer.new(locale).to_h }
|
|
16
17
|
}
|
|
17
18
|
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def locale_name(code)
|
|
22
|
-
I18n.t('spree.i18n.this_file_language', locale: code, default: code)
|
|
23
|
-
end
|
|
24
19
|
end
|
|
25
20
|
end
|
|
26
21
|
end
|
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Store
|
|
5
5
|
class NewsletterSubscribersController < Store::BaseController
|
|
6
|
+
allow_guest_storefront_access!
|
|
6
7
|
rate_limit to: Spree::Api::Config[:rate_limit_register],
|
|
7
8
|
within: Spree::Api::Config[:rate_limit_window].seconds,
|
|
8
9
|
store: Rails.cache,
|
|
@@ -30,7 +30,7 @@ module Spree
|
|
|
30
30
|
|
|
31
31
|
def filters_cache_key
|
|
32
32
|
products_table = Spree::Product.table_name
|
|
33
|
-
stats = current_store.products.available(Time.current, current_currency)
|
|
33
|
+
stats = current_store.products.available(Time.current, current_currency, include_preorderable: true)
|
|
34
34
|
.pick(Arel.sql("MAX(#{products_table}.updated_at)"), Arel.sql("COUNT(DISTINCT #{products_table}.id)"))
|
|
35
35
|
max_updated = stats&.first&.to_i
|
|
36
36
|
product_count = stats&.last || 0
|
|
@@ -51,7 +51,10 @@ module Spree
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def filters_scope
|
|
54
|
-
scope
|
|
54
|
+
# Mirror ProductsController#scope so facet counts describe the
|
|
55
|
+
# same product set the listing returns (coming-soon pre-orders
|
|
56
|
+
# included).
|
|
57
|
+
scope = current_store.products.available(Time.current, current_currency, include_preorderable: true)
|
|
55
58
|
scope = scope.in_category(category) if category.present?
|
|
56
59
|
scope.accessible_by(current_ability, :show)
|
|
57
60
|
end
|
|
@@ -29,7 +29,7 @@ module Spree
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def scope
|
|
32
|
-
super.available(Time.current, Spree::Current.currency)
|
|
32
|
+
super.available(Time.current, Spree::Current.currency, include_preorderable: true)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# these scopes are not automatically picked by ar_lazy_preload gem and we need to explicitly include them
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Adds the opt-in per-locale translation matrix to an admin serializer
|
|
6
|
+
# for a model that includes +Spree::TranslatableResource+. Include it in
|
|
7
|
+
# every translatable resource's admin serializer (product, option type,
|
|
8
|
+
# category, …) so the +?expand=translations+ surface stays consistent
|
|
9
|
+
# and defined in one place.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# class ProductSerializer < V3::ProductSerializer
|
|
13
|
+
# include Spree::Api::V3::Admin::Translatable
|
|
14
|
+
# end
|
|
15
|
+
module Translatable
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
included do
|
|
19
|
+
# locale code => { field => value, translated_field_count } — mirrors
|
|
20
|
+
# the hand-written LocaleTranslations shape, inlined so the generated
|
|
21
|
+
# types need no import from the hand-written barrel.
|
|
22
|
+
typelize translations: 'Record<string, Record<string, string | number | null>>'
|
|
23
|
+
|
|
24
|
+
# Full per-locale translation matrix { locale => { field => value } }.
|
|
25
|
+
# Opt-in via ?expand=translations — the matrix (non-default locales ×
|
|
26
|
+
# translatable fields × translation-table joins) is wasteful on list
|
|
27
|
+
# views, so it is never rendered by default. params[:expand] is part
|
|
28
|
+
# of the HTTP cache key, so expanded/non-expanded responses cache
|
|
29
|
+
# separately. Writes go through the dedicated /translations endpoint.
|
|
30
|
+
attribute :translations, if: proc { expand?('translations') } do |record|
|
|
31
|
+
Spree::Translations.matrix_for(record)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -7,11 +7,17 @@ module Spree
|
|
|
7
7
|
first_name: [:string, nullable: true],
|
|
8
8
|
last_name: [:string, nullable: true],
|
|
9
9
|
full_name: [:string, nullable: true],
|
|
10
|
+
selected_locale: [:string, nullable: true],
|
|
11
|
+
avatar_url: [:string, nullable: true],
|
|
10
12
|
roles: 'Array<{ id: string; name: string }>'
|
|
11
13
|
|
|
12
|
-
attributes :email, :first_name, :last_name, :full_name,
|
|
14
|
+
attributes :email, :first_name, :last_name, :full_name, :selected_locale,
|
|
13
15
|
created_at: :iso8601, updated_at: :iso8601
|
|
14
16
|
|
|
17
|
+
attribute :avatar_url do |user|
|
|
18
|
+
image_url_for(user.avatar)
|
|
19
|
+
end
|
|
20
|
+
|
|
15
21
|
# Roles assigned to this user *for the current store*. Each store
|
|
16
22
|
# gets its own role set via `Spree::RoleUser`, so this attribute is
|
|
17
23
|
# scoped against `current_store` rather than returning every role
|
|
@@ -5,11 +5,13 @@ module Spree
|
|
|
5
5
|
# Admin API Category Serializer
|
|
6
6
|
# Full category data including admin-only fields
|
|
7
7
|
class CategorySerializer < V3::CategorySerializer
|
|
8
|
-
|
|
8
|
+
include Spree::Api::V3::Admin::Translatable
|
|
9
|
+
|
|
10
|
+
typelize pretty_name: :string, lft: :number, rgt: :number, products_count: :number,
|
|
9
11
|
metadata: 'Record<string, unknown>'
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
attributes :metadata, :pretty_name, :lft, :rgt, :products_count,
|
|
14
|
+
created_at: :iso8601, updated_at: :iso8601
|
|
13
15
|
|
|
14
16
|
# Override inherited associations to use admin serializers
|
|
15
17
|
one :parent,
|
|
@@ -4,9 +4,13 @@ module Spree
|
|
|
4
4
|
module Admin
|
|
5
5
|
class ChannelSerializer < V3::ChannelSerializer
|
|
6
6
|
typelize store_id: :string,
|
|
7
|
-
preferred_order_routing_strategy: [:string, nullable: true]
|
|
7
|
+
preferred_order_routing_strategy: [:string, nullable: true],
|
|
8
|
+
preferred_storefront_access: [:string, nullable: true],
|
|
9
|
+
preferred_guest_checkout: [:boolean, nullable: true]
|
|
8
10
|
|
|
9
11
|
attributes :preferred_order_routing_strategy,
|
|
12
|
+
:preferred_storefront_access,
|
|
13
|
+
:preferred_guest_checkout,
|
|
10
14
|
created_at: :iso8601, updated_at: :iso8601
|
|
11
15
|
end
|
|
12
16
|
end
|
|
@@ -17,7 +17,8 @@ module Spree
|
|
|
17
17
|
display_total_spent: :string,
|
|
18
18
|
last_order_completed_at: [:string, nullable: true],
|
|
19
19
|
default_billing_address_id: [:string, nullable: true],
|
|
20
|
-
default_shipping_address_id: [:string, nullable: true]
|
|
20
|
+
default_shipping_address_id: [:string, nullable: true],
|
|
21
|
+
customer_group_ids: [:string, multi: true]
|
|
21
22
|
|
|
22
23
|
# Admin-only attributes
|
|
23
24
|
attributes :login, :metadata,
|
|
@@ -41,7 +42,7 @@ module Spree
|
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
attribute :tags do |user|
|
|
44
|
-
user.
|
|
45
|
+
user.tags.map(&:name) # not pluck as we preload tags
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
attribute :internal_note_html do |user|
|
|
@@ -92,6 +93,10 @@ module Spree
|
|
|
92
93
|
resource: proc { Spree.api.admin_store_credit_serializer },
|
|
93
94
|
if: proc { expand?('store_credits') }
|
|
94
95
|
|
|
96
|
+
attribute :customer_group_ids do |user|
|
|
97
|
+
user.customer_groups.map(&:prefixed_id)
|
|
98
|
+
end
|
|
99
|
+
|
|
95
100
|
many :customer_groups,
|
|
96
101
|
resource: proc { Spree.api.admin_customer_group_serializer },
|
|
97
102
|
if: proc { expand?('customer_groups') }
|
|
@@ -3,6 +3,10 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Admin
|
|
5
5
|
class DiscountSerializer < V3::DiscountSerializer
|
|
6
|
+
# The Admin API has no guest gating — money fields inherited from the
|
|
7
|
+
# store serializer are always present, so override their nullability.
|
|
8
|
+
typelize amount: [:string, nullable: false], display_amount: [:string, nullable: false]
|
|
9
|
+
|
|
6
10
|
attributes created_at: :iso8601, updated_at: :iso8601
|
|
7
11
|
end
|
|
8
12
|
end
|