spree_api 5.4.0.beta6 → 5.4.0.beta8

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +45 -0
  4. data/app/controllers/concerns/spree/api/v3/error_handler.rb +22 -15
  5. data/app/controllers/concerns/spree/api/v3/order_lock.rb +4 -4
  6. data/app/controllers/spree/api/v3/store/carts/coupon_codes_controller.rb +57 -0
  7. data/app/controllers/spree/api/v3/store/{orders/line_items_controller.rb → carts/items_controller.rb} +18 -31
  8. data/app/controllers/spree/api/v3/store/carts/payment_methods_controller.rb +24 -0
  9. data/app/controllers/spree/api/v3/store/{orders → carts}/payment_sessions_controller.rb +14 -19
  10. data/app/controllers/spree/api/v3/store/{orders → carts}/payments_controller.rb +18 -29
  11. data/app/controllers/spree/api/v3/store/carts/shipments_controller.rb +65 -0
  12. data/app/controllers/spree/api/v3/store/{orders → carts}/store_credits_controller.rb +9 -10
  13. data/app/controllers/spree/api/v3/store/carts_controller.rb +176 -0
  14. data/app/controllers/spree/api/v3/store/customer/orders_controller.rb +1 -1
  15. data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +89 -0
  16. data/app/controllers/spree/api/v3/store/orders_controller.rb +21 -114
  17. data/app/controllers/spree/api/v3/store/products_controller.rb +1 -1
  18. data/app/controllers/spree/api/v3/webhooks/payments_controller.rb +52 -0
  19. data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +21 -0
  20. data/app/serializers/spree/api/v3/admin/media_serializer.rb +17 -0
  21. data/app/serializers/spree/api/v3/admin/order_serializer.rb +2 -2
  22. data/app/serializers/spree/api/v3/admin/product_serializer.rb +8 -4
  23. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +8 -3
  24. data/app/serializers/spree/api/v3/base_serializer.rb +1 -1
  25. data/app/serializers/spree/api/v3/cart_promotion_serializer.rb +18 -0
  26. data/app/serializers/spree/api/v3/cart_serializer.rb +56 -0
  27. data/app/serializers/spree/api/v3/media_serializer.rb +60 -0
  28. data/app/serializers/spree/api/v3/order_serializer.rb +7 -9
  29. data/app/serializers/spree/api/v3/product_serializer.rb +10 -6
  30. data/app/serializers/spree/api/v3/variant_serializer.rb +14 -9
  31. data/config/locales/en.yml +3 -0
  32. data/config/routes.rb +23 -24
  33. data/lib/spree/api/configuration.rb +1 -0
  34. data/lib/spree/api/dependencies.rb +4 -2
  35. data/lib/spree/api/openapi/schema_helper.rb +28 -0
  36. metadata +22 -17
  37. data/app/controllers/concerns/spree/api/v3/order_concern.rb +0 -46
  38. data/app/controllers/spree/api/v3/store/cart_controller.rb +0 -97
  39. data/app/controllers/spree/api/v3/store/orders/coupon_codes_controller.rb +0 -73
  40. data/app/controllers/spree/api/v3/store/orders/payment_methods_controller.rb +0 -43
  41. data/app/controllers/spree/api/v3/store/orders/shipments_controller.rb +0 -56
  42. data/app/serializers/spree/api/v3/admin/image_serializer.rb +0 -10
  43. data/app/serializers/spree/api/v3/image_serializer.rb +0 -43
