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.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +36 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
  4. data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +52 -0
  5. data/app/controllers/concerns/spree/api/v3/admin/subclassed_resource.rb +149 -0
  6. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +54 -0
  7. data/app/controllers/concerns/spree/api/v3/bulk_operations.rb +103 -0
  8. data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +60 -0
  9. data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
  10. data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +84 -0
  11. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +104 -0
  12. data/app/controllers/concerns/spree/api/v3/store/search_provider_support.rb +35 -1
  13. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +109 -0
  14. data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +25 -0
  15. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +84 -0
  16. data/app/controllers/spree/api/v3/admin/auth_controller.rb +134 -0
  17. data/app/controllers/spree/api/v3/admin/base_controller.rb +3 -17
  18. data/app/controllers/spree/api/v3/admin/categories_controller.rb +25 -0
  19. data/app/controllers/spree/api/v3/admin/channels_controller.rb +65 -0
  20. data/app/controllers/spree/api/v3/admin/countries_controller.rb +38 -0
  21. data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +33 -0
  22. data/app/controllers/spree/api/v3/admin/custom_field_definitions_controller.rb +34 -0
  23. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +129 -0
  24. data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +31 -0
  25. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +83 -0
  26. data/app/controllers/spree/api/v3/admin/customers/base_controller.rb +33 -0
  27. data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +25 -0
  28. data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +92 -0
  29. data/app/controllers/spree/api/v3/admin/customers_controller.rb +119 -0
  30. data/app/controllers/spree/api/v3/admin/dashboard_controller.rb +44 -0
  31. data/app/controllers/spree/api/v3/admin/direct_uploads_controller.rb +40 -0
  32. data/app/controllers/spree/api/v3/admin/exports_controller.rb +136 -0
  33. data/app/controllers/spree/api/v3/admin/gift_card_batches_controller.rb +31 -0
  34. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +33 -0
  35. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +138 -0
  36. data/app/controllers/spree/api/v3/admin/invitations_controller.rb +81 -0
  37. data/app/controllers/spree/api/v3/admin/markets_controller.rb +42 -0
  38. data/app/controllers/spree/api/v3/admin/me_controller.rb +69 -0
  39. data/app/controllers/spree/api/v3/admin/media_controller.rb +119 -0
  40. data/app/controllers/spree/api/v3/admin/option_types_controller.rb +34 -0
  41. data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +27 -0
  42. data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +31 -0
  43. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +104 -0
  44. data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +79 -0
  45. data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +92 -0
  46. data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +90 -0
  47. data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +53 -0
  48. data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +59 -0
  49. data/app/controllers/spree/api/v3/admin/orders_controller.rb +190 -0
  50. data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +73 -0
  51. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +179 -0
  52. data/app/controllers/spree/api/v3/admin/prices_controller.rb +157 -0
  53. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +48 -0
  54. data/app/controllers/spree/api/v3/admin/products_controller.rb +237 -0
  55. data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +78 -0
  56. data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +56 -0
  57. data/app/controllers/spree/api/v3/admin/promotions_controller.rb +78 -0
  58. data/app/controllers/spree/api/v3/admin/resource_controller.rb +29 -11
  59. data/app/controllers/spree/api/v3/admin/roles_controller.rb +29 -0
  60. data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +35 -0
  61. data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +36 -0
  62. data/app/controllers/spree/api/v3/admin/stock_reservations_controller.rb +29 -0
  63. data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +75 -0
  64. data/app/controllers/spree/api/v3/admin/store_controller.rb +53 -0
  65. data/app/controllers/spree/api/v3/admin/store_credit_categories_controller.rb +21 -0
  66. data/app/controllers/spree/api/v3/admin/tags_controller.rb +51 -0
  67. data/app/controllers/spree/api/v3/admin/tax_categories_controller.rb +21 -0
  68. data/app/controllers/spree/api/v3/admin/variants_controller.rb +33 -0
  69. data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +49 -0
  70. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +75 -0
  71. data/app/controllers/spree/api/v3/resource_controller.rb +117 -8
  72. data/app/controllers/spree/api/v3/store/auth_controller.rb +8 -28
  73. data/app/controllers/spree/api/v3/store/base_controller.rb +6 -0
  74. data/app/controllers/spree/api/v3/store/carts_controller.rb +1 -0
  75. data/app/controllers/spree/api/v3/store/customers_controller.rb +6 -0
  76. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +77 -0
  77. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +2 -2
  78. data/app/controllers/spree/api/v3/store/products_controller.rb +4 -3
  79. data/app/controllers/spree/api/v3/store/resource_controller.rb +10 -2
  80. data/app/jobs/spree/webhook_delivery_job.rb +5 -0
  81. data/app/models/spree/api_key_ability.rb +16 -0
  82. data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -6
  83. data/app/serializers/spree/api/v3/admin/adjustment_serializer.rb +3 -15
  84. data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +19 -3
  85. data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +2 -6
  86. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +42 -0
  87. data/app/serializers/spree/api/v3/admin/category_serializer.rb +4 -3
  88. data/app/serializers/spree/api/v3/admin/channel_serializer.rb +15 -0
  89. data/app/serializers/spree/api/v3/admin/country_serializer.rb +1 -1
  90. data/app/serializers/spree/api/v3/admin/coupon_code_serializer.rb +30 -0
  91. data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +4 -2
  92. data/app/serializers/spree/api/v3/admin/custom_field_definition_serializer.rb +21 -0
  93. data/app/serializers/spree/api/v3/admin/custom_field_serializer.rb +8 -3
  94. data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +27 -0
  95. data/app/serializers/spree/api/v3/admin/customer_serializer.rb +58 -2
  96. data/app/serializers/spree/api/v3/admin/dashboard_analytics_serializer.rb +143 -0
  97. data/app/serializers/spree/api/v3/admin/export_serializer.rb +40 -0
  98. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -6
  99. data/app/serializers/spree/api/v3/admin/{asset_serializer.rb → gift_card_batch_serializer.rb} +1 -1
  100. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +39 -4
  101. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +64 -0
  102. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +4 -16
  103. data/app/serializers/spree/api/v3/admin/media_serializer.rb +24 -2
  104. data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +4 -1
  105. data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +4 -1
  106. data/app/serializers/spree/api/v3/admin/order_serializer.rb +21 -6
  107. data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +11 -2
  108. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +2 -6
  109. data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +4 -1
  110. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +51 -0
  111. data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +55 -0
  112. data/app/serializers/spree/api/v3/admin/price_serializer.rb +4 -0
  113. data/app/serializers/spree/api/v3/admin/product_publication_serializer.rb +11 -0
  114. data/app/serializers/spree/api/v3/admin/product_serializer.rb +34 -10
  115. data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +71 -0
  116. data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +85 -0
  117. data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +41 -0
  118. data/app/serializers/spree/api/v3/admin/refund_serializer.rb +4 -2
  119. data/app/serializers/spree/api/v3/admin/role_serializer.rb +17 -0
  120. data/app/serializers/spree/api/v3/admin/stock_item_serializer.rb +16 -1
  121. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +11 -2
  122. data/app/serializers/spree/api/v3/admin/stock_reservation_serializer.rb +46 -0
  123. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +37 -0
  124. data/app/serializers/spree/api/v3/admin/store_credit_category_serializer.rb +19 -0
  125. data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +11 -5
  126. data/app/serializers/spree/api/v3/admin/store_serializer.rb +55 -0
  127. data/app/serializers/spree/api/v3/admin/tax_category_serializer.rb +4 -2
  128. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +37 -6
  129. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +45 -0
  130. data/app/serializers/spree/api/v3/admin/webhook_endpoint_serializer.rb +69 -0
  131. data/app/serializers/spree/api/v3/channel_serializer.rb +14 -0
  132. data/app/serializers/spree/api/v3/custom_field_serializer.rb +9 -10
  133. data/app/serializers/spree/api/v3/customer_serializer.rb +5 -0
  134. data/app/serializers/spree/api/v3/market_serializer.rb +2 -1
  135. data/app/serializers/spree/api/v3/media_serializer.rb +8 -6
  136. data/app/serializers/spree/api/v3/order_serializer.rb +6 -1
  137. data/app/serializers/spree/api/v3/payment_method_serializer.rb +11 -2
  138. data/app/serializers/spree/api/v3/product_publication_serializer.rb +22 -0
  139. data/app/serializers/spree/api/v3/product_serializer.rb +6 -1
  140. data/app/serializers/spree/api/v3/stock_reservation_serializer.rb +10 -0
  141. data/config/locales/en.yml +2 -0
  142. data/config/routes.rb +235 -1
  143. data/lib/spree/api/configuration.rb +2 -2
  144. data/lib/spree/api/dependencies.rb +25 -1
  145. data/lib/spree/api/openapi/path_sorter.rb +126 -0
  146. data/lib/spree/api/openapi/schema_helper.rb +185 -6
  147. data/lib/spree/api/testing_support/v3/base.rb +28 -0
  148. metadata +98 -8
  149. 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
- # Require secret API key for all Admin API requests
7
- before_action :authenticate_secret_key!
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
- # Override JWT audience to require admin tokens
15
- def expected_audience
16
- JWT_AUDIENCE_ADMIN
14
+ def authenticate_request!
15
+ authenticate_admin!
17
16
  end
18
17
 
19
- private
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 set_no_store_cache
22
- response.headers['Cache-Control'] = 'private, no-store'
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