spree_api 5.4.3 → 5.5.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +36 -0
- data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
- data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +52 -0
- data/app/controllers/concerns/spree/api/v3/admin/subclassed_resource.rb +149 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +54 -0
- data/app/controllers/concerns/spree/api/v3/bulk_operations.rb +103 -0
- data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +60 -0
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
- data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +84 -0
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +104 -0
- data/app/controllers/concerns/spree/api/v3/store/search_provider_support.rb +35 -1
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +109 -0
- data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +84 -0
- data/app/controllers/spree/api/v3/admin/auth_controller.rb +134 -0
- data/app/controllers/spree/api/v3/admin/base_controller.rb +3 -17
- data/app/controllers/spree/api/v3/admin/categories_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/channels_controller.rb +65 -0
- data/app/controllers/spree/api/v3/admin/countries_controller.rb +38 -0
- data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/custom_field_definitions_controller.rb +34 -0
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +129 -0
- data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +83 -0
- data/app/controllers/spree/api/v3/admin/customers/base_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +92 -0
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +119 -0
- data/app/controllers/spree/api/v3/admin/dashboard_controller.rb +44 -0
- data/app/controllers/spree/api/v3/admin/direct_uploads_controller.rb +40 -0
- data/app/controllers/spree/api/v3/admin/exports_controller.rb +136 -0
- data/app/controllers/spree/api/v3/admin/gift_card_batches_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +138 -0
- data/app/controllers/spree/api/v3/admin/invitations_controller.rb +81 -0
- data/app/controllers/spree/api/v3/admin/markets_controller.rb +42 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +69 -0
- data/app/controllers/spree/api/v3/admin/media_controller.rb +119 -0
- data/app/controllers/spree/api/v3/admin/option_types_controller.rb +34 -0
- data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +27 -0
- data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +104 -0
- data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +79 -0
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +92 -0
- data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +90 -0
- data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +53 -0
- data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +59 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +190 -0
- data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +73 -0
- data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +179 -0
- data/app/controllers/spree/api/v3/admin/prices_controller.rb +157 -0
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +48 -0
- data/app/controllers/spree/api/v3/admin/products_controller.rb +237 -0
- data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +78 -0
- data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +56 -0
- data/app/controllers/spree/api/v3/admin/promotions_controller.rb +78 -0
- data/app/controllers/spree/api/v3/admin/resource_controller.rb +29 -11
- data/app/controllers/spree/api/v3/admin/roles_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +35 -0
- data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +36 -0
- data/app/controllers/spree/api/v3/admin/stock_reservations_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +75 -0
- data/app/controllers/spree/api/v3/admin/store_controller.rb +53 -0
- data/app/controllers/spree/api/v3/admin/store_credit_categories_controller.rb +21 -0
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +51 -0
- data/app/controllers/spree/api/v3/admin/tax_categories_controller.rb +21 -0
- data/app/controllers/spree/api/v3/admin/variants_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +49 -0
- data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +75 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +117 -8
- data/app/controllers/spree/api/v3/store/auth_controller.rb +8 -28
- data/app/controllers/spree/api/v3/store/base_controller.rb +6 -0
- data/app/controllers/spree/api/v3/store/carts_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/customers_controller.rb +6 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +77 -0
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +2 -2
- data/app/controllers/spree/api/v3/store/products_controller.rb +4 -3
- data/app/controllers/spree/api/v3/store/resource_controller.rb +10 -2
- data/app/jobs/spree/webhook_delivery_job.rb +5 -0
- data/app/models/spree/api_key_ability.rb +16 -0
- data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/adjustment_serializer.rb +3 -15
- data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +19 -3
- data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +42 -0
- data/app/serializers/spree/api/v3/admin/category_serializer.rb +4 -3
- data/app/serializers/spree/api/v3/admin/channel_serializer.rb +15 -0
- data/app/serializers/spree/api/v3/admin/country_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/coupon_code_serializer.rb +30 -0
- data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/custom_field_definition_serializer.rb +21 -0
- data/app/serializers/spree/api/v3/admin/custom_field_serializer.rb +8 -3
- data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/admin/customer_serializer.rb +58 -2
- data/app/serializers/spree/api/v3/admin/dashboard_analytics_serializer.rb +143 -0
- data/app/serializers/spree/api/v3/admin/export_serializer.rb +40 -0
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/{asset_serializer.rb → gift_card_batch_serializer.rb} +1 -1
- data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +39 -4
- data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +64 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +4 -16
- data/app/serializers/spree/api/v3/admin/media_serializer.rb +24 -2
- data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +21 -6
- data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +51 -0
- data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +55 -0
- data/app/serializers/spree/api/v3/admin/price_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/product_publication_serializer.rb +11 -0
- data/app/serializers/spree/api/v3/admin/product_serializer.rb +34 -10
- data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +71 -0
- data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +85 -0
- data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +41 -0
- data/app/serializers/spree/api/v3/admin/refund_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/role_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/admin/stock_item_serializer.rb +16 -1
- data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/admin/stock_reservation_serializer.rb +46 -0
- data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +37 -0
- data/app/serializers/spree/api/v3/admin/store_credit_category_serializer.rb +19 -0
- data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +11 -5
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +55 -0
- data/app/serializers/spree/api/v3/admin/tax_category_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +37 -6
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +45 -0
- data/app/serializers/spree/api/v3/admin/webhook_endpoint_serializer.rb +69 -0
- data/app/serializers/spree/api/v3/channel_serializer.rb +14 -0
- data/app/serializers/spree/api/v3/custom_field_serializer.rb +9 -10
- data/app/serializers/spree/api/v3/customer_serializer.rb +5 -0
- data/app/serializers/spree/api/v3/market_serializer.rb +2 -1
- data/app/serializers/spree/api/v3/media_serializer.rb +8 -6
- data/app/serializers/spree/api/v3/order_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/payment_method_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/product_publication_serializer.rb +22 -0
- data/app/serializers/spree/api/v3/product_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/stock_reservation_serializer.rb +10 -0
- data/config/locales/en.yml +2 -0
- data/config/routes.rb +235 -1
- data/lib/spree/api/configuration.rb +2 -2
- data/lib/spree/api/dependencies.rb +25 -1
- data/lib/spree/api/openapi/path_sorter.rb +126 -0
- data/lib/spree/api/openapi/schema_helper.rb +185 -6
- data/lib/spree/api/testing_support/v3/base.rb +28 -0
- metadata +98 -8
- data/app/serializers/spree/api/v3/admin/shipping_category_serializer.rb +0 -14
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class OrdersController < ResourceController
|
|
6
|
+
include Spree::Api::V3::OrderLock
|
|
7
|
+
|
|
8
|
+
scoped_resource :orders
|
|
9
|
+
|
|
10
|
+
skip_before_action :set_resource, only: [:index, :create]
|
|
11
|
+
before_action :set_resource, only: [:show, :update, :destroy, :complete, :cancel, :approve, :resume, :resend_confirmation]
|
|
12
|
+
|
|
13
|
+
# POST /api/v3/admin/orders
|
|
14
|
+
def create
|
|
15
|
+
authorize!(:create, Spree::Order)
|
|
16
|
+
|
|
17
|
+
result = Spree.order_create_service.call(
|
|
18
|
+
store: current_store,
|
|
19
|
+
user: resolve_user,
|
|
20
|
+
params: order_create_params
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if result.success?
|
|
24
|
+
@resource = result.value
|
|
25
|
+
render json: serialize_resource(@resource), status: :created
|
|
26
|
+
else
|
|
27
|
+
render_service_error(result.error)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# PATCH /api/v3/admin/orders/:id
|
|
32
|
+
def update
|
|
33
|
+
with_order_lock do
|
|
34
|
+
result = Spree.order_update_service.call(
|
|
35
|
+
order: @resource,
|
|
36
|
+
params: order_update_params
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if result.success?
|
|
40
|
+
render json: serialize_resource(result.value)
|
|
41
|
+
else
|
|
42
|
+
render_validation_error(@resource.errors.presence || result.error)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# PATCH /api/v3/admin/orders/:id/complete
|
|
48
|
+
def complete
|
|
49
|
+
with_order_lock do
|
|
50
|
+
result = Spree.order_complete_service.call(
|
|
51
|
+
order: @resource,
|
|
52
|
+
payment_pending: ActiveModel::Type::Boolean.new.cast(params[:payment_pending]),
|
|
53
|
+
notify_customer: ActiveModel::Type::Boolean.new.cast(params[:notify_customer])
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if result.success?
|
|
57
|
+
render json: serialize_resource(@resource.reload)
|
|
58
|
+
else
|
|
59
|
+
render_service_error(@resource.errors.presence || result.error, code: ERROR_CODES[:order_cannot_complete])
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# PATCH /api/v3/admin/orders/:id/cancel
|
|
65
|
+
def cancel
|
|
66
|
+
with_order_lock do
|
|
67
|
+
@resource.canceled_by(try_spree_current_user)
|
|
68
|
+
render json: serialize_resource(@resource.reload)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# PATCH /api/v3/admin/orders/:id/approve
|
|
73
|
+
def approve
|
|
74
|
+
with_order_lock do
|
|
75
|
+
@resource.approved_by(try_spree_current_user)
|
|
76
|
+
render json: serialize_resource(@resource.reload)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# PATCH /api/v3/admin/orders/:id/resume
|
|
81
|
+
def resume
|
|
82
|
+
with_order_lock do
|
|
83
|
+
@resource.resume!
|
|
84
|
+
render json: serialize_resource(@resource.reload)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# POST /api/v3/admin/orders/:id/resend_confirmation
|
|
89
|
+
def resend_confirmation
|
|
90
|
+
@resource.publish_event('order.completed')
|
|
91
|
+
render json: serialize_resource(@resource)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
protected
|
|
95
|
+
|
|
96
|
+
def model_class
|
|
97
|
+
Spree::Order
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def serializer_class
|
|
101
|
+
Spree.api.admin_order_serializer
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Override scope — Order uses SingleStoreResource (for_store)
|
|
105
|
+
def scope
|
|
106
|
+
current_store.orders.accessible_by(current_ability, :show).preload_associations_lazily
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def set_resource
|
|
110
|
+
@resource = scope.find_by_prefix_id!(params[:id])
|
|
111
|
+
@order = @resource # needed for OrderLock
|
|
112
|
+
authorize_resource!(@resource)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Map state transition actions to :update permission
|
|
116
|
+
def authorize_resource!(resource = @resource, action = action_name.to_sym)
|
|
117
|
+
mapped_action = case action
|
|
118
|
+
when :complete, :cancel, :approve, :resume, :resend_confirmation
|
|
119
|
+
:update
|
|
120
|
+
else
|
|
121
|
+
action
|
|
122
|
+
end
|
|
123
|
+
authorize!(mapped_action, resource)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def collection_includes
|
|
127
|
+
[:line_items, :user, :channel, :rich_text_internal_note]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def resolve_user
|
|
133
|
+
customer_param = params[:customer_id].presence || params[:user_id].presence
|
|
134
|
+
return unless customer_param
|
|
135
|
+
|
|
136
|
+
Spree.user_class.find_by_param!(customer_param)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def order_create_params
|
|
140
|
+
normalize_params(
|
|
141
|
+
params.permit(
|
|
142
|
+
:email, :customer_id, :user_id, :use_customer_default_address,
|
|
143
|
+
:currency, :market_id, :channel_id, :locale,
|
|
144
|
+
:customer_note, :internal_note,
|
|
145
|
+
:shipping_address_id, :billing_address_id,
|
|
146
|
+
:preferred_stock_location_id,
|
|
147
|
+
:coupon_code,
|
|
148
|
+
metadata: {},
|
|
149
|
+
tags: [],
|
|
150
|
+
shipping_address: address_permitted_keys,
|
|
151
|
+
billing_address: address_permitted_keys,
|
|
152
|
+
items: item_permitted_keys
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def order_update_params
|
|
158
|
+
normalize_params(
|
|
159
|
+
params.permit(
|
|
160
|
+
:email, :customer_id, :user_id,
|
|
161
|
+
:customer_note, :internal_note,
|
|
162
|
+
:currency, :locale, :market_id, :channel_id,
|
|
163
|
+
:preferred_stock_location_id,
|
|
164
|
+
metadata: {},
|
|
165
|
+
tags: [],
|
|
166
|
+
ship_address: address_permitted_keys,
|
|
167
|
+
bill_address: address_permitted_keys,
|
|
168
|
+
items: item_permitted_keys
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def address_permitted_keys
|
|
174
|
+
[
|
|
175
|
+
:firstname, :lastname, :first_name, :last_name,
|
|
176
|
+
:address1, :address2, :city,
|
|
177
|
+
:country_iso, :state_abbr, :country_id, :state_id,
|
|
178
|
+
:zipcode, :postal_code, :phone, :alternative_phone,
|
|
179
|
+
:state_name, :company, :label
|
|
180
|
+
]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def item_permitted_keys
|
|
184
|
+
[:variant_id, :quantity, { metadata: {} }]
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class PaymentMethodsController < ResourceController
|
|
6
|
+
include Spree::Api::V3::Admin::SubclassedResource
|
|
7
|
+
|
|
8
|
+
scoped_resource :settings
|
|
9
|
+
|
|
10
|
+
subclassed_via -> { Spree::PaymentMethod.providers },
|
|
11
|
+
unknown_type_error: 'unknown_payment_method_type'
|
|
12
|
+
|
|
13
|
+
# Lists available payment provider subclasses for the create form.
|
|
14
|
+
# Returns: { data: [{ type, label, description, preference_schema }] }.
|
|
15
|
+
# The preference_schema array describes the provider-specific
|
|
16
|
+
# configuration fields, so admin UIs can render a generic
|
|
17
|
+
# preferences form without hard-coding per-provider knowledge.
|
|
18
|
+
# Filters out subclasses already installed in the current store —
|
|
19
|
+
# mirrors the legacy admin's "available_payment_methods" helper, so
|
|
20
|
+
# admins don't see (and accidentally double-install) the same
|
|
21
|
+
# provider twice.
|
|
22
|
+
def types
|
|
23
|
+
authorize! :create, model_class
|
|
24
|
+
|
|
25
|
+
# Query via direct join rather than `current_store.payment_methods`
|
|
26
|
+
# — the has_many-through association can cache stale results when
|
|
27
|
+
# `current_store` was loaded earlier in the request (e.g. by the
|
|
28
|
+
# auth layer).
|
|
29
|
+
installed_class_names = Spree::PaymentMethod
|
|
30
|
+
.joins(:store_payment_methods)
|
|
31
|
+
.where(spree_payment_methods_stores: { store_id: current_store.id })
|
|
32
|
+
.pluck(:type)
|
|
33
|
+
installed_shorthands = installed_class_names.filter_map do |name|
|
|
34
|
+
name.safe_constantize&.api_type
|
|
35
|
+
end
|
|
36
|
+
available = model_class.subclasses_with_preference_schema.reject do |entry|
|
|
37
|
+
installed_shorthands.include?(entry[:type])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
render json: { data: available }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
protected
|
|
44
|
+
|
|
45
|
+
def model_class
|
|
46
|
+
Spree::PaymentMethod
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def serializer_class
|
|
50
|
+
Spree.api.admin_payment_method_serializer
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Explicit allowlist per the v3 convention — flat params, no
|
|
54
|
+
# reach into the global `Spree::PermittedAttributes` registry
|
|
55
|
+
# (which is the legacy Rails admin's surface). `type` and
|
|
56
|
+
# `preferences` are added by `SubclassedResource` on top.
|
|
57
|
+
def permitted_params
|
|
58
|
+
params.permit(:name, :description, :active, :storefront_visible, :auto_capture, :position, metadata: {}, preferences: {})
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# New payment methods get scoped to the current store automatically.
|
|
64
|
+
def build_subclassed_resource(klass, attrs)
|
|
65
|
+
resource = klass.new(attrs)
|
|
66
|
+
resource.stores = [current_store] if resource.stores.empty?
|
|
67
|
+
resource
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Admin CRUD for `Spree::PriceList`, plus the lifecycle transitions
|
|
6
|
+
# (`activate` / `deactivate`) and the spreadsheet's data feed
|
|
7
|
+
# (`prices`).
|
|
8
|
+
#
|
|
9
|
+
# Everything writable on the list — name, schedule, match policy,
|
|
10
|
+
# product membership (`product_ids: [...]`), nested rules
|
|
11
|
+
# (`rules: [...]`), and individual price overrides
|
|
12
|
+
# (`prices: [...]`) — flows through the regular PATCH payload, so
|
|
13
|
+
# the SPA saves the entire editor in one round-trip. No separate
|
|
14
|
+
# add_products / remove_products / add_rule / bulk_update_prices
|
|
15
|
+
# endpoints.
|
|
16
|
+
#
|
|
17
|
+
# Scoped under the `products` API-key scope — price lists are a
|
|
18
|
+
# product/pricing concern; we don't introduce a separate
|
|
19
|
+
# `read_price_lists` scope.
|
|
20
|
+
class PriceListsController < ResourceController
|
|
21
|
+
scoped_resource :products
|
|
22
|
+
|
|
23
|
+
# The base ResourceController limits `set_resource` to
|
|
24
|
+
# `show/update/destroy`. We need it on the custom member
|
|
25
|
+
# actions below too, so swap in our own filter — Rails keys
|
|
26
|
+
# before_actions by method name, so this would otherwise
|
|
27
|
+
# *replace* the parent's narrower filter and break the standard
|
|
28
|
+
# actions. Wrapping it under a different name keeps both.
|
|
29
|
+
before_action :load_member_resource, only: [:activate, :deactivate, :prices]
|
|
30
|
+
|
|
31
|
+
# GET /api/v3/admin/price_lists/price_rule_types
|
|
32
|
+
#
|
|
33
|
+
# Returns `[{ type, label, description, preference_schema }]`
|
|
34
|
+
# for every registered subclass in `Spree.pricing.rules`. The
|
|
35
|
+
# SPA uses this to build the "Add rule" picker + render a
|
|
36
|
+
# generic preferences form per subclass. Rules themselves are
|
|
37
|
+
# not a separate REST resource — they ride along on the price
|
|
38
|
+
# list's PATCH body via `rules: [...]`.
|
|
39
|
+
def price_rule_types
|
|
40
|
+
authorize! :read, Spree::PriceRule
|
|
41
|
+
render json: { data: Spree::PriceRule.subclasses_with_preference_schema }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# PATCH /api/v3/admin/price_lists/:id/activate
|
|
45
|
+
#
|
|
46
|
+
# State transition: draft|inactive → active (or → scheduled when
|
|
47
|
+
# `starts_at` is in the future). Mirrors the old Rails admin's
|
|
48
|
+
# "Activate" button which automatically scheduled future lists.
|
|
49
|
+
def activate
|
|
50
|
+
authorize! :update, @resource
|
|
51
|
+
event = @resource.starts_at.present? && @resource.starts_at.future? ? :schedule : :activate
|
|
52
|
+
|
|
53
|
+
if @resource.send(event)
|
|
54
|
+
render json: serialize_resource(@resource)
|
|
55
|
+
else
|
|
56
|
+
render_validation_error(@resource.errors)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# PATCH /api/v3/admin/price_lists/:id/deactivate
|
|
61
|
+
def deactivate
|
|
62
|
+
authorize! :update, @resource
|
|
63
|
+
|
|
64
|
+
if @resource.deactivate
|
|
65
|
+
render json: serialize_resource(@resource)
|
|
66
|
+
else
|
|
67
|
+
render_validation_error(@resource.errors)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# GET /api/v3/admin/price_lists/:id/prices
|
|
72
|
+
#
|
|
73
|
+
# The spreadsheet editor's data source. Returns every Price row
|
|
74
|
+
# in this list (filtered by `?currency=`), eager-loading
|
|
75
|
+
# `variant.product` + option values so each cell can render
|
|
76
|
+
# product name, variant options and SKU without N+1.
|
|
77
|
+
def prices
|
|
78
|
+
authorize! :read, @resource
|
|
79
|
+
currency = params[:currency].presence || current_store.default_currency
|
|
80
|
+
prices = @resource.prices
|
|
81
|
+
.includes(variant: [:product, { option_values: :option_type }])
|
|
82
|
+
.where(currency: currency)
|
|
83
|
+
.joins(variant: :product)
|
|
84
|
+
.order(Arel.sql("#{Spree::Product.table_name}.name ASC"))
|
|
85
|
+
.order(Arel.sql("#{Spree::Variant.table_name}.position ASC"))
|
|
86
|
+
|
|
87
|
+
render json: {
|
|
88
|
+
data: prices.map { |p| serialize_price(p) },
|
|
89
|
+
meta: { currency: currency, count: prices.size }
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
protected
|
|
94
|
+
|
|
95
|
+
def model_class
|
|
96
|
+
Spree::PriceList
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def serializer_class
|
|
100
|
+
Spree.api.admin_price_list_serializer
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def scope
|
|
104
|
+
super.for_store(current_store)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def permitted_params
|
|
108
|
+
attrs = normalize_params(
|
|
109
|
+
params.permit(
|
|
110
|
+
:name, :description, :position,
|
|
111
|
+
:starts_at, :ends_at, :match_policy,
|
|
112
|
+
product_ids: [],
|
|
113
|
+
rules: [:id, :type, { preferences: {} }],
|
|
114
|
+
prices: [:id, :variant_id, :currency, :amount, :compare_at_amount]
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
reject_foreign_membership(attrs)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The PriceList model setters (`product_ids=`, `prices=`) resolve
|
|
121
|
+
# member ids with no store scoping, so a list in this store could
|
|
122
|
+
# otherwise be populated with another store's products/variants.
|
|
123
|
+
# Drop any id that isn't in the current store before assignment.
|
|
124
|
+
def reject_foreign_membership(attrs)
|
|
125
|
+
if attrs[:product_ids].present?
|
|
126
|
+
store_product_ids = current_store.products.where(id: attrs[:product_ids]).pluck(:id).map(&:to_s).to_set
|
|
127
|
+
attrs[:product_ids] = Array(attrs[:product_ids]).select { |id| store_product_ids.include?(id.to_s) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if attrs[:prices].present?
|
|
131
|
+
incoming = Array(attrs[:prices])
|
|
132
|
+
store_variant_ids = current_store.variants.where(id: incoming.map { |r| r[:variant_id] }.compact).
|
|
133
|
+
pluck(:id).map(&:to_s).to_set
|
|
134
|
+
attrs[:prices] = incoming.select do |row|
|
|
135
|
+
row[:variant_id].blank? || store_variant_ids.include?(row[:variant_id].to_s)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
attrs
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
# Loads the record without the action-derived authorization
|
|
145
|
+
# `set_resource` runs (which would check `:activate` /
|
|
146
|
+
# `:deactivate` / `:prices` — actions that abilities don't
|
|
147
|
+
# grant). The per-action methods below explicitly call
|
|
148
|
+
# `authorize!` with the standard action that ability rules
|
|
149
|
+
# actually mention (`:update` / `:read`).
|
|
150
|
+
def load_member_resource
|
|
151
|
+
@resource = find_resource
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Hand-rolled flat shape for the spreadsheet — keeps the payload
|
|
155
|
+
# narrow (no nested variant/product/option_value objects) and
|
|
156
|
+
# avoids paying for the admin Price serializer when we only need
|
|
157
|
+
# ~6 fields per row. The grouping the UI does (rows → product
|
|
158
|
+
# header) is driven entirely off `product_id`/`product_name`.
|
|
159
|
+
def serialize_price(price)
|
|
160
|
+
variant = price.variant
|
|
161
|
+
product = variant.product
|
|
162
|
+
{
|
|
163
|
+
id: price.prefixed_id,
|
|
164
|
+
variant_id: variant.prefixed_id,
|
|
165
|
+
product_id: product.prefixed_id,
|
|
166
|
+
product_name: product.name,
|
|
167
|
+
variant_label: variant.options_text.presence,
|
|
168
|
+
sku: variant.sku,
|
|
169
|
+
currency: price.currency,
|
|
170
|
+
amount: price.amount&.to_s,
|
|
171
|
+
compare_at_amount: price.compare_at_amount&.to_s
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Admin CRUD for `Spree::Price`. Covers both base prices
|
|
6
|
+
# (`price_list_id: nil`) and price-list overrides under one
|
|
7
|
+
# resource, filtered via Ransack predicates on the index.
|
|
8
|
+
class PricesController < ResourceController
|
|
9
|
+
include Spree::Api::V3::BulkOperations
|
|
10
|
+
|
|
11
|
+
scoped_resource :products
|
|
12
|
+
|
|
13
|
+
before_action :require_ids!, only: [:bulk_destroy]
|
|
14
|
+
before_action :require_prices!, only: [:bulk_upsert]
|
|
15
|
+
|
|
16
|
+
# Bulk-upserts prices on the unique-key triple
|
|
17
|
+
# `(variant_id, currency, price_list_id)`.
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
def bulk_upsert
|
|
21
|
+
authorize! :create, Spree::Price
|
|
22
|
+
authorize! :update, Spree::Price
|
|
23
|
+
|
|
24
|
+
rows = Array(params[:prices]).map { |row| decode_price_row(row) }
|
|
25
|
+
invalid = rows.each_with_index.filter_map do |row, idx|
|
|
26
|
+
missing = %i[variant_id currency].reject { |k| row[k].present? }
|
|
27
|
+
{ index: idx, missing: missing } if missing.any?
|
|
28
|
+
end
|
|
29
|
+
if invalid.any?
|
|
30
|
+
return render_error(
|
|
31
|
+
code: 'invalid_prices',
|
|
32
|
+
message: 'Each row must include variant_id and currency.',
|
|
33
|
+
status: :unprocessable_content,
|
|
34
|
+
details: { rows: invalid }
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
foreign = foreign_rows(rows)
|
|
39
|
+
if foreign.any?
|
|
40
|
+
return render_error(
|
|
41
|
+
code: 'invalid_prices',
|
|
42
|
+
message: 'Each row must reference a variant and price list in the current store.',
|
|
43
|
+
status: :unprocessable_content,
|
|
44
|
+
details: { rows: foreign }
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
result = Spree::Prices::BulkUpsert.call(rows: rows)
|
|
49
|
+
render json: result.value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Soft-deletes the listed prices.
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
def bulk_destroy
|
|
56
|
+
authorize! :destroy, Spree::Price
|
|
57
|
+
|
|
58
|
+
destroy_scope = scope.where(id: decode_ids(params[:ids]))
|
|
59
|
+
destroyed = destroy_scope.count(&:destroy)
|
|
60
|
+
|
|
61
|
+
render json: { price_count: destroyed }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
protected
|
|
65
|
+
|
|
66
|
+
def model_class
|
|
67
|
+
Spree::Price
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def serializer_class
|
|
71
|
+
Spree.api.admin_price_serializer
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def collection_includes
|
|
75
|
+
{
|
|
76
|
+
variant: [
|
|
77
|
+
:tax_category,
|
|
78
|
+
:prices,
|
|
79
|
+
product: :tax_category,
|
|
80
|
+
option_values: :option_type,
|
|
81
|
+
stock_items: [:stock_location, :active_stock_reservations]
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Disabled: Ransack's default `result(distinct: true)` makes
|
|
87
|
+
# Postgres reject `sort=variant_product_name` because the order
|
|
88
|
+
# column isn't in the DISTINCT select list. The store scope
|
|
89
|
+
# already guarantees one Price row per result.
|
|
90
|
+
def collection_distinct?
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def permitted_params
|
|
95
|
+
normalize_params(
|
|
96
|
+
params.permit(:variant_id, :currency, :amount, :compare_at_amount, :price_list_id)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def bulk_record_count_key
|
|
103
|
+
:price_count
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def require_prices!
|
|
107
|
+
return if params.key?(:prices)
|
|
108
|
+
|
|
109
|
+
render_error(
|
|
110
|
+
code: 'missing_prices',
|
|
111
|
+
message: 'prices is required (send an empty array to no-op).',
|
|
112
|
+
status: :unprocessable_content
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def decode_price_row(row)
|
|
117
|
+
row = row.respond_to?(:to_unsafe_h) ? row.to_unsafe_h : row.to_h
|
|
118
|
+
row = row.with_indifferent_access
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
id: decode_id(row[:id]),
|
|
122
|
+
variant_id: decode_id(row[:variant_id]),
|
|
123
|
+
price_list_id: row.key?(:price_list_id) ? decode_id(row[:price_list_id]) : nil,
|
|
124
|
+
currency: row[:currency],
|
|
125
|
+
amount: row[:amount],
|
|
126
|
+
compare_at_amount: row[:compare_at_amount]
|
|
127
|
+
}.compact
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def decode_id(value)
|
|
131
|
+
return nil if value.blank?
|
|
132
|
+
|
|
133
|
+
Spree::PrefixedId.prefixed_id?(value) ? Spree::PrefixedId.decode_prefixed_id(value) : value
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Rejects rows whose variant or price list belongs to another store.
|
|
137
|
+
# `Spree::Prices::BulkUpsert` writes rows keyed on the raw variant_id
|
|
138
|
+
# with no ownership check, so the store boundary is enforced here.
|
|
139
|
+
def foreign_rows(rows)
|
|
140
|
+
variant_ids = rows.map { |r| r[:variant_id] }.compact.uniq
|
|
141
|
+
price_list_ids = rows.map { |r| r[:price_list_id] }.compact.uniq
|
|
142
|
+
|
|
143
|
+
store_variant_ids = current_store.variants.where(id: variant_ids).pluck(:id).map(&:to_s).to_set
|
|
144
|
+
store_price_list_ids = Spree::PriceList.for_store(current_store).where(id: price_list_ids).pluck(:id).map(&:to_s).to_set
|
|
145
|
+
|
|
146
|
+
rows.each_with_index.filter_map do |row, idx|
|
|
147
|
+
variant_ok = store_variant_ids.include?(row[:variant_id].to_s)
|
|
148
|
+
price_list_ok = row[:price_list_id].blank? || store_price_list_ids.include?(row[:price_list_id].to_s)
|
|
149
|
+
|
|
150
|
+
{ index: idx } unless variant_ok && price_list_ok
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
module Products
|
|
6
|
+
class VariantsController < ResourceController
|
|
7
|
+
scoped_resource :products
|
|
8
|
+
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
def model_class
|
|
12
|
+
Spree::Variant
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def serializer_class
|
|
16
|
+
Spree.api.admin_variant_serializer
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def set_parent
|
|
20
|
+
@parent = current_store.products.find_by_prefix_id!(params[:product_id])
|
|
21
|
+
authorize!(:show, @parent)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parent_association
|
|
25
|
+
:variants_including_master
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def scope_includes
|
|
29
|
+
[:prices, stock_items: :stock_location]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def permitted_params
|
|
33
|
+
params.permit(
|
|
34
|
+
:sku, :barcode, :price, :compare_at_price,
|
|
35
|
+
:cost_price, :cost_currency,
|
|
36
|
+
:weight, :height, :width, :depth, :weight_unit, :dimensions_unit,
|
|
37
|
+
:track_inventory, :tax_category_id, :position,
|
|
38
|
+
options: [:name, :value],
|
|
39
|
+
prices: [:amount, :compare_at_amount, :currency],
|
|
40
|
+
stock_items: [:stock_location_id, :count_on_hand, :backorderable]
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|