@@ -0,0 +1,176 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ class CartsController < Store::ResourceController
6
+ include Spree::Api::V3::CartResolvable
7
+ include Spree::Api::V3::OrderLock
8
+
9
+ skip_before_action :set_resource
10
+ prepend_before_action :require_authentication!, only: [:index, :associate]
11
+
12
+ # GET /api/v3/store/carts/:id
13
+ # Returns cart by prefixed ID
14
+ # Auto-advances the checkout state machine so that shipments and
15
+ # payment requirements are up-to-date (temporary until Spree 6 removes
16
+ # the state machine).
17
+ def show
18
+ @cart = find_cart
19
+
20
+ if @cart.ship_address_id.present? && @cart.shipments.empty?
21
+ ActiveRecord::Base.connected_to(role: :writing) do
22
+ with_order_lock { Spree::Checkout::Advance.call(order: @cart) }
23
+ end
24
+ end
25
+
26
+ render_cart
27
+ end
28
+
29
+ # POST /api/v3/store/carts
30
+ # Creates a new shopping cart (order)
31
+ # Can be created by guests or authenticated customers
32
+ def create
33
+ result = Spree::Carts::Create.call(
34
+ params: permitted_params.merge(
35
+ user: current_user,
36
+ store: current_store,
37
+ currency: current_currency,
38
+ locale: current_locale
39
+ )
40
+ )
41
+
42
+ if result.success?
43
+ @cart = result.value
44
+ render_cart(status: :created)
45
+ else
46
+ render_service_error(result.error.to_s)
47
+ end
48
+ end
49
+
50
+ # PATCH /api/v3/store/carts/:id
51
+ # Updates cart info (email, addresses, special instructions).
52
+ # Auto-advances to the next checkout step when possible.
53
+ def update
54
+ find_cart!
55
+
56
+ with_order_lock do
57
+ result = Spree::Carts::Update.call(
58
+ cart: @cart,
59
+ params: permitted_params
60
+ )
61
+
62
+ if result.success?
63
+ render_cart
64
+ else
65
+ render_service_error(result.error, code: ERROR_CODES[:validation_error])
66
+ end
67
+ end
68
+ end
69
+
70
+ # DELETE /api/v3/store/carts/:id
71
+ # Deletes/abandons the cart
72
+ def destroy
73
+ find_cart!
74
+
75
+ result = Spree.cart_destroy_service.call(order: @cart)
76
+
77
+ if result.success?
78
+ head :no_content
79
+ else
80
+ render_service_error(result.error.to_s)
81
+ end
82
+ end
83
+
84
+ # PATCH /api/v3/store/carts/:id/associate
85
+ # Associates a guest cart with the currently authenticated user
86
+ # Requires: JWT authentication + cart ID in URL
87
+ def associate
88
+ @cart = find_cart_for_association
89
+
90
+ result = Spree.cart_associate_service.call(guest_order: @cart, user: current_user, guest_only: true)
91
+
92
+ if result.success?
93
+ render_cart
94
+ else
95
+ render_service_error(result.error.to_s)
96
+ end
97
+ end
98
+
99
+ # POST /api/v3/store/carts/:id/complete
100
+ # Completes the checkout — returns Order (not Cart).
101
+ # Idempotent: if the cart is already completed, falls back to the
102
+ # orders scope and returns the completed order.
103
+ def complete
104
+ find_cart!
105
+
106
+ result = Spree::Dependencies.carts_complete_service.constantize.call(cart: @cart)
107
+
108
+ if result.success?
109
+ @cart = result.value
110
+ render_order
111
+ else
112
+ render_service_error(
113
+ result.error.to_s.presence || 'Could not complete checkout',
114
+ code: ERROR_CODES[:cart_cannot_complete]
115
+ )
116
+ end
117
+ rescue ActiveRecord::RecordNotFound
118
+ @cart = current_store.orders.complete.find_by_prefix_id!(params[:id])
119
+ authorize!(:show, @cart, cart_token)
120
+
121
+ render_order
122
+ end
123
+
124
+ protected
125
+
126
+ def model_class
127
+ Spree::Order
128
+ end
129
+
130
+ def serializer_class
131
+ Spree.api.cart_serializer
132
+ end
133
+
134
+ def scope
135
+ current_store.carts.where(user: current_user).order(updated_at: :desc)
136
+ end
137
+
138
+ private
139
+
140
+ def permitted_params
141
+ params.permit(
142
+ :email,
143
+ :special_instructions,
144
+ :currency,
145
+ :locale,
146
+ :ship_address_id,
147
+ :bill_address_id,
148
+ ship_address: address_params,
149
+ bill_address: address_params,
150
+ metadata: {},
151
+ items: item_params
152
+ )
153
+ end
154
+
155
+ def address_params
156
+ [
157
+ :id, :firstname, :lastname, :address1, :address2,
158
+ :city, :zipcode, :phone, :company,
159
+ :country_iso, :state_abbr, :state_name, :quick_checkout
160
+ ]
161
+ end
162
+
163
+ def item_params
164
+ [:variant_id, :quantity, { metadata: {}, options: {} }]
165
+ end
166
+
167
+ # Find incomplete cart for associate action.
168
+ # Only finds guest carts (no user) or carts already owned by current user (idempotent).
169
+ def find_cart_for_association
170
+ current_store.carts.where(user: [nil, current_user]).find_by_prefix_id!(params[:id])
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -25,7 +25,7 @@ module Spree
25
25
  end
