spree_api 5.4.3 → 5.5.0.rc2
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/Rakefile +36 -0
- data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
- data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +52 -0
- data/app/controllers/concerns/spree/api/v3/admin/subclassed_resource.rb +149 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +54 -0
- data/app/controllers/concerns/spree/api/v3/bulk_operations.rb +103 -0
- data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +60 -0
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
- data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +84 -0
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +104 -0
- data/app/controllers/concerns/spree/api/v3/store/search_provider_support.rb +35 -1
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +109 -0
- data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +84 -0
- data/app/controllers/spree/api/v3/admin/auth_controller.rb +134 -0
- data/app/controllers/spree/api/v3/admin/base_controller.rb +3 -17
- data/app/controllers/spree/api/v3/admin/categories_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/channels_controller.rb +65 -0
- data/app/controllers/spree/api/v3/admin/countries_controller.rb +38 -0
- data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/custom_field_definitions_controller.rb +34 -0
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +129 -0
- data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +83 -0
- data/app/controllers/spree/api/v3/admin/customers/base_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +92 -0
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +119 -0
- data/app/controllers/spree/api/v3/admin/dashboard_controller.rb +44 -0
- data/app/controllers/spree/api/v3/admin/direct_uploads_controller.rb +40 -0
- data/app/controllers/spree/api/v3/admin/exports_controller.rb +136 -0
- data/app/controllers/spree/api/v3/admin/gift_card_batches_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +138 -0
- data/app/controllers/spree/api/v3/admin/invitations_controller.rb +81 -0
- data/app/controllers/spree/api/v3/admin/markets_controller.rb +42 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +69 -0
- data/app/controllers/spree/api/v3/admin/media_controller.rb +119 -0
- data/app/controllers/spree/api/v3/admin/option_types_controller.rb +34 -0
- data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +27 -0
- data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +104 -0
- data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +79 -0
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +92 -0
- data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +90 -0
- data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +53 -0
- data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +59 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +190 -0
- data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +73 -0
- data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +179 -0
- data/app/controllers/spree/api/v3/admin/prices_controller.rb +157 -0
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +48 -0
- data/app/controllers/spree/api/v3/admin/products_controller.rb +237 -0
- data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +78 -0
- data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +56 -0
- data/app/controllers/spree/api/v3/admin/promotions_controller.rb +78 -0
- data/app/controllers/spree/api/v3/admin/resource_controller.rb +29 -11
- data/app/controllers/spree/api/v3/admin/roles_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +35 -0
- data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +36 -0
- data/app/controllers/spree/api/v3/admin/stock_reservations_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +75 -0
- data/app/controllers/spree/api/v3/admin/store_controller.rb +53 -0
- data/app/controllers/spree/api/v3/admin/store_credit_categories_controller.rb +21 -0
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +51 -0
- data/app/controllers/spree/api/v3/admin/tax_categories_controller.rb +21 -0
- data/app/controllers/spree/api/v3/admin/variants_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +49 -0
- data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +75 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +117 -8
- data/app/controllers/spree/api/v3/store/auth_controller.rb +8 -28
- data/app/controllers/spree/api/v3/store/base_controller.rb +6 -0
- data/app/controllers/spree/api/v3/store/carts_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/customers_controller.rb +6 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +77 -0
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +2 -2
- data/app/controllers/spree/api/v3/store/products_controller.rb +4 -3
- data/app/controllers/spree/api/v3/store/resource_controller.rb +10 -2
- data/app/jobs/spree/webhook_delivery_job.rb +5 -0
- data/app/models/spree/api_key_ability.rb +16 -0
- data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/adjustment_serializer.rb +3 -15
- data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +19 -3
- data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +42 -0
- data/app/serializers/spree/api/v3/admin/category_serializer.rb +4 -3
- data/app/serializers/spree/api/v3/admin/channel_serializer.rb +15 -0
- data/app/serializers/spree/api/v3/admin/country_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/coupon_code_serializer.rb +30 -0
- data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/custom_field_definition_serializer.rb +21 -0
- data/app/serializers/spree/api/v3/admin/custom_field_serializer.rb +8 -3
- data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/admin/customer_serializer.rb +58 -2
- data/app/serializers/spree/api/v3/admin/dashboard_analytics_serializer.rb +143 -0
- data/app/serializers/spree/api/v3/admin/export_serializer.rb +40 -0
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/{asset_serializer.rb → gift_card_batch_serializer.rb} +1 -1
- data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +39 -4
- data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +64 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +4 -16
- data/app/serializers/spree/api/v3/admin/media_serializer.rb +24 -2
- data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +21 -6
- data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +51 -0
- data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +55 -0
- data/app/serializers/spree/api/v3/admin/price_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/product_publication_serializer.rb +11 -0
- data/app/serializers/spree/api/v3/admin/product_serializer.rb +34 -10
- data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +71 -0
- data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +85 -0
- data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +41 -0
- data/app/serializers/spree/api/v3/admin/refund_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/role_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/admin/stock_item_serializer.rb +16 -1
- data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/admin/stock_reservation_serializer.rb +46 -0
- data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +37 -0
- data/app/serializers/spree/api/v3/admin/store_credit_category_serializer.rb +19 -0
- data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +11 -5
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +55 -0
- data/app/serializers/spree/api/v3/admin/tax_category_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +37 -6
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +45 -0
- data/app/serializers/spree/api/v3/admin/webhook_endpoint_serializer.rb +69 -0
- data/app/serializers/spree/api/v3/channel_serializer.rb +14 -0
- data/app/serializers/spree/api/v3/custom_field_serializer.rb +9 -10
- data/app/serializers/spree/api/v3/customer_serializer.rb +5 -0
- data/app/serializers/spree/api/v3/market_serializer.rb +2 -1
- data/app/serializers/spree/api/v3/media_serializer.rb +8 -6
- data/app/serializers/spree/api/v3/order_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/payment_method_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/product_publication_serializer.rb +22 -0
- data/app/serializers/spree/api/v3/product_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/stock_reservation_serializer.rb +10 -0
- data/config/locales/en.yml +2 -0
- data/config/routes.rb +235 -1
- data/lib/spree/api/configuration.rb +2 -2
- data/lib/spree/api/dependencies.rb +25 -1
- data/lib/spree/api/openapi/path_sorter.rb +126 -0
- data/lib/spree/api/openapi/schema_helper.rb +185 -6
- data/lib/spree/api/testing_support/v3/base.rb +28 -0
- metadata +98 -8
- data/app/serializers/spree/api/v3/admin/shipping_category_serializer.rb +0 -14
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class ProductsController < ResourceController
|
|
6
|
+
include Spree::Api::V3::BulkOperations
|
|
7
|
+
|
|
8
|
+
scoped_resource :products
|
|
9
|
+
|
|
10
|
+
before_action :require_ids!, only: [
|
|
11
|
+
:bulk_status_update,
|
|
12
|
+
:bulk_add_to_categories,
|
|
13
|
+
:bulk_remove_from_categories,
|
|
14
|
+
:bulk_add_to_channels,
|
|
15
|
+
:bulk_remove_from_channels,
|
|
16
|
+
:bulk_destroy
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# POST /api/v3/admin/products/:id/clone
|
|
20
|
+
def clone
|
|
21
|
+
@resource = find_resource
|
|
22
|
+
authorize!(:create, @resource)
|
|
23
|
+
|
|
24
|
+
result = @resource.duplicate
|
|
25
|
+
if result.success?
|
|
26
|
+
render json: serialize_resource(result.value), status: :created
|
|
27
|
+
else
|
|
28
|
+
render_service_error(result.error)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# POST /api/v3/admin/products/bulk_status_update
|
|
33
|
+
# Body: { ids: [...], status: 'draft' | 'active' | 'archived' }
|
|
34
|
+
def bulk_status_update
|
|
35
|
+
authorize! :update, model_class
|
|
36
|
+
|
|
37
|
+
unless Spree::Product::STATUSES.include?(params[:status].to_s)
|
|
38
|
+
return render_error(
|
|
39
|
+
code: 'invalid_status',
|
|
40
|
+
message: Spree.t(:invalid_status, scope: 'errors.messages', default: 'Invalid status'),
|
|
41
|
+
status: :unprocessable_content
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
count = bulk_collection.update_all(status: params[:status], updated_at: Time.current)
|
|
46
|
+
# `update_all` skips `after_commit`, so the search index won't refresh on its own.
|
|
47
|
+
bulk_collection.each(&:enqueue_search_index)
|
|
48
|
+
|
|
49
|
+
render json: { product_count: count, status: params[:status] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# POST /api/v3/admin/products/bulk_add_to_categories
|
|
53
|
+
# Body: { ids: [...], category_ids: [...] }
|
|
54
|
+
def bulk_add_to_categories
|
|
55
|
+
apply_categories(Spree::Taxons::AddProducts)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# POST /api/v3/admin/products/bulk_remove_from_categories
|
|
59
|
+
# Body: { ids: [...], category_ids: [...] }
|
|
60
|
+
def bulk_remove_from_categories
|
|
61
|
+
apply_categories(Spree::Taxons::RemoveProducts)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# POST /api/v3/admin/products/bulk_add_to_channels
|
|
65
|
+
# Body: { ids: [...], channel_ids: [...] }
|
|
66
|
+
def bulk_add_to_channels
|
|
67
|
+
authorize! :update, model_class
|
|
68
|
+
|
|
69
|
+
channels = scoped_channels
|
|
70
|
+
product_ids = bulk_collection.distinct.ids
|
|
71
|
+
channels.find_each { |channel| channel.add_products(product_ids) }
|
|
72
|
+
|
|
73
|
+
render json: { product_count: product_ids.size, channel_count: channels.size }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# POST /api/v3/admin/products/bulk_remove_from_channels
|
|
77
|
+
# Body: { ids: [...], channel_ids: [...] }
|
|
78
|
+
def bulk_remove_from_channels
|
|
79
|
+
authorize! :update, model_class
|
|
80
|
+
|
|
81
|
+
channels = scoped_channels
|
|
82
|
+
product_ids = bulk_collection.distinct.ids
|
|
83
|
+
removed = channels.sum { |channel| channel.remove_products(product_ids) }
|
|
84
|
+
|
|
85
|
+
render json: { product_count: product_ids.size, channel_count: channels.size, removed: removed }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# DELETE /api/v3/admin/products/bulk_destroy
|
|
89
|
+
# Body: { ids: [...] }
|
|
90
|
+
def bulk_destroy
|
|
91
|
+
authorize! :destroy, model_class
|
|
92
|
+
|
|
93
|
+
# Scope by `:destroy` rather than reusing `bulk_collection`
|
|
94
|
+
# (which is `:update`-scoped). Otherwise an admin with update
|
|
95
|
+
# rights but no destroy rights could soft-delete records.
|
|
96
|
+
destroy_scope = model_class.for_store(current_store)
|
|
97
|
+
.accessible_by(current_ability, :destroy)
|
|
98
|
+
.where(id: decode_ids(params[:ids]))
|
|
99
|
+
destroyed = destroy_scope.count(&:destroy)
|
|
100
|
+
|
|
101
|
+
render json: { product_count: destroyed }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
protected
|
|
105
|
+
|
|
106
|
+
def model_class
|
|
107
|
+
Spree::Product
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def serializer_class
|
|
111
|
+
Spree.api.admin_product_serializer
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def scope_includes
|
|
115
|
+
[
|
|
116
|
+
:tax_category,
|
|
117
|
+
product_publications: :channel,
|
|
118
|
+
primary_media: [attachment_attachment: :blob],
|
|
119
|
+
master: [:prices, stock_items: [:stock_location, :active_stock_reservations]],
|
|
120
|
+
variants: [:prices, stock_items: [:stock_location, :active_stock_reservations]]
|
|
121
|
+
]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Use SearchProvider::Database for collection to handle price/best_selling
|
|
125
|
+
# sorting correctly (counts before sorting, avoiding PG/Mobility issues).
|
|
126
|
+
def collection
|
|
127
|
+
return @collection if @collection.present?
|
|
128
|
+
|
|
129
|
+
filters = params[:q]&.to_unsafe_h || params[:q] || {}
|
|
130
|
+
# Decode Stripe-style prefixed IDs in `*_id_in`/`id_eq`/etc. so SPA
|
|
131
|
+
# filters can pass `prod_…` keys; the search provider expects raw
|
|
132
|
+
# IDs because it goes straight to Ransack on the underlying scope.
|
|
133
|
+
filters = decode_prefixed_id_predicates(filters)
|
|
134
|
+
# `q[search]` is the global text-search predicate; pass it through
|
|
135
|
+
# the provider's `query` arg so it invokes `Product.search` rather
|
|
136
|
+
# than being treated as a Ransack predicate (which gets stripped
|
|
137
|
+
# by the provider's filter sanitizer).
|
|
138
|
+
query = filters['search'] || filters[:search]
|
|
139
|
+
|
|
140
|
+
result = search_provider.search_and_filter(
|
|
141
|
+
scope: scope.includes(collection_includes).preload_associations_lazily.accessible_by(current_ability, :show),
|
|
142
|
+
query: query,
|
|
143
|
+
filters: filters,
|
|
144
|
+
sort: sort_param,
|
|
145
|
+
page: page,
|
|
146
|
+
limit: limit
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@pagy = result.pagy
|
|
150
|
+
@collection = result.products
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def permitted_params
|
|
154
|
+
# Product is purely a catalog grouping in API v3. All purchasable
|
|
155
|
+
# attributes (sku, barcode, price, weight, dimensions, stock,
|
|
156
|
+
# track_inventory) live on variants. See
|
|
157
|
+
# docs/plans/6.0-remove-master-variant.md.
|
|
158
|
+
#
|
|
159
|
+
# Top-level `prices` is a convenience for simple (no-options)
|
|
160
|
+
# products: the merchant doesn't need to know the master variant
|
|
161
|
+
# exists, so they ship prices alongside name/status and the
|
|
162
|
+
# `Spree::Product#prices=` setter forwards them to the master.
|
|
163
|
+
params.permit(
|
|
164
|
+
:name, :description, :slug, :status,
|
|
165
|
+
:meta_title, :meta_description, :meta_keywords,
|
|
166
|
+
:tax_category_id,
|
|
167
|
+
:promotionable, :digital,
|
|
168
|
+
tags: [],
|
|
169
|
+
category_ids: [],
|
|
170
|
+
metadata: {},
|
|
171
|
+
prices: [:amount, :compare_at_amount, :currency],
|
|
172
|
+
# Inline custom field values keyed by definition id. The model
|
|
173
|
+
# setter (`Spree::Metafields#custom_fields=`) validates each
|
|
174
|
+
# entry against its definition. We permit `value` as both a
|
|
175
|
+
# scalar AND `value: {}` (any-shape Hash/Array) — Strong
|
|
176
|
+
# Parameters merges them so JSON metafields can ship parsed
|
|
177
|
+
# objects while text/number/boolean ship scalars.
|
|
178
|
+
custom_fields: [:id, :custom_field_definition_id, :value, value: {}],
|
|
179
|
+
# Inline media. Entries with `id` patch an existing asset
|
|
180
|
+
# (alt, position, variant_ids). Entries with `signed_id` create
|
|
181
|
+
# + attach a fresh upload. Lets the dashboard ship media changes
|
|
182
|
+
# alongside the rest of the product form. See
|
|
183
|
+
# `Spree::Product#media=`.
|
|
184
|
+
media: [:id, :signed_id, :alt, :position, :type, variant_ids: []],
|
|
185
|
+
product_publications: [:id, :channel_id, :published_at, :unpublished_at],
|
|
186
|
+
variants: [
|
|
187
|
+
:id, :sku, :barcode,
|
|
188
|
+
:cost_price, :cost_currency,
|
|
189
|
+
:weight, :height, :width, :depth, :weight_unit, :dimensions_unit,
|
|
190
|
+
:track_inventory, :tax_category_id, :position,
|
|
191
|
+
options: [:name, :value],
|
|
192
|
+
prices: [:amount, :compare_at_amount, :currency],
|
|
193
|
+
stock_items: [:id, :stock_location_id, :count_on_hand, :backorderable]
|
|
194
|
+
]
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def search_provider
|
|
201
|
+
@search_provider ||= Spree::SearchProvider::Database.new(current_store)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Mirrors `Spree::Admin::ProductsController#after_bulk_tags_change`:
|
|
205
|
+
# tag changes can flip automatic-taxon matches, and `Tags::Bulk*`
|
|
206
|
+
# touch records via `touch_all` (which skips `after_commit`), so the
|
|
207
|
+
# search index needs an explicit kick.
|
|
208
|
+
def after_bulk_tags_change
|
|
209
|
+
Spree::Product.bulk_auto_match_taxons(current_store, bulk_collection.ids)
|
|
210
|
+
bulk_collection.each(&:enqueue_search_index)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def bulk_record_count_key
|
|
214
|
+
:product_count
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def apply_categories(service)
|
|
218
|
+
authorize! :update, model_class
|
|
219
|
+
|
|
220
|
+
category_ids = decode_ids(params[:category_ids])
|
|
221
|
+
categories = current_store.taxons.accessible_by(current_ability, :update).where(id: category_ids)
|
|
222
|
+
|
|
223
|
+
service.call(taxons: categories, products: bulk_collection)
|
|
224
|
+
Spree::Product.bulk_auto_match_taxons(current_store, bulk_collection.ids)
|
|
225
|
+
|
|
226
|
+
render json: { product_count: bulk_collection.size, category_count: categories.size }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def scoped_channels
|
|
230
|
+
channel_ids = decode_ids(params[:channel_ids])
|
|
231
|
+
current_store.channels.accessible_by(current_ability, :manage).where(id: channel_ids)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# CRUD for `Spree::PromotionAction` STI subclasses. Each action is
|
|
6
|
+
# nested under a promotion; the create body picks the subclass via
|
|
7
|
+
# `type` and the typed-preferences shape (`preferences: {...}`).
|
|
8
|
+
class PromotionActionsController < ResourceController
|
|
9
|
+
include Spree::Api::V3::Admin::SubclassedResource
|
|
10
|
+
|
|
11
|
+
scoped_resource :promotions
|
|
12
|
+
|
|
13
|
+
subclassed_via -> { Spree.promotions.actions },
|
|
14
|
+
unknown_type_error: 'unknown_promotion_action_type'
|
|
15
|
+
|
|
16
|
+
def types
|
|
17
|
+
authorize! :read, model_class
|
|
18
|
+
|
|
19
|
+
render json: { data: model_class.subclasses_with_preference_schema }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the calculator subclasses registered for the given action
|
|
23
|
+
# `type` (e.g. `?type=Spree::Promotion::Actions::CreateAdjustment`).
|
|
24
|
+
# Each entry includes the calculator's class name, label,
|
|
25
|
+
# description, and preference schema so the SPA can render the
|
|
26
|
+
# picker + nested fields without hardcoding the calculator list.
|
|
27
|
+
def calculators
|
|
28
|
+
authorize! :read, model_class
|
|
29
|
+
|
|
30
|
+
klass = resolve_subclass(params[:type])
|
|
31
|
+
return render_unknown_type unless klass && klass.respond_to?(:calculators)
|
|
32
|
+
|
|
33
|
+
data = klass.calculators.map do |calc|
|
|
34
|
+
{
|
|
35
|
+
type: calc.to_s,
|
|
36
|
+
label: calc.respond_to?(:description) ? calc.description : calc.to_s.demodulize.titleize,
|
|
37
|
+
preference_schema: calc.respond_to?(:serialized_preference_schema) ? calc.serialized_preference_schema : []
|
|
38
|
+
}
|
|
39
|
+
end.sort_by { |entry| entry[:label].to_s }
|
|
40
|
+
|
|
41
|
+
render json: { data: data }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
protected
|
|
45
|
+
|
|
46
|
+
def model_class
|
|
47
|
+
Spree::PromotionAction
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def serializer_class
|
|
51
|
+
Spree.api.admin_promotion_action_serializer
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def permitted_params
|
|
55
|
+
params.permit(:type, preferences: {})
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def set_parent
|
|
59
|
+
return if %w[types calculators].include?(action_name)
|
|
60
|
+
|
|
61
|
+
@parent = Spree::Promotion.accessible_by(current_ability, :show)
|
|
62
|
+
.find_by_prefix_id!(params[:promotion_id])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parent_association
|
|
66
|
+
:promotion_actions
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def build_subclassed_resource(klass, attrs)
|
|
72
|
+
klass.new(attrs.merge(promotion: @parent))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# CRUD for `Spree::PromotionRule` STI subclasses. Same shape as
|
|
6
|
+
# PromotionActionsController — only the registry differs
|
|
7
|
+
# (`Spree.promotions.rules` instead of `Spree.promotions.actions`).
|
|
8
|
+
class PromotionRulesController < ResourceController
|
|
9
|
+
include Spree::Api::V3::Admin::SubclassedResource
|
|
10
|
+
|
|
11
|
+
scoped_resource :promotions
|
|
12
|
+
|
|
13
|
+
subclassed_via -> { Spree.promotions.rules },
|
|
14
|
+
unknown_type_error: 'unknown_promotion_rule_type'
|
|
15
|
+
|
|
16
|
+
def types
|
|
17
|
+
authorize! :read, model_class
|
|
18
|
+
|
|
19
|
+
render json: { data: model_class.subclasses_with_preference_schema }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def model_class
|
|
25
|
+
Spree::PromotionRule
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def serializer_class
|
|
29
|
+
Spree.api.admin_promotion_rule_serializer
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def permitted_params
|
|
33
|
+
params.permit(:type, preferences: {})
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def set_parent
|
|
37
|
+
return if action_name == 'types'
|
|
38
|
+
|
|
39
|
+
@parent = Spree::Promotion.accessible_by(current_ability, :show)
|
|
40
|
+
.find_by_prefix_id!(params[:promotion_id])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parent_association
|
|
44
|
+
:promotion_rules
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_subclassed_resource(klass, attrs)
|
|
50
|
+
klass.new(attrs.merge(promotion: @parent))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class PromotionsController < ResourceController
|
|
6
|
+
scoped_resource :promotions
|
|
7
|
+
|
|
8
|
+
protected
|
|
9
|
+
|
|
10
|
+
def model_class
|
|
11
|
+
Spree::Promotion
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def serializer_class
|
|
15
|
+
Spree.api.admin_promotion_serializer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def collection_includes
|
|
19
|
+
promotion_includes
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def scope_includes
|
|
23
|
+
promotion_includes
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A single POST/PATCH /promotions can ship rules and actions
|
|
27
|
+
# alongside the basics; `Promotion#rules=` / `actions=` reconcile
|
|
28
|
+
# to the desired set. The nested allowlist below is the union of
|
|
29
|
+
# every built-in rule/action's expected keys. Plugin-defined
|
|
30
|
+
# subclasses can add to it — see `additional_permitted_attributes`
|
|
31
|
+
# below.
|
|
32
|
+
def permitted_params
|
|
33
|
+
normalize_params(params.permit(*permitted_attributes))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def permitted_attributes
|
|
37
|
+
[
|
|
38
|
+
:name, :description, :code, :path,
|
|
39
|
+
:starts_at, :expires_at, :usage_limit, :match_policy,
|
|
40
|
+
:kind, :multi_codes, :number_of_codes, :code_prefix,
|
|
41
|
+
:promotion_category_id,
|
|
42
|
+
rules: rule_attributes,
|
|
43
|
+
actions: action_attributes
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def rule_attributes
|
|
48
|
+
[:id, :type, { preferences: {} }, *subclassed_collection_attributes(Spree.promotions.rules)]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def action_attributes
|
|
52
|
+
[
|
|
53
|
+
:id, :type,
|
|
54
|
+
{ preferences: {} },
|
|
55
|
+
{ calculator: [:type, { preferences: {} }] },
|
|
56
|
+
*subclassed_collection_attributes(Spree.promotions.actions)
|
|
57
|
+
]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Pulls in plugin-defined permitted attributes from every
|
|
61
|
+
# registered rule/action subclass. Subclasses declare these via
|
|
62
|
+
# `additional_permitted_attributes` (e.g. `[product_ids: []]`).
|
|
63
|
+
def subclassed_collection_attributes(registry)
|
|
64
|
+
registry.flat_map do |klass|
|
|
65
|
+
klass.respond_to?(:additional_permitted_attributes) ? klass.additional_permitted_attributes : []
|
|
66
|
+
end.uniq
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def promotion_includes
|
|
72
|
+
[:stores, :promotion_actions, :promotion_rules]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -2,24 +2,42 @@ module Spree
|
|
|
2
2
|
module Api
|
|
3
3
|
module V3
|
|
4
4
|
module Admin
|
|
5
|
+
# Mirrors Admin::BaseController's concerns. Both classes anchor parallel
|
|
6
|
+
# inheritance branches (V3::BaseController vs V3::ResourceController);
|
|
7
|
+
# any concern added here MUST also be added to Admin::BaseController.
|
|
5
8
|
class ResourceController < Spree::Api::V3::ResourceController
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# Admin API responses must never be cached
|
|
10
|
-
after_action :set_no_store_cache
|
|
9
|
+
include Spree::Api::V3::AdminAuthentication
|
|
10
|
+
include Spree::Api::V3::ScopedAuthorization
|
|
11
11
|
|
|
12
12
|
protected
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
JWT_AUDIENCE_ADMIN
|
|
14
|
+
def authenticate_request!
|
|
15
|
+
authenticate_admin!
|
|
17
16
|
end
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
# Render error from ServiceModule::Result, extracting ActiveModel::Errors
|
|
19
|
+
# from the ResultError wrapper to get proper validation_error responses.
|
|
20
|
+
def render_result_error(result)
|
|
21
|
+
error = result.error
|
|
22
|
+
errors = error.respond_to?(:value) ? error.value : error
|
|
23
|
+
|
|
24
|
+
if errors.is_a?(ActiveModel::Errors)
|
|
25
|
+
render_validation_error(errors)
|
|
26
|
+
else
|
|
27
|
+
render_service_error(error)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def decode_ids(ids, klass)
|
|
32
|
+
Array(ids).map do |id|
|
|
33
|
+
Spree::PrefixedId.prefixed_id?(id) ? klass.find_by_param!(id).id : id
|
|
34
|
+
end
|
|
35
|
+
end
|
|
20
36
|
|
|
21
|
-
def
|
|
22
|
-
|
|
37
|
+
def decode_prefixed_ids(ids)
|
|
38
|
+
Array(ids).map do |id|
|
|
39
|
+
Spree::PrefixedId.prefixed_id?(id) ? Spree::PrefixedId.decode_prefixed_id(id) : id
|
|
40
|
+
end
|
|
23
41
|
end
|
|
24
42
|
end
|
|
25
43
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Read-only list of roles available for staff role pickers (invite +
|
|
6
|
+
# edit forms). Roles are global, not per-store; CRUD is handled
|
|
7
|
+
# outside the SPA today and may grow into a richer permissions UI
|
|
8
|
+
# later. The controller ignores `current_store` for that reason.
|
|
9
|
+
class RolesController < ResourceController
|
|
10
|
+
scoped_resource :settings
|
|
11
|
+
|
|
12
|
+
protected
|
|
13
|
+
|
|
14
|
+
def model_class
|
|
15
|
+
Spree::Role
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def serializer_class
|
|
19
|
+
Spree.api.admin_role_serializer
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def scope
|
|
23
|
+
Spree::Role.accessible_by(current_ability, :show)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Stock items are auto-created when a variant lands at a stock
|
|
6
|
+
# location, so there's deliberately no `create` route — use the
|
|
7
|
+
# variants / stock-locations endpoints for that flow.
|
|
8
|
+
class StockItemsController < ResourceController
|
|
9
|
+
scoped_resource :stock
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def model_class
|
|
14
|
+
Spree::StockItem
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def serializer_class
|
|
18
|
+
Spree.api.admin_stock_item_serializer
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def collection_includes
|
|
22
|
+
[:stock_location, :variant]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# `StockItem.for_store` already applies its own `distinct`, and
|
|
26
|
+
# `id`-asc gives a stable order across edits (variant.position
|
|
27
|
+
# alone isn't unique — see git blame for the row-jumping bug).
|
|
28
|
+
def apply_collection_sort(collection)
|
|
29
|
+
collection.order(Spree::StockItem.arel_table[:id].asc)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class StockLocationsController < ResourceController
|
|
6
|
+
scoped_resource :stock
|
|
7
|
+
|
|
8
|
+
protected
|
|
9
|
+
|
|
10
|
+
def model_class
|
|
11
|
+
Spree::StockLocation
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def serializer_class
|
|
15
|
+
Spree.api.admin_stock_location_serializer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def scope
|
|
19
|
+
super.order_default
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def permitted_params
|
|
23
|
+
params.permit(
|
|
24
|
+
:name, :admin_name, :active, :default,
|
|
25
|
+
:kind, :propagate_all_variants, :backorderable_default,
|
|
26
|
+
:address1, :address2, :city, :zipcode, :phone, :company,
|
|
27
|
+
:country_iso, :state_abbr, :state_name,
|
|
28
|
+
:pickup_enabled, :pickup_stock_policy,
|
|
29
|
+
:pickup_ready_in_minutes, :pickup_instructions
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class StockReservationsController < ResourceController
|
|
6
|
+
scoped_resource :stock
|
|
7
|
+
|
|
8
|
+
protected
|
|
9
|
+
|
|
10
|
+
def model_class
|
|
11
|
+
Spree::StockReservation
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def serializer_class
|
|
15
|
+
Spree.api.admin_stock_reservation_serializer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def scope
|
|
19
|
+
Spree::StockReservation.for_store(current_store).accessible_by(current_ability, ability_action_for_request)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def collection_includes
|
|
23
|
+
[{ stock_item: [:variant, :stock_location], line_item: [], order: [] }]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|