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,33 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Read-only listing of coupon codes. Codes are generated server-side
6
+ # based on the parent promotion's `code_prefix` and `number_of_codes`,
7
+ # so this controller intentionally only exposes index/show.
8
+ class CouponCodesController < ResourceController
9
+ scoped_resource :promotions
10
+
11
+ protected
12
+
13
+ def model_class
14
+ Spree::CouponCode
15
+ end
16
+
17
+ def serializer_class
18
+ Spree.api.admin_coupon_code_serializer
19
+ end
20
+
21
+ def set_parent
22
+ @parent = Spree::Promotion.accessible_by(current_ability, :show)
23
+ .find_by_prefix_id!(params[:promotion_id])
24
+ end
25
+
26
+ def parent_association
27
+ :coupon_codes
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Schema-side metadata for custom fields. Definitions are per resource
6
+ # *type* (every Spree::Product shares the same definitions), so this is
7
+ # a flat top-level endpoint. Filter by `?resource_type=Spree::Product`
8
+ # (or any other registered custom-field-bearing resource) to scope the
9
+ # list to one parent type.
10
+ class CustomFieldDefinitionsController < ResourceController
11
+ scoped_resource :settings
12
+
13
+ protected
14
+
15
+ def model_class
16
+ Spree::CustomFieldDefinition
17
+ end
18
+
19
+ def serializer_class
20
+ Spree.api.admin_custom_field_definition_serializer
21
+ end
22
+
23
+ # `label`, `field_type`, `storefront_visible` are model-side aliases
24
+ # (alias_attribute / accessors) on Spree::CustomFieldDefinition. The
25
+ # API → DB column rename lands in 6.0 and this controller stays flat.
26
+ def permitted_params
27
+ params.permit(:namespace, :key, :label, :field_type,
28
+ :resource_type, :storefront_visible)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,129 @@
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_relation.find_by_prefix_id!(parent_lookup.value)
57
+ authorize!(:show, @parent)
58
+ end
59
+
60
+ # Resolves the parent within the current store so a token for one
61
+ # store can't read or write custom fields on another store's records.
62
+ # Users are intentionally global in Spree (`User.for_store` is a
63
+ # no-op), so the store boundary for customer parents is enforced by
64
+ # the `authorize!(:show, @parent)` ability check above.
65
+ def parent_relation
66
+ klass = parent_lookup.klass
67
+ klass.respond_to?(:for_store) ? klass.for_store(current_store) : klass
68
+ end
69
+
70
+ # Product-family parents have no scope pair of their own — their
71
+ # custom fields are catalog data, gated by `products` like the rest
72
+ # of the variant/option-type surface.
73
+ SCOPE_OVERRIDES = { 'option_type' => :products, 'variant' => :products }.freeze
74
+
75
+ # Per-parent scope check: a key holding `write_products` may write a
76
+ # product's custom fields, `write_orders` may write an order's, etc.
77
+ # Resolves the parent at request time rather than via the static
78
+ # `scoped_resource` declaration.
79
+ def scoped_resource_name
80
+ segment = parent_lookup&.segment
81
+ return unless segment
82
+
83
+ SCOPE_OVERRIDES[segment] || segment.pluralize.to_sym
84
+ end
85
+
86
+ # `custom_field_definition_id` is an alias_attribute on Spree::CustomField;
87
+ # AR resolves the prefixed-ID and the alias to the canonical FK on assign.
88
+ def permitted_params
89
+ params.permit(:custom_field_definition_id, :value)
90
+ end
91
+
92
+ def update_permitted_params
93
+ params.permit(:value)
94
+ end
95
+
96
+ private
97
+
98
+ ParentLookup = Struct.new(:klass, :value, :segment)
99
+
100
+ # Stores class names (not class objects) so the map survives dev-mode
101
+ # code reloads — `enabled_resources` is captured at boot and its
102
+ # class references go stale. Aliases `'customer'` because the route
103
+ # uses `customer_id` while user_class.model_name.element is `'user'`,
104
+ # and `'category'` because the routes expose taxons as categories
105
+ # (5.5 rename) while the model's element is still `'taxon'`.
106
+ def parent_route_map
107
+ @parent_route_map ||= Spree.metafields.enabled_resources.each_with_object({}) do |klass, m|
108
+ m[klass.model_name.element.to_s] = klass.name
109
+ end.merge('customer' => Spree.user_class.name, 'category' => 'Spree::Taxon')
110
+ end
111
+
112
+ # Returns the first segment whose `<segment>_id` is present in params,
113
+ # paired with its class and the raw id value, or nil. Memoized — read
114
+ # by both `set_parent` and `scoped_resource_name`.
115
+ def parent_lookup
116
+ return @parent_lookup if defined?(@parent_lookup)
117
+
118
+ match = parent_route_map.find { |segment, _| params[:"#{segment}_id"].present? }
119
+ @parent_lookup =
120
+ if match
121
+ segment, klass_name = match
122
+ ParentLookup.new(klass_name.constantize, params[:"#{segment}_id"], segment)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ 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,83 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ module Customers
6
+ class AddressesController < BaseController
7
+
8
+ # POST /api/v3/admin/customers/:customer_id/addresses
9
+ def create
10
+ @resource = @parent.addresses.new(address_attrs)
11
+ authorize_resource!(@resource, :create)
12
+
13
+ ApplicationRecord.transaction do
14
+ if @resource.save
15
+ apply_default_flags(@resource)
16
+ render json: serialize_resource(@resource.reload), status: :created
17
+ else
18
+ render_validation_error(@resource.errors)
19
+ raise ActiveRecord::Rollback
20
+ end
21
+ end
22
+ end
23
+
24
+ # PATCH /api/v3/admin/customers/:customer_id/addresses/:id
25
+ def update
26
+ authorize_resource!(@resource)
27
+
28
+ ApplicationRecord.transaction do
29
+ if @resource.update(address_attrs)
30
+ apply_default_flags(@resource)
31
+ render json: serialize_resource(@resource.reload)
32
+ else
33
+ render_validation_error(@resource.errors)
34
+ raise ActiveRecord::Rollback
35
+ end
36
+ end
37
+ end
38
+
39
+ # DELETE /api/v3/admin/customers/:customer_id/addresses/:id
40
+ def destroy
41
+ authorize_resource!(@resource)
42
+ @resource.destroy
43
+ head :no_content
44
+ end
45
+
46
+ protected
47
+
48
+ def parent_association
49
+ :addresses
50
+ end
51
+
52
+ def model_class
53
+ Spree::Address
54
+ end
55
+
56
+ def serializer_class
57
+ Spree.api.admin_address_serializer
58
+ end
59
+
60
+ private
61
+
62
+ def address_attrs
63
+ params.permit(
64
+ :firstname, :lastname, :first_name, :last_name,
65
+ :address1, :address2, :city,
66
+ :country_iso, :state_abbr, :country_id, :state_id,
67
+ :zipcode, :postal_code, :phone, :alternative_phone,
68
+ :state_name, :company, :label, :quick_checkout
69
+ )
70
+ end
71
+
72
+ def apply_default_flags(address)
73
+ updates = {}
74
+ updates[:bill_address_id] = address.id if ActiveModel::Type::Boolean.new.cast(params[:is_default_billing])
75
+ updates[:ship_address_id] = address.id if ActiveModel::Type::Boolean.new.cast(params[:is_default_shipping])
76
+ @parent.update!(updates) if updates.any?
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,33 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ module Customers
6
+ # Shared base for resources nested under a customer
7
+ # (`/admin/customers/:customer_id/...`). Resolves the parent customer
8
+ # and authorizes it per action (`:show` for reads, `:update` for
9
+ # writes) so a role that can only view a customer can't mutate its
10
+ # nested collections. Mirrors `Orders::BaseController`.
11
+ class BaseController < ResourceController
12
+ scoped_resource :customers
13
+
14
+ protected
15
+
16
+ # Resolve the customer through the ability-scoped relation, using
17
+ # the action-appropriate ability on the parent (`:show` for reads,
18
+ # `:update` for writes — see `parent_ability_action`). A customer
19
+ # the caller can't access for the requested action is filtered out
20
+ # and 404s, rather than leaking its existence as a 403. Users are
21
+ # global in Spree (`User.for_store` is a no-op), so the ability is
22
+ # the only boundary here.
23
+ def set_parent
24
+ @parent = Spree.user_class.
25
+ accessible_by(current_ability, parent_ability_action).
26
+ find_by_prefix_id!(params[:customer_id])
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ module Customers
6
+ class CreditCardsController < BaseController
7
+ protected
8
+
9
+ def parent_association
10
+ :credit_cards
11
+ end
12
+
13
+ def model_class
14
+ Spree::CreditCard
15
+ end
16
+
17
+ def serializer_class
18
+ Spree.api.admin_credit_card_serializer
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,92 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ module Customers
6
+ class StoreCreditsController < BaseController
7
+ # Store credits gate on their own scope rather than the parent's
8
+ # `:customers`, so a store-credit integration key doesn't need
9
+ # broad customer access.
10
+ scoped_resource :store_credits
11
+
12
+ # POST /api/v3/admin/customers/:customer_id/store_credits
13
+ def create
14
+ @resource = @parent.store_credits.new(create_attrs)
15
+ @resource.created_by = try_spree_current_user
16
+ @resource.store ||= current_store
17
+ @resource.category ||= Spree::StoreCreditCategory.first
18
+ authorize_resource!(@resource, :create)
19
+
20
+ if @resource.save
21
+ render json: serialize_resource(@resource), status: :created
22
+ else
23
+ render_validation_error(@resource.errors)
24
+ end
25
+ end
26
+
27
+ # PATCH /api/v3/admin/customers/:customer_id/store_credits/:id
28
+ def update
29
+ authorize_resource!(@resource)
30
+
31
+ if @resource.amount_used.positive? && update_attrs.key?(:amount)
32
+ render_error(
33
+ code: 'store_credit_in_use',
34
+ message: 'Cannot change amount on a store credit that has already been used',
35
+ status: :unprocessable_content
36
+ )
37
+ return
38
+ end
39
+
40
+ if @resource.update(update_attrs)
41
+ render json: serialize_resource(@resource.reload)
42
+ else
43
+ render_validation_error(@resource.errors)
44
+ end
45
+ end
46
+
47
+ # DELETE /api/v3/admin/customers/:customer_id/store_credits/:id
48
+ def destroy
49
+ authorize_resource!(@resource)
50
+
51
+ if @resource.amount_used.positive?
52
+ render_error(
53
+ code: 'store_credit_in_use',
54
+ message: 'Cannot delete a store credit that has already been used',
55
+ status: :unprocessable_content
56
+ )
57
+ return
58
+ end
59
+
60
+ @resource.destroy
61
+ head :no_content
62
+ end
63
+
64
+ protected
65
+
66
+ def parent_association
67
+ :store_credits
68
+ end
69
+
70
+ def model_class
71
+ Spree::StoreCredit
72
+ end
73
+
74
+ def serializer_class
75
+ Spree.api.admin_store_credit_serializer
76
+ end
77
+
78
+ private
79
+
80
+ def create_attrs
81
+ params.permit(:amount, :currency, :category_id, :memo).to_h
82
+ end
83
+
84
+ def update_attrs
85
+ params.permit(:memo, :category_id, :amount).to_h
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ 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