26
26
 
27
27
  def scope
28
- super.for_store(current_store)
28
+ super.for_store(current_store).complete
29
29
  end
30
30
  end
31
31
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Api
5
+ module V3
6
+ module Store
7
+ module Customer
8
+ class PasswordResetsController < Store::BaseController
9
+ rate_limit to: Spree::Api::Config[:rate_limit_password_reset],
10
+ within: Spree::Api::Config[:rate_limit_window].seconds,
11
+ store: Rails.cache,
12
+ with: RATE_LIMIT_RESPONSE
13
+
14
+ skip_before_action :authenticate_user
15
+
16
+ # POST /api/v3/store/customer/password_resets
17
+ def create
18
+ redirect_url = params[:redirect_url]
19
+
20
+ # Validate redirect_url against allowed origins (secure by default).
21
+ # If no allowed origins are configured, redirect_url is silently ignored
22
+ # to prevent open redirect / token exfiltration attacks.
23
+ if redirect_url.present?
24
+ if current_store.allowed_origins.exists? && current_store.allowed_origin?(redirect_url)
25
+ # redirect_url is valid — keep it
26
+ else
27
+ redirect_url = nil
28
+ end
29
+ end
30
+
31
+ user = Spree.user_class.find_by(email: params[:email])
32
+
33
+ if user
34
+ token = user.generate_token_for(:password_reset)
35
+ event_payload = { reset_token: token, email: user.email }
36
+ event_payload[:redirect_url] = redirect_url if redirect_url.present?
37
+ user.publish_event('customer.password_reset_requested', event_payload)
38
+ end
39
+
40
+ # Always return 202 to prevent email enumeration
41
+ render json: { message: Spree.t(:password_reset_requested, scope: :api) }, status: :accepted
42
+ end
43
+
44
+ # PATCH /api/v3/store/customer/password_resets/:id
45
+ def update
46
+ user = Spree.user_class.find_by_password_reset_token(params[:id])
47
+
48
+ unless user
49
+ return render_error(
50
+ code: ERROR_CODES[:password_reset_token_invalid],
51
+ message: Spree.t(:password_reset_token_invalid, scope: :api),
52
+ status: :unprocessable_content
53
+ )
54
+ end
55
+
56
+ if user.update(password: params[:password], password_confirmation: params[:password_confirmation])
57
+ jwt = generate_jwt(user)
58
+ user.publish_event('customer.password_reset')
59
+
60
+ render json: {
61
+ token: jwt,
62
+ user: serializer_class.new(user, params: serializer_params).to_h
63
+ }
64
+ else
65
+ render_errors(user.errors)
66
+ end
67
+ end
68
+
69
+ protected
70
+
71
+ def serializer_class
72
+ Spree.api.customer_serializer
73
+ end
74
+
75
+ def serializer_params
76
+ {
77
+ store: current_store,
78
+ locale: current_locale,
79
+ currency: current_currency,
80
+ user: nil,
81
+ includes: []
82
+ }
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -2,133 +2,40 @@ module Spree
2
2
  module Api
3
3
  module V3
4
4
  module Store
5
- class OrdersController < ResourceController
6
- include Spree::Api::V3::OrderConcern
7
- include Spree::Api::V3::OrderLock
5
+ class OrdersController < Store::BaseController
6
+ # GET /api/v3/store/orders/:id
7
+ # Single order lookup — accessible via order token (guests) or JWT (authenticated users)
8
+ before_action :find_order!
8
9
 
