spree_api 5.4.3 → 5.5.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.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +19 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
  4. data/app/controllers/concerns/spree/api/v3/admin/subclassed_resource.rb +149 -0
  5. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +54 -0
  6. data/app/controllers/concerns/spree/api/v3/bulk_operations.rb +103 -0
  7. data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +60 -0
  8. data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
  9. data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +84 -0
  10. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +88 -0
  11. data/app/controllers/concerns/spree/api/v3/store/search_provider_support.rb +35 -1
  12. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +97 -0
  13. data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +25 -0
  14. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +55 -0
  15. data/app/controllers/spree/api/v3/admin/auth_controller.rb +134 -0
  16. data/app/controllers/spree/api/v3/admin/base_controller.rb +3 -17
  17. data/app/controllers/spree/api/v3/admin/categories_controller.rb +25 -0
  18. data/app/controllers/spree/api/v3/admin/channels_controller.rb +65 -0
  19. data/app/controllers/spree/api/v3/admin/countries_controller.rb +38 -0
  20. data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +33 -0
  21. data/app/controllers/spree/api/v3/admin/custom_field_definitions_controller.rb +34 -0
  22. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +108 -0
  23. data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +31 -0
  24. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +88 -0
  25. data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +31 -0
  26. data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +93 -0
  27. data/app/controllers/spree/api/v3/admin/customers_controller.rb +119 -0
  28. data/app/controllers/spree/api/v3/admin/dashboard_controller.rb +44 -0
  29. data/app/controllers/spree/api/v3/admin/direct_uploads_controller.rb +40 -0
  30. data/app/controllers/spree/api/v3/admin/exports_controller.rb +89 -0
  31. data/app/controllers/spree/api/v3/admin/gift_card_batches_controller.rb +31 -0
  32. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +33 -0
  33. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +138 -0
  34. data/app/controllers/spree/api/v3/admin/invitations_controller.rb +70 -0
  35. data/app/controllers/spree/api/v3/admin/markets_controller.rb +42 -0
  36. data/app/controllers/spree/api/v3/admin/me_controller.rb +69 -0
  37. data/app/controllers/spree/api/v3/admin/media_controller.rb +119 -0
  38. data/app/controllers/spree/api/v3/admin/option_types_controller.rb +34 -0
  39. data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +27 -0
  40. data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +26 -0
  41. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +104 -0
  42. data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +79 -0
  43. data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +92 -0
  44. data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +90 -0
  45. data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +53 -0
  46. data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +59 -0
  47. data/app/controllers/spree/api/v3/admin/orders_controller.rb +190 -0
  48. data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +73 -0
  49. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +156 -0
  50. data/app/controllers/spree/api/v3/admin/prices_controller.rb +129 -0
  51. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +48 -0
  52. data/app/controllers/spree/api/v3/admin/products_controller.rb +237 -0
  53. data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +78 -0
  54. data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +56 -0
  55. data/app/controllers/spree/api/v3/admin/promotions_controller.rb +78 -0
  56. data/app/controllers/spree/api/v3/admin/resource_controller.rb +29 -11
  57. data/app/controllers/spree/api/v3/admin/roles_controller.rb +29 -0
  58. data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +35 -0
  59. data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +36 -0
  60. data/app/controllers/spree/api/v3/admin/stock_reservations_controller.rb +29 -0
  61. data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +75 -0
  62. data/app/controllers/spree/api/v3/admin/store_controller.rb +53 -0
  63. data/app/controllers/spree/api/v3/admin/store_credit_categories_controller.rb +21 -0
  64. data/app/controllers/spree/api/v3/admin/tags_controller.rb +51 -0
  65. data/app/controllers/spree/api/v3/admin/tax_categories_controller.rb +21 -0
  66. data/app/controllers/spree/api/v3/admin/variants_controller.rb +33 -0
  67. data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +49 -0
  68. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +75 -0
  69. data/app/controllers/spree/api/v3/resource_controller.rb +90 -8
  70. data/app/controllers/spree/api/v3/store/auth_controller.rb +8 -28
  71. data/app/controllers/spree/api/v3/store/base_controller.rb +6 -0
  72. data/app/controllers/spree/api/v3/store/carts_controller.rb +1 -0
  73. data/app/controllers/spree/api/v3/store/customers_controller.rb +6 -0
  74. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +77 -0
  75. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +2 -2
  76. data/app/controllers/spree/api/v3/store/products_controller.rb +3 -3
  77. data/app/controllers/spree/api/v3/store/resource_controller.rb +10 -2
  78. data/app/jobs/spree/webhook_delivery_job.rb +5 -0
  79. data/app/models/spree/api_key_ability.rb +16 -0
  80. data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -6
  81. data/app/serializers/spree/api/v3/admin/adjustment_serializer.rb +3 -15
  82. data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +19 -3
  83. data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +2 -6
  84. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +42 -0
  85. data/app/serializers/spree/api/v3/admin/category_serializer.rb +4 -3
  86. data/app/serializers/spree/api/v3/admin/channel_serializer.rb +15 -0
  87. data/app/serializers/spree/api/v3/admin/country_serializer.rb +1 -1
  88. data/app/serializers/spree/api/v3/admin/coupon_code_serializer.rb +30 -0
  89. data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +4 -2
  90. data/app/serializers/spree/api/v3/admin/custom_field_definition_serializer.rb +21 -0
  91. data/app/serializers/spree/api/v3/admin/custom_field_serializer.rb +8 -3
  92. data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +27 -0
  93. data/app/serializers/spree/api/v3/admin/customer_serializer.rb +58 -2
  94. data/app/serializers/spree/api/v3/admin/dashboard_analytics_serializer.rb +143 -0
  95. data/app/serializers/spree/api/v3/admin/export_serializer.rb +40 -0
  96. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -6
  97. data/app/serializers/spree/api/v3/admin/{asset_serializer.rb → gift_card_batch_serializer.rb} +1 -1
  98. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +39 -4
  99. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +64 -0
  100. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +4 -16
  101. data/app/serializers/spree/api/v3/admin/media_serializer.rb +24 -2
  102. data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +4 -1
  103. data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +4 -1
  104. data/app/serializers/spree/api/v3/admin/order_serializer.rb +21 -6
  105. data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +11 -2
  106. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +2 -6
  107. data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +4 -1
  108. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +51 -0
  109. data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +55 -0
  110. data/app/serializers/spree/api/v3/admin/price_serializer.rb +4 -0
  111. data/app/serializers/spree/api/v3/admin/product_publication_serializer.rb +11 -0
  112. data/app/serializers/spree/api/v3/admin/product_serializer.rb +34 -10
  113. data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +71 -0
  114. data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +85 -0
  115. data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +41 -0
  116. data/app/serializers/spree/api/v3/admin/refund_serializer.rb +4 -2
  117. data/app/serializers/spree/api/v3/admin/role_serializer.rb +17 -0
  118. data/app/serializers/spree/api/v3/admin/stock_item_serializer.rb +16 -1
  119. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +11 -2
  120. data/app/serializers/spree/api/v3/admin/stock_reservation_serializer.rb +46 -0
  121. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +37 -0
  122. data/app/serializers/spree/api/v3/admin/store_credit_category_serializer.rb +19 -0
  123. data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +11 -5
  124. data/app/serializers/spree/api/v3/admin/store_serializer.rb +55 -0
  125. data/app/serializers/spree/api/v3/admin/tax_category_serializer.rb +4 -2
  126. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +37 -6
  127. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +45 -0
  128. data/app/serializers/spree/api/v3/admin/webhook_endpoint_serializer.rb +69 -0
  129. data/app/serializers/spree/api/v3/channel_serializer.rb +14 -0
  130. data/app/serializers/spree/api/v3/custom_field_serializer.rb +9 -10
  131. data/app/serializers/spree/api/v3/customer_serializer.rb +5 -0
  132. data/app/serializers/spree/api/v3/market_serializer.rb +2 -1
  133. data/app/serializers/spree/api/v3/media_serializer.rb +8 -6
  134. data/app/serializers/spree/api/v3/order_serializer.rb +6 -1
  135. data/app/serializers/spree/api/v3/payment_method_serializer.rb +11 -2
  136. data/app/serializers/spree/api/v3/product_publication_serializer.rb +22 -0
  137. data/app/serializers/spree/api/v3/product_serializer.rb +6 -1
  138. data/app/serializers/spree/api/v3/stock_reservation_serializer.rb +10 -0
  139. data/config/locales/en.yml +2 -0
  140. data/config/routes.rb +235 -1
  141. data/lib/spree/api/configuration.rb +2 -2
  142. data/lib/spree/api/dependencies.rb +25 -1
  143. data/lib/spree/api/openapi/path_sorter.rb +126 -0
  144. data/lib/spree/api/openapi/schema_helper.rb +185 -6
  145. metadata +96 -8
  146. data/app/serializers/spree/api/v3/admin/shipping_category_serializer.rb +0 -14