9
- # Skip base controller's set_resource and define our own complete list
10
- skip_before_action :set_resource
11
- before_action :set_resource, only: [:show, :update, :next, :advance, :complete]
12
-
13
- # PATCH /api/v3/store/orders/:id
14
- #
15
- # Accepts flat parameters:
16
- # {
17
- # "email": "customer@example.com",
18
- # "currency": "EUR",
19
- # "ship_address": { "firstname": "John", "country_iso": "US", ... },
20
- # "bill_address": { "firstname": "John", "country_iso": "US", ... }
21
- # }
22
- #
23
- def update
24
- with_order_lock do
25
- result = Spree.order_update_service.call(
26
- order: @order,
27
- params: order_params
28
- )
29
-
30
- if result.success?
31
- render json: serialize_resource(@order.reload)
32
- else
33
- render_service_error(result.error, code: ERROR_CODES[:validation_error])
34
- end
35
- end
36
- end
37
-
38
- # PATCH /api/v3/store/orders/:id/next
39
- def next
40
- with_order_lock do
41
- result = Spree.checkout_next_service.call(order: @order)
42
-
43
- if result.success?
44
- render json: serialize_resource(@order)
45
- else
46
- render_service_error(result.error, code: ERROR_CODES[:order_cannot_transition])
47
- end
48
- end
49
- end
50
-
51
- # PATCH /api/v3/store/orders/:id/advance
52
- def advance
53
- with_order_lock do
54
- result = Spree.checkout_advance_service.call(order: @order)
55
-
56
- if result.success?
57
- render json: serialize_resource(@order)
58
- else
59
- render_service_error(result.error, code: ERROR_CODES[:order_cannot_transition])
60
- end
61
- end
10
+ def show
11
+ render json: serializer_class.new(@order, params: serializer_params).to_h
62
12
  end
63
13
 
64
- # PATCH /api/v3/store/orders/:id/complete
65
- def complete
66
- with_order_lock do
67
- result = Spree.checkout_complete_service.call(order: @order)
14
+ private
68
15
 
69
- if result.success?
70
- render json: serialize_resource(@order)
71
- else
72
- render_service_error(result.error, code: ERROR_CODES[:order_already_completed])
73
- end
74
- end
16
+ def find_order!
17
+ @order = scope.find_by_prefix_id!(params[:id])
18
+ authorize!(:show, @order, order_token)
75
19
  end
76
20
 
77
- protected
78
-
79
- # Override scope to avoid accessible_by (Order permissions use blocks)
80
21
  def scope
81
- order_scope
82
- end
83
-
84
- # Override set_resource to scope lookup by user or order token (IDOR prevention)
85
- def set_resource
86
- @order = order_scope.find_by_prefix_id!(params[:id])
87
- @resource = @order
88
- authorize_resource!(@order)
89
- end
90
-
91
- # override authorize_resource! to pass the order token
92
- # Maps custom checkout actions to appropriate permissions
93
- def authorize_resource!(resource = @resource, action = action_name.to_sym)
94
- mapped_action = case action
95
- when :next, :advance, :complete
96
- :update # Checkout actions require update (non-completed order)
97
- else
98
- action
99
- end
100
- authorize!(mapped_action, resource, order_token)
101
- end
102
-
103
- def model_class
104
- Spree::Order
22
+ base = current_store.orders.complete
23
+
24
+ if current_user.present?
25
+ base.where(user: current_user)
26
+ elsif order_token.present?
27
+ base.where(token: order_token)
28
+ else
29
+ base.none
30
+ end
105
31
  end
106
32
 
107
33
  def serializer_class
108
34
  Spree.api.order_serializer
109
35
  end
110
36
 
111
- def order_params
112
- params.permit(
113
- :email,
114
- :currency,
115
- :locale,
116
- :special_instructions,
117
- :ship_address_id,
118
- :bill_address_id,
119
- ship_address: address_params,
120
- bill_address: address_params,
121
- metadata: {},
122
- line_items: [:variant_id, :quantity, { metadata: {}, options: {} }]
123
- )
124
- end
125
-
126
- def address_params
127
- [
128
- :id, :firstname, :lastname, :address1, :address2,
129
- :city, :zipcode, :phone, :company,
130
- :country_iso, :state_abbr, :state_name, :quick_checkout
131
- ]
37
+ def order_token
38
+ request.headers['x-spree-token']
132
39
  end
133
40
  end
134
41
  end
@@ -41,7 +41,7 @@ module Spree
41
41
  # these scopes are not automatically picked by ar_lazy_preload gem and we need to explicitly include them
42
42
  def scope_includes
43
43
  [
44
- thumbnail: [attachment_attachment: :blob],
44
+ primary_media: [attachment_attachment: :blob],
45
45
  master: [:prices, stock_items: :stock_location],
46
46
  variants: [:prices, stock_items: :stock_location]
47
47
  ]
@@ -0,0 +1,52 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Webhooks
5
+ class PaymentsController < ActionController::API
6
+ include ActionController::RateLimiting
7
+
8
+ RATE_LIMIT_RESPONSE = -> {
9
+ [429, { 'Content-Type' => 'application/json', 'Retry-After' => '60' },
10
+ [{ error: { code: 'rate_limit_exceeded', message: 'Too many requests' } }.to_json]]
11
+ }
12
+
13
+ rate_limit to: 120, within: 1.minute,
14
+ store: Rails.cache,
15
+ by: -> { request.remote_ip },
16
+ with: RATE_LIMIT_RESPONSE
17
+
18
+ # POST /api/v3/webhooks/payments/:payment_method_id
19
+ #
20
+ # Verifies the webhook signature synchronously (returns 401 if invalid),
21
+ # then enqueues async processing and returns 200 immediately.
22
+ def create
23
+ payment_method = Spree::PaymentMethod.find_by_prefix_id!(params[:payment_method_id])
24
+
25
+ # Signature verification must be synchronous — invalid = 401
26
+ result = payment_method.parse_webhook_event(request.raw_post, request.headers)
27
+
28
+ # Unsupported event — acknowledge receipt
29
+ return head :ok if result.nil?
30
+
31
+ # Process asynchronously — gateways have timeout limits and will
32
+ # retry on timeouts, so we must return 200 quickly.
33
+ Spree::Payments::HandleWebhookJob.perform_later(
34
+ payment_method_id: payment_method.id,
35
+ action: result[:action].to_s,
36
+ payment_session_id: result[:payment_session].id
37
+ )
38
+
39
+ head :ok
40
+ rescue Spree::PaymentMethod::WebhookSignatureError
41
+ head :unauthorized
42
+ rescue ActiveRecord::RecordNotFound
43
+ head :not_found
44
+ rescue StandardError => e
45
+ Rails.error.report(e, source: 'spree.webhooks.payments')
46
+ head :ok
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Api
5
+ module V3
6
+ module Admin
7
+ class AllowedOriginSerializer < V3::BaseSerializer
8
+ typelize store_id: :string
9
+
10
+ attributes :id, :origin
11
+
12
+ attribute :store_id do |allowed_origin|
13
+ allowed_origin.store&.prefixed_id
14
+ end
15
+
16
+ attributes created_at: :iso8601, updated_at: :iso8601
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class MediaSerializer < V3::MediaSerializer
6
+ typelize viewable_type: :string, viewable_id: :string
7
+
8
+ attribute :viewable_id do |asset|
9
+ asset.viewable&.prefixed_id
10
+ end
11
+
12
+ attributes :viewable_type
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -43,8 +43,8 @@ module Spree
43
43
  end
44
44
 
45
45
  # Override inherited associations to use admin serializers
46
- many :order_promotions, resource: Spree.api.admin_order_promotion_serializer, if: proc { expand?('order_promotions') }
47
- many :line_items, resource: Spree.api.admin_line_item_serializer, if: proc { expand?('line_items') }
46
+ many :order_promotions, key: :promotions, resource: Spree.api.admin_order_promotion_serializer, if: proc { expand?('promotions') }
47
+ many :line_items, key: :items, resource: Spree.api.admin_line_item_serializer, if: proc { expand?('items') }
48
48
  many :shipments, resource: Spree.api.admin_shipment_serializer, if: proc { expand?('shipments') }
49
49
  many :payments, resource: Spree.api.admin_payment_serializer, if: proc { expand?('payments') }
50
50
 
@@ -37,10 +37,14 @@ module Spree
37
37
  resource: Spree.api.admin_variant_serializer,
38
38
  if: proc { expand?('master_variant') }
39
39
 
40
- many :variant_images,
41
- key: :images,
42
- resource: Spree.api.admin_image_serializer,
43
- if: proc { expand?('images') }
40
+ one :primary_media,
41
+ resource: Spree.api.admin_media_serializer,
42
+ if: proc { expand?('primary_media') }
43
+
44
+ many :gallery_media,
45
+ key: :media,
46
+ resource: Spree.api.admin_media_serializer,
47
+ if: proc { expand?('media') }
44
48
 
45
49
  many :option_types,
46
50
  resource: Spree.api.admin_option_type_serializer,
@@ -20,9 +20,14 @@ module Spree
20
20
  end
21
21
 