@@ -0,0 +1,108 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Custom field values for any parent that includes the custom-fields
6
+ # concern. Mounted via the `:custom_fieldable` route concern; the parent
7
+ # class is inferred from whichever `<segment>_id` route param matches a
8
+ # registered owner.
9
+ class CustomFieldsController < ResourceController
10
+ # POST /api/v3/admin/<parent>/<parent_id>/custom_fields
11
+ def create
12
+ @resource = @parent.metafields.new(permitted_params)
13
+ authorize_resource!(@resource, :create)
14
+
15
+ if @resource.save
16
+ render json: serialize_resource(@resource), status: :created
17
+ else
18
+ render_validation_error(@resource.errors)
19
+ end
20
+ end
21
+
22
+ # PATCH /api/v3/admin/<parent>/<parent_id>/custom_fields/:id
23
+ # Only `value` is mutable. Switching the linked definition is a
24
+ # delete-and-create — the model rejects it (definition + resource
25
+ # uniqueness) and the type wouldn't match.
26
+ def update
27
+ authorize_resource!(@resource)
28
+
29
+ if @resource.update(update_permitted_params)
30
+ render json: serialize_resource(@resource)
31
+ else
32
+ render_validation_error(@resource.errors)
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def model_class
39
+ Spree::CustomField
40
+ end
41
+
42
+ def serializer_class
43
+ Spree.api.admin_custom_field_serializer
44
+ end
45
+
46
+ def parent_association
47
+ :metafields
48
+ end
49
+
50
+ def set_parent
51
+ # Routes always mount this controller under a recognized parent, so
52
+ # `parent_lookup` matches in normal flows. The explicit raise is a
53
+ # defensive guard against a future route nesting that doesn't.
54
+ raise ActiveRecord::RecordNotFound, 'Parent resource not found' unless parent_lookup
55
+
56
+ @parent = parent_lookup.klass.find_by_prefix_id!(parent_lookup.value)
57
+ end
58
+
59
+ # Per-parent scope check: a key holding `write_products` may write a
60
+ # product's custom fields, `write_orders` may write an order's, etc.
61
+ # Resolves the parent at request time rather than via the static
62
+ # `scoped_resource` declaration.
63
+ def scoped_resource_name
64
+ parent_lookup&.segment&.pluralize&.to_sym
65
+ end
66
+
67
+ # `custom_field_definition_id` is an alias_attribute on Spree::CustomField;
68
+ # AR resolves the prefixed-ID and the alias to the canonical FK on assign.
69
+ def permitted_params
70
+ params.permit(:custom_field_definition_id, :value)
71
+ end
72
+
73
+ def update_permitted_params
74
+ params.permit(:value)
75
+ end
76
+
77
+ private
78
+
79
+ ParentLookup = Struct.new(:klass, :value, :segment)
80
+
81
+ # Stores class names (not class objects) so the map survives dev-mode
82
+ # code reloads — `enabled_resources` is captured at boot and its
83
+ # class references go stale. Aliases `'customer'` because the route
84
+ # uses `customer_id` while user_class.model_name.element is `'user'`.
85
+ def parent_route_map
86
+ @parent_route_map ||= Spree.metafields.enabled_resources.each_with_object({}) do |klass, m|
87
+ m[klass.model_name.element.to_s] = klass.name
88
+ end.merge('customer' => Spree.user_class.name)
89
+ end
90
+
91
+ # Returns the first segment whose `<segment>_id` is present in params,
92
+ # paired with its class and the raw id value, or nil. Memoized — read
93
+ # by both `set_parent` and `scoped_resource_name`.
94
+ def parent_lookup
95
+ return @parent_lookup if defined?(@parent_lookup)
96
+
97
+ match = parent_route_map.find { |segment, _| params[:"#{segment}_id"].present? }
98
+ @parent_lookup =
99
+ if match
100
+ segment, klass_name = match
101
+ ParentLookup.new(klass_name.constantize, params[:"#{segment}_id"], segment)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,31 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin CRUD for `Spree::CustomerGroup`. Scoped to the current
6
+ # store so groups from sibling stores don't leak into pickers.
7
+ class CustomerGroupsController < ResourceController
8
+ scoped_resource :customers
9
+
10
+ protected
11
+
12
+ def model_class
13
+ Spree::CustomerGroup
14
+ end
15
+
16
+ def serializer_class
17
+ Spree.api.admin_customer_group_serializer
18
+ end
19
+
20
+ def scope
21
+ super.for_store(current_store)
22
+ end
23
+
24
+ def permitted_params
25
+ params.permit(:name, :description, customer_ids: [])
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,88 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ module Customers
6
+ class AddressesController < ResourceController
7
+ scoped_resource :customers
8
+
9
+ # POST /api/v3/admin/customers/:customer_id/addresses
10
+ def create
11
+ @resource = @parent.addresses.new(address_attrs)
12
+ authorize_resource!(@resource, :create)
13
+
14
+ ApplicationRecord.transaction do
15
+ if @resource.save
16
+ apply_default_flags(@resource)
17
+ render json: serialize_resource(@resource.reload), status: :created
18
+ else
19
+ render_validation_error(@resource.errors)
20
+ raise ActiveRecord::Rollback
21
+ end
22
+ end
23
+ end
24
+
25
+ # PATCH /api/v3/admin/customers/:customer_id/addresses/:id
26
+ def update
27
+ authorize_resource!(@resource)
28
+
29
+ ApplicationRecord.transaction do
30
+ if @resource.update(address_attrs)
31
+ apply_default_flags(@resource)
32
+ render json: serialize_resource(@resource.reload)
33
+ else
34
+ render_validation_error(@resource.errors)
35
+ raise ActiveRecord::Rollback
36
+ end
37
+ end
38
+ end
39
+
40
+ # DELETE /api/v3/admin/customers/:customer_id/addresses/:id
41
+ def destroy
42
+ authorize_resource!(@resource)
43
+ @resource.destroy
44
+ head :no_content
45
+ end
46
+
47
+ protected
48
+
49
+ def set_parent
50
+ @parent = Spree.user_class.find_by_prefix_id!(params[:customer_id])
51
+ end
52
+
53
+ def parent_association
54
+ :addresses
55
+ end
56
+
57
+ def model_class
58
+ Spree::Address
59
+ end
60
+
61
+ def serializer_class
62
+ Spree.api.admin_address_serializer
63
+ end
64
+
65
+ private
66
+
67
+ def address_attrs
68
+ params.permit(
69
+ :firstname, :lastname, :first_name, :last_name,
70
+ :address1, :address2, :city,
71
+ :country_iso, :state_abbr, :country_id, :state_id,
72
+ :zipcode, :postal_code, :phone, :alternative_phone,
73
+ :state_name, :company, :label, :quick_checkout
74
+ )
75
+ end
76
+
77
+ def apply_default_flags(address)
78
+ updates = {}
79
+ updates[:bill_address_id] = address.id if ActiveModel::Type::Boolean.new.cast(params[:is_default_billing])
80
+ updates[:ship_address_id] = address.id if ActiveModel::Type::Boolean.new.cast(params[:is_default_shipping])
81
+ @parent.update!(updates) if updates.any?
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,31 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ module Customers
6
+ class CreditCardsController < ResourceController
7
+ scoped_resource :customers
8
+
9
+ protected
10
+
11
+ def set_parent
12
+ @parent = Spree.user_class.find_by_prefix_id!(params[:customer_id])
13
+ end
14
+
15
+ def parent_association
16
+ :credit_cards
17
+ end
18
+
19
+ def model_class
20
+ Spree::CreditCard
21
+ end
22
+
23
+ def serializer_class
24
+ Spree.api.admin_credit_card_serializer
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,93 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ module Customers
6
+ class StoreCreditsController < ResourceController
7
+ scoped_resource :store_credits
8
+
9
+ # POST /api/v3/admin/customers/:customer_id/store_credits
10
+ def create
11
+ @resource = @parent.store_credits.new(create_attrs)
12
+ @resource.created_by = try_spree_current_user
13
+ @resource.store ||= current_store
14
+ @resource.category ||= Spree::StoreCreditCategory.first
15
+ authorize_resource!(@resource, :create)
16
+
17
+ if @resource.save
18
+ render json: serialize_resource(@resource), status: :created
19
+ else
20
+ render_validation_error(@resource.errors)
21
+ end
22
+ end
23
+
24
+ # PATCH /api/v3/admin/customers/:customer_id/store_credits/:id
25
+ def update
26
+ authorize_resource!(@resource)
27
+
28
+ if @resource.amount_used.positive? && update_attrs.key?(:amount)
29
+ render_error(
30
+ code: 'store_credit_in_use',
31
+ message: 'Cannot change amount on a store credit that has already been used',
32
+ status: :unprocessable_content
33
+ )
34
+ return
35
+ end
36
+
37
+ if @resource.update(update_attrs)
38
+ render json: serialize_resource(@resource.reload)
39
+ else
40
+ render_validation_error(@resource.errors)
41
+ end
42
+ end
43
+
44
+ # DELETE /api/v3/admin/customers/:customer_id/store_credits/:id
45
+ def destroy
46
+ authorize_resource!(@resource)
47
+
48
+ if @resource.amount_used.positive?
49
+ render_error(
50
+ code: 'store_credit_in_use',
51
+ message: 'Cannot delete a store credit that has already been used',
52
+ status: :unprocessable_content
53
+ )
54
+ return
55
+ end
56
+
57
+ @resource.destroy
58
+ head :no_content
59
+ end
60
+
61
+ protected
62
+
63
+ def set_parent
64
+ @parent = Spree.user_class.find_by_prefix_id!(params[:customer_id])
65
+ end
66
+
67
+ def parent_association
68
+ :store_credits
69
+ end
70
+
71
+ def model_class
72
+ Spree::StoreCredit
73
+ end
74
+
75
+ def serializer_class
76
+ Spree.api.admin_store_credit_serializer
77
+ end
78
+
79
+ private
80
+
81
+ def create_attrs
82
+ params.permit(:amount, :currency, :category_id, :memo).to_h
83
+ end
84
+
85
+ def update_attrs
86
+ params.permit(:memo, :category_id, :amount).to_h
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,119 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class CustomersController < ResourceController
6
+ include Spree::Api::V3::BulkOperations
7
+
8
+ scoped_resource :customers
9
+
10
+ before_action :require_ids!, only: [:bulk_add_to_groups, :bulk_remove_from_groups]
11
+
12
+ def create
13
+ @resource = Spree.user_class.new(permitted_params)
14
+ # Admin-created customers don't pick a password upfront — they
15
+ # claim the account via password reset later.
16
+ # `Spree::UserMethods` exposes `skip_password_validation` so
17
+ # Devise's `:validatable` lets a nil credential through on this
18
+ # code path. Storefront registration never sets the flag, so
19
+ # customer self-signup still requires a password.
20
+ @resource.skip_password_validation = true if @resource.password.blank?
21
+ authorize!(:create, @resource)
22
+
23
+ if @resource.save
24
+ render json: serialize_resource(@resource), status: :created
25
+ else
26
+ render_validation_error(@resource.errors)
27
+ end
28
+ end
29
+
30
+ def update
31
+ authorize_resource!(@resource)
32
+
33
+ if @resource.update(permitted_params)
34
+ render json: serialize_resource(@resource.reload)
35
+ else
36
+ render_validation_error(@resource.errors)
37
+ end
38
+ end
39
+
40
+ def destroy
41
+ authorize_resource!(@resource)
42
+ @resource.destroy
43
+ head :no_content
44
+ rescue Spree::Core::DestroyWithOrdersError => e
45
+ render_error(
46
+ code: 'customer_has_orders',
47
+ message: e.message.presence || Spree.t(:error_user_destroy_with_orders),
48
+ status: :unprocessable_content
49
+ )
50
+ end
51
+
52
+ # Bulk add the given customers to the given groups. Idempotent —
53
+ # customers already in a group are skipped at the model layer.
54
+ def bulk_add_to_groups
55
+ apply_groups(:add_customers)
56
+ end
57
+
58
+ # Bulk remove the given customers from the given groups.
59
+ def bulk_remove_from_groups
60
+ apply_groups(:remove_customers)
61
+ end
62
+
63
+ protected
64
+
65
+ def model_class
66
+ Spree.user_class
67
+ end
68
+
69
+ def serializer_class
70
+ Spree.api.admin_customer_serializer
71
+ end
72
+
73
+ def scope
74
+ super.with_order_aggregates
75
+ end
76
+
77
+ def collection_includes
78
+ [:rich_text_internal_note, taggings: :tag]
79
+ end
80
+
81
+ private
82
+
83
+ # Mirrors the products controller's resource-named key so SPA toasts
84
+ # can substitute `{customer_count}` instead of the generic
85
+ # `{record_count}` shipped by `Spree::Api::V3::BulkOperations`.
86
+ def bulk_record_count_key
87
+ :customer_count
88
+ end
89
+
90
+ def permitted_params
91
+ params.permit(
92
+ :email, :first_name, :last_name, :phone,
93
+ :password, :password_confirmation, :selected_locale,
94
+ :avatar, :accepts_email_marketing, :internal_note,
95
+ metadata: {}, tags: []
96
+ )
97
+ end
98
+
99
+ # Authorises bulk group mutation, decodes prefixed IDs, then dispatches
100
+ # to `add_customers` / `remove_customers` per group. Returns the
101
+ # counts of records actually affected so the UI can show a toast.
102
+ def apply_groups(method)
103
+ authorize! :update, model_class
104
+
105
+ user_ids = decode_ids(params[:ids])
106
+ group_ids = decode_ids(params[:customer_group_ids])
107
+
108
+ scoped_user_ids = scope.where(id: user_ids).pluck(:id)
109
+ scoped_groups = Spree::CustomerGroup.for_store(current_store).where(id: group_ids)
110
+
111
+ scoped_groups.find_each { |group| group.public_send(method, scoped_user_ids) }
112
+
113
+ render json: { customer_count: scoped_user_ids.size, customer_group_count: scoped_groups.size }
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,44 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class DashboardController < Admin::BaseController
6
+ scoped_resource :dashboard
7
+
8
+ # GET /api/v3/admin/dashboard/analytics
9
+ def analytics
10
+ date_from = (params[:date_from] || 30.days.ago).to_time.beginning_of_day
11
+ date_to = (params[:date_to] || Time.current).to_time.end_of_day
12
+ currency = params[:currency] || current_store.default_currency
13
+
14
+ serializer = DashboardAnalyticsSerializer.new(
15
+ store: current_store,
16
+ currency: currency,
17
+ time_range: date_from..date_to,
18
+ params: serializer_params
19
+ )
20
+
21
+ render json: serializer.to_h
22
+ end
23
+
24
+ private
25
+
26
+ def action_kind
27
+ 'read'
28
+ end
29
+
30
+ def serializer_params
31
+ {
32
+ store: current_store,
33
+ locale: current_locale,
34
+ currency: current_currency,
35
+ user: current_user,
36
+ includes: [],
37
+ expand: []
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class DirectUploadsController < Admin::BaseController
6
+ # Direct uploads is a write-adjacent presigning helper: callers exchange
7
+ # blob metadata for an upload URL, then reference the resulting
8
+ # signed_id when creating/updating a resource (product media, customer
9
+ # avatar, etc). The narrowest scope it can map to is `write_products`
10
+ # since that covers the dominant upload flow (product/variant media).
11
+ # Other admin-write flows that take signed_ids (e.g. customer avatar)
12
+ # already require the relevant `write_<resource>` scope on the
13
+ # subsequent PATCH, so this gate is the floor, not the only check.
14
+ scoped_resource :products
15
+
16
+ skip_before_action :authenticate_user
17
+
18
+ # POST /api/v3/admin/direct_uploads
19
+ def create
20
+ blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_params)
21
+
22
+ render json: {
23
+ direct_upload: {
24
+ url: blob.service_url_for_direct_upload,
25
+ headers: blob.service_headers_for_direct_upload
26
+ },
27
+ signed_id: blob.signed_id
28
+ }, status: :created
29
+ end
30
+
31
+ private
32
+
33
+ def blob_params
34
+ params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type).to_h.symbolize_keys
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,89 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # See `docs/plans/5.5-admin-spa-csv-export.md`.
6
+ class ExportsController < ResourceController
7
+ include ActiveStorage::SetCurrent
8
+
9
+ scoped_resource :exports
10
+
11
+ # We stream the CSV inline rather than redirecting to ActiveStorage's
12
+ # signed-URL endpoint because the SPA's Vite proxy only forwards
13
+ # `/api/*`. A cross-origin redirect to `/rails/active_storage/...`
14
+ # strips the Authorization header and the download fails silently.
15
+ def download
16
+ @resource = find_resource
17
+ authorize_resource!(@resource, :show)
18
+
19
+ unless @resource.done?
20
+ return render_error(
21
+ code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:export_not_ready],
22
+ message: 'Export is not ready yet',
23
+ status: :unprocessable_content
24
+ )
25
+ end
26
+
27
+ attachment = @resource.attachment
28
+ send_data(
29
+ attachment.download,
30
+ filename: attachment.filename.to_s,
31
+ type: attachment.content_type || 'text/csv',
32
+ disposition: 'attachment'
33
+ )
34
+ end
35
+
36
+ protected
37
+
38
+ def model_class
39
+ Spree::Export
40
+ end
41
+
42
+ def serializer_class
43
+ Spree.api.admin_export_serializer
44
+ end
45
+
46
+ def scope_includes
47
+ [:user, { attachment_attachment: :blob }]
48
+ end
49
+
50
+ def build_resource
51
+ klass = resolve_export_type(permitted_params[:type]) || Spree::Export
52
+ attrs = permitted_params.except(:type).merge(
53
+ store: current_store,
54
+ user: try_spree_current_user
55
+ )
56
+ klass.new(attrs)
57
+ end
58
+
59
+ # `search_params` carries an arbitrary Ransack hash with nested
60
+ # groupings (`{ g: [{ name_cont: 'foo' }] }`). Rails' `permit(k: {})`
61
+ # rejects nested hashes, so we extract via `to_unsafe_h`. `:format`
62
+ # is intentionally dropped — only CSV is supported and Rails' request
63
+ # format would otherwise overwrite the model's enum.
64
+ def permitted_params
65
+ attrs = params.permit(:type, :record_selection)
66
+ raw = params[:search_params]
67
+ attrs[:search_params] = raw.respond_to?(:to_unsafe_h) ? raw.to_unsafe_h : raw if raw.present?
68
+ attrs
69
+ end
70
+
71
+ # Returns the registered Export subclass matching `name`, or nil.
72
+ #
73
+ # The constantize target comes from `available_types` (a trusted
74
+ # in-process registry), not from the request — `name` is only used
75
+ # to *select* an entry in the allowlist. This keeps the data flow
76
+ # from user input → trusted-string → `constantize` legible to
77
+ # static analyzers (CodeQL otherwise flags the inverse pattern of
78
+ # gating user input with `include?` before calling `constantize`).
79
+ def resolve_export_type(name)
80
+ return nil if name.blank?
81
+
82
+ target = Spree::Export.available_types.map(&:to_s).find { |t| t == name.to_s }
83
+ target&.constantize
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,31 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin bulk-issue endpoint for `Spree::GiftCardBatch`. Creating a
6
+ # batch synchronously generates the `codes_count` gift cards inline
7
+ # (or kicks off a background job when the count exceeds
8
+ # `Spree.config.gift_card_batch_web_limit`, default 500). Read-only
9
+ # access lives behind `list`/`show` so the SPA can surface batch
10
+ # context on the gift cards index (filter chip, batch chip on rows).
11
+ class GiftCardBatchesController < ResourceController
12
+ scoped_resource :gift_cards
13
+
14
+ protected
15
+
16
+ def model_class
17
+ Spree::GiftCardBatch
18
+ end
19
+
20
+ def serializer_class
21
+ Spree.api.admin_gift_card_batch_serializer
22
+ end
23
+
24
+ def permitted_params
25
+ params.permit(:prefix, :codes_count, :amount, :expires_at, :currency)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end