22
22
  # Override inherited associations to use admin serializers
23
- many :images,
24
- resource: Spree.api.admin_image_serializer,
25
- if: proc { expand?('images') }
23
+ one :primary_media,
24
+ resource: Spree.api.admin_media_serializer,
25
+ if: proc { expand?('primary_media') }
26
+
27
+ many :gallery_media,
28
+ key: :media,
29
+ resource: Spree.api.admin_media_serializer,
30
+ if: proc { expand?('media') }
26
31
 
27
32
  many :option_values, resource: Spree.api.admin_option_value_serializer
28
33
 
@@ -38,7 +38,7 @@ module Spree
38
38
  end
39
39
 
40
40
  # Check if an association should be expanded
41
- # Supports dot notation: expand?('variants') matches both 'variants' and 'variants.images'
41
+ # Supports dot notation: expand?('variants') matches both 'variants' and 'variants.media'
42
42
  def expand?(name)
43
43
  name = name.to_s
44
44
  expands.any? { |e| e == name || e.start_with?("#{name}.") }
@@ -0,0 +1,18 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ # Cart-facing promotion serializer.
5
+ # Same data as OrderPromotionSerializer but IDs use the cp_ prefix.
6
+ class CartPromotionSerializer < BaseSerializer
7
+ typelize name: :string, description: [:string, nullable: true], code: [:string, nullable: true],
8
+ amount: :string, display_amount: :string, promotion_id: :string
9
+
10
+ attribute :promotion_id do |cart_promotion|
11
+ cart_promotion.promotion&.prefixed_id
12
+ end
13
+
14
+ attributes :name, :description, :code, :amount, :display_amount
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,56 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ # Store API Cart Serializer
5
+ # Pre-purchase cart data with checkout progression info
6
+ class CartSerializer < BaseSerializer
7
+ typelize number: :string, current_step: :string, completed_steps: 'string[]', token: :string, email: [:string, nullable: true],
8
+ special_instructions: [:string, nullable: true], currency: :string, locale: [:string, nullable: true], item_count: :number,
9
+ requirements: 'Array<{step: string, field: string, message: string}>',
10
+ item_total: :string, display_item_total: :string,
11
+ ship_total: :string, display_ship_total: :string,
12
+ adjustment_total: :string, display_adjustment_total: :string,
13
+ promo_total: :string, display_promo_total: :string,
14
+ tax_total: :string, display_tax_total: :string,
15
+ included_tax_total: :string, display_included_tax_total: :string,
16
+ additional_tax_total: :string, display_additional_tax_total: :string,
17
+ total: :string, display_total: :string,
18
+ bill_address: { nullable: true }, ship_address: { nullable: true }
19
+
20
+ # Override ID to use cart_ prefix
21
+ attribute :id do |order|
22
+ "cart_#{Spree::PrefixedId::SQIDS.encode([order.id])}"
23
+ end
24
+
25
+ attributes :number, :token, :email, :special_instructions,
26
+ :currency, :locale, :item_count,
27
+ :item_total, :display_item_total, :ship_total, :display_ship_total,
28
+ :adjustment_total, :display_adjustment_total, :promo_total, :display_promo_total,
29
+ :tax_total, :display_tax_total, :included_tax_total, :display_included_tax_total,
30
+ :additional_tax_total, :display_additional_tax_total, :total, :display_total,
31
+ created_at: :iso8601, updated_at: :iso8601
32
+
33
+ attribute :current_step do |order|
34
+ order.current_checkout_step
35
+ end
36
+
37
+ attribute :completed_steps do |order|
38
+ order.completed_checkout_steps
39
+ end
40
+
41
+ attribute :requirements do |order|
42
+ Spree::Checkout::Requirements.new(order).call
43
+ end
44
+
45
+ many :cart_promotions, key: :promotions, resource: Spree.api.cart_promotion_serializer
46
+ many :line_items, key: :items, resource: Spree.api.line_item_serializer
47
+ many :shipments, resource: Spree.api.shipment_serializer
48
+ many :payments, resource: Spree.api.payment_serializer
49
+ one :bill_address, resource: Spree.api.address_serializer
50
+ one :ship_address, resource: Spree.api.address_serializer
51
+
52
+ many :payment_methods, resource: Spree.api.payment_method_serializer
53
+ end
54
+ end
55
+ end
56
+ end