spree_api 5.4.4 → 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 (148) 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 +3 -15
  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 +6 -6
  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 +1 -6
  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/app/services/spree/webhooks/deliver_webhook.rb +2 -6
  140. data/app/subscribers/spree/webhook_event_subscriber.rb +2 -5
  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. metadata +95 -8
  148. data/app/serializers/spree/api/v3/admin/shipping_category_serializer.rb +0 -14
@@ -0,0 +1,42 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin API serializer for {Spree::ApiKey}.
6
+ #
7
+ # Never exposes `token` or `token_digest` — only the 12-char
8
+ # `token_prefix` (e.g. `sk_abc123def`) so existing keys can be
9
+ # identified in the UI without leaking material that would let an
10
+ # attacker make requests. The full plaintext token is delivered
11
+ # exactly once, as the response body of `POST /api/v3/admin/api_keys`,
12
+ # via {#plaintext_token} below — it is `nil` everywhere else.
13
+ class ApiKeySerializer < V3::BaseSerializer
14
+ typelize name: :string,
15
+ key_type: :string,
16
+ token_prefix: [:string, nullable: true],
17
+ plaintext_token: [:string, nullable: true],
18
+ scopes: [:string, multi: true],
19
+ revoked_at: [:string, nullable: true],
20
+ last_used_at: [:string, nullable: true],
21
+ created_by_email: [:string, nullable: true]
22
+
23
+ attributes :name, :key_type, :token_prefix, :scopes,
24
+ created_at: :iso8601, updated_at: :iso8601,
25
+ revoked_at: :iso8601, last_used_at: :iso8601
26
+
27
+ # Returned only on the create response — `plaintext_token` is held in
28
+ # memory on the model after `generate_token` and is never persisted
29
+ # for secret keys, so we serialize it whenever it's available rather
30
+ # than gating on the action.
31
+ attribute :plaintext_token do |key|
32
+ key.plaintext_token
33
+ end
34
+
35
+ attribute :created_by_email do |key|
36
+ key.created_by&.email
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -5,10 +5,11 @@ module Spree
5
5
  # Admin API Category Serializer
6
6
  # Full category data including admin-only fields
7
7
  class CategorySerializer < V3::CategorySerializer
8
- typelize lft: :number, rgt: :number
8
+ typelize pretty_name: :string, lft: :number, rgt: :number, sort_order: :string,
9
+ metadata: 'Record<string, unknown>'
9
10
 
10
- # Nested set columns for tree operations
11
- attributes :lft, :rgt, created_at: :iso8601, updated_at: :iso8601
11
+ # Admin-only attributes
12
+ attributes :metadata, :pretty_name, :lft, :rgt, created_at: :iso8601, updated_at: :iso8601
12
13
 
13
14
  # Override inherited associations to use admin serializers
14
15
  one :parent,
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class ChannelSerializer < V3::ChannelSerializer
6
+ typelize store_id: :string,
7
+ preferred_order_routing_strategy: [:string, nullable: true]
8
+
9
+ attributes :preferred_order_routing_strategy,
10
+ created_at: :iso8601, updated_at: :iso8601
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -7,7 +7,7 @@ module Spree
7
7
 
8
8
  many :states,
9
9
  resource: Spree.api.admin_state_serializer,
10
- if: proc { expand?(:states) }
10
+ if: proc { params[:expand]&.include?('states') }
11
11
  end
12
12
  end
13
13
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Api
5
+ module V3
6
+ module Admin
7
+ # Coupon codes belong to multi-code promotions. Read-only here:
8
+ # codes are generated server-side based on the promotion's
9
+ # `code_prefix` + `number_of_codes`.
10
+ class CouponCodeSerializer < BaseSerializer
11
+ typelize code: :string,
12
+ state: [:string, nullable: true],
13
+ promotion_id: :string,
14
+ order_id: [:string, nullable: true]
15
+
16
+ attributes :code, :state,
17
+ created_at: :iso8601, updated_at: :iso8601
18
+
19
+ attribute :promotion_id do |coupon|
20
+ coupon.promotion&.prefixed_id
21
+ end
22
+
23
+ attribute :order_id do |coupon|
24
+ coupon.order&.prefixed_id
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -4,7 +4,8 @@ module Spree
4
4
  module Admin
5
5
  class CreditCardSerializer < V3::CreditCardSerializer
6
6
  typelize customer_id: [:string, nullable: true],
7
- payment_method_id: [:string, nullable: true]
7
+ payment_method_id: [:string, nullable: true],
8
+ metadata: 'Record<string, unknown>'
8
9
 
9
10
  attribute :customer_id do |credit_card|
10
11
  credit_card.user&.prefixed_id
@@ -14,7 +15,8 @@ module Spree
14
15
  credit_card.payment_method&.prefixed_id
15
16
  end
16
17
 
17
- attributes created_at: :iso8601, updated_at: :iso8601
18
+ attributes :metadata,
19
+ created_at: :iso8601, updated_at: :iso8601
18
20
  end
19
21
  end
20
22
  end
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin API Custom Field Definition Serializer
6
+ # Schema-side metadata for custom fields (per resource type).
7
+ class CustomFieldDefinitionSerializer < BaseSerializer
8
+ typelize namespace: :string,
9
+ key: :string,
10
+ label: :string,
11
+ field_type: Spree::Metafield::FIELD_TYPE_TOKENS,
12
+ resource_type: :string,
13
+ storefront_visible: :boolean
14
+
15
+ attributes :namespace, :key, :label, :field_type, :resource_type, :storefront_visible,
16
+ created_at: :iso8601, updated_at: :iso8601
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -5,12 +5,17 @@ module Spree
5
5
  # Admin API Custom Field Serializer
6
6
  # Full custom field data including admin-only fields
7
7
  class CustomFieldSerializer < V3::CustomFieldSerializer
8
- typelize storefront_visible: :boolean
8
+ typelize storefront_visible: :boolean,
9
+ custom_field_definition_id: :string
9
10
 
10
11
  attributes created_at: :iso8601, updated_at: :iso8601
11
12
 
12
- attribute :storefront_visible do |metafield|
13
- metafield.display_on.in?(%w[both front_end])
13
+ attribute :storefront_visible do |custom_field|
14
+ custom_field.metafield_definition.available_on_front_end?
15
+ end
16
+
17
+ attribute :custom_field_definition_id do |custom_field|
18
+ custom_field.metafield_definition.prefixed_id
14
19
  end
15
20
  end
16
21
  end
@@ -0,0 +1,27 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Serializes Spree::CustomerGroup for the admin pickers
6
+ # (e.g. promotion rule, customer-group filters). Surfaces only
7
+ # what the admin UI needs to display + select rows.
8
+ class CustomerGroupSerializer < V3::BaseSerializer
9
+ typelize name: :string,
10
+ description: 'string | null',
11
+ customers_count: :number
12
+
13
+ attributes :name, :description, :customers_count,
14
+ created_at: :iso8601, updated_at: :iso8601
15
+
16
+ # Members are paginated separately via `/customers?customer_group_id_in=…`
17
+ # because a group can hold tens of thousands of users — embedding the
18
+ # whole list on every group fetch would explode the index payload.
19
+ # Pass `expand=customers` when you need them inline (single-record reads only).
20
+ many :customers,
21
+ resource: Spree.api.admin_customer_serializer,
22
+ if: proc { expand?('customers') }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -8,10 +8,19 @@ module Spree
8
8
  typelize login: [:string, nullable: true],
9
9
  last_sign_in_at: [:string, nullable: true], current_sign_in_at: [:string, nullable: true],
10
10
  sign_in_count: :number, failed_attempts: :number,
11
- last_sign_in_ip: [:string, nullable: true], current_sign_in_ip: [:string, nullable: true]
11
+ last_sign_in_ip: [:string, nullable: true], current_sign_in_ip: [:string, nullable: true],
12
+ tags: [:string, multi: true],
13
+ internal_note_html: [:string, nullable: true],
14
+ metadata: 'Record<string, unknown>',
15
+ orders_count: :number,
16
+ total_spent: :string,
17
+ display_total_spent: :string,
18
+ last_order_completed_at: [:string, nullable: true],
19
+ default_billing_address_id: [:string, nullable: true],
20
+ default_shipping_address_id: [:string, nullable: true]
12
21
 
13
22
  # Admin-only attributes
14
- attributes :login,
23
+ attributes :login, :metadata,
15
24
  last_sign_in_at: :iso8601, current_sign_in_at: :iso8601,
16
25
  created_at: :iso8601, updated_at: :iso8601
17
26
 
@@ -31,6 +40,45 @@ module Spree
31
40
  user.current_sign_in_ip
32
41
  end
33
42
 
43
+ attribute :tags do |user|
44
+ user.tag_list.to_a
45
+ end
46
+
47
+ attribute :internal_note_html do |user|
48
+ user.respond_to?(:internal_note) ? user.internal_note&.body&.to_s.presence : nil
49
+ end
50
+
51
+ attribute :default_billing_address_id do |user|
52
+ user.bill_address&.prefixed_id
53
+ end
54
+
55
+ attribute :default_shipping_address_id do |user|
56
+ user.ship_address&.prefixed_id
57
+ end
58
+
59
+ # Order aggregates: prefer attributes precomputed on the scope (see
60
+ # CustomersController#scope) to avoid N+1 on list endpoints. Fall
61
+ # back to per-user queries when not preloaded (e.g. show endpoint
62
+ # for a freshly loaded record).
63
+ attribute :orders_count do |user|
64
+ user.attributes['orders_count']&.to_i || user.orders.complete.count
65
+ end
66
+
67
+ attribute :total_spent do |user|
68
+ (user.attributes['total_spent'] || user.orders.complete.sum(:total)).to_s
69
+ end
70
+
71
+ attribute :display_total_spent do |user|
72
+ amount = user.attributes['total_spent'] || user.orders.complete.sum(:total)
73
+ currency = Spree::Current.currency || Spree::Current.store&.default_currency || Spree::Config[:currency]
74
+ Spree::Money.new(amount, currency: currency).to_s
75
+ end
76
+
77
+ attribute :last_order_completed_at do |user|
78
+ value = user.attributes.key?('last_order_completed_at') ? user.attributes['last_order_completed_at'] : user.orders.complete.maximum(:completed_at)
79
+ value.respond_to?(:iso8601) ? value.iso8601 : value
80
+ end
81
+
34
82
  # Override inherited associations to use admin serializers
35
83
  many :addresses, resource: Spree.api.admin_address_serializer, if: proc { expand?('addresses') }
36
84
  one :bill_address, key: :default_billing_address, resource: Spree.api.admin_address_serializer, if: proc { expand?('default_billing_address') }
@@ -39,6 +87,14 @@ module Spree
39
87
  many :orders,
40
88
  resource: Spree.api.admin_order_serializer,
41
89
  if: proc { expand?('orders') }
90
+
91
+ many :store_credits,
92
+ resource: Spree.api.admin_store_credit_serializer,
93
+ if: proc { expand?('store_credits') }
94
+
95
+ many :customer_groups,
96
+ resource: Spree.api.admin_customer_group_serializer,
97
+ if: proc { expand?('customer_groups') }
42
98
  end
43
99
  end
44
100
  end
@@ -0,0 +1,143 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class DashboardAnalyticsSerializer
6
+ attr_reader :store, :currency, :time_range, :params
7
+
8
+ def initialize(store:, currency:, time_range:, params: {})
9
+ @store = store
10
+ @currency = currency
11
+ @time_range = time_range
12
+ @params = params
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ currency: currency,
18
+ date_from: time_range.first.iso8601,
19
+ date_to: time_range.last.iso8601,
20
+ summary: summary,
21
+ chart_data: chart_data,
22
+ top_products: top_products
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ def orders
29
+ @orders ||= store.orders.complete.where(currency: currency, completed_at: time_range)
30
+ end
31
+
32
+ def prev_orders
33
+ @prev_orders ||= store.orders.complete.where(currency: currency, completed_at: previous_time_range)
34
+ end
35
+
36
+ def previous_time_range
37
+ duration = time_range.last - time_range.first
38
+ (time_range.first - duration)..(time_range.last - duration)
39
+ end
40
+
41
+ # ---- Summary ----
42
+
43
+ def summary
44
+ sales = sales_total
45
+ count = orders_count
46
+ avg = count > 0 ? (sales / count).round(2) : 0.0
47
+
48
+ prev_sales = prev_orders.sum(:total).to_f
49
+ prev_count = prev_orders.count
50
+ prev_avg = prev_count > 0 ? (prev_sales / prev_count).round(2) : 0.0
51
+
52
+ {
53
+ sales_total: sales.round(2),
54
+ display_sales_total: money(sales),
55
+ sales_growth: growth_rate(sales, prev_sales),
56
+ orders_count: count,
57
+ orders_growth: growth_rate(count, prev_count),
58
+ avg_order_value: avg,
59
+ display_avg_order_value: money(avg),
60
+ avg_order_value_growth: growth_rate(avg, prev_avg)
61
+ }
62
+ end
63
+
64
+ def sales_total
65
+ @sales_total ||= orders.sum(:total).to_f
66
+ end
67
+
68
+ def orders_count
69
+ @orders_count ||= orders.count
70
+ end
71
+
72
+ # ---- Chart data ----
73
+
74
+ def chart_data
75
+ daily = orders
76
+ .select("DATE(completed_at) AS day, SUM(total) AS day_total, COUNT(*) AS day_count")
77
+ .group("DATE(completed_at)")
78
+ .order("day")
79
+ .index_by { |r| r.day.to_s }
80
+
81
+ (time_range.first.to_date..time_range.last.to_date).map do |date|
82
+ key = date.to_s
83
+ row = daily[key]
84
+ total = row&.day_total.to_f
85
+ count = row&.day_count.to_i || 0
86
+ {
87
+ date: key,
88
+ sales: total.round(2),
89
+ orders: count,
90
+ avg_order_value: count > 0 ? (total / count).round(2) : 0.0
91
+ }
92
+ end
93
+ end
94
+
95
+ # ---- Top products ----
96
+
97
+ def top_products
98
+ rows = Spree::LineItem
99
+ .joins(:variant)
100
+ .where(order: orders)
101
+ .group('spree_variants.product_id')
102
+ .order(Arel.sql('SUM(spree_line_items.quantity * spree_line_items.price) DESC'))
103
+ .limit(5)
104
+ .pluck(Arel.sql('spree_variants.product_id, SUM(spree_line_items.quantity), SUM(spree_line_items.quantity * spree_line_items.price)'))
105
+
106
+ product_ids = rows.map(&:first).compact
107
+ return [] if product_ids.empty?
108
+
109
+ products = store.products.with_deleted.includes(:primary_media).where(id: product_ids)
110
+ product_serializer = Spree.api.admin_product_serializer
111
+
112
+ rows.filter_map do |product_id, quantity, amount|
113
+ product = products.find { |p| p.id == product_id }
114
+ next unless product
115
+
116
+ serialized = product_serializer.new(product, params: params).to_h
117
+ {
118
+ id: serialized['id'],
119
+ name: serialized['name'],
120
+ slug: serialized['slug'],
121
+ image_url: serialized['thumbnail_url'],
122
+ price: serialized.dig('price', 'display_amount'),
123
+ quantity: quantity.to_i,
124
+ total: money(amount)
125
+ }
126
+ end
127
+ end
128
+
129
+ # ---- Helpers ----
130
+
131
+ def money(amount)
132
+ Spree::Money.new(amount, currency: currency).to_s
133
+ end
134
+
135
+ def growth_rate(current, previous)
136
+ return 0.0 if previous.zero?
137
+ (((current - previous) / previous.to_f) * 100).round(1)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,40 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin API serializer for {Spree::Export}.
6
+ #
7
+ # `download_url` is the path to our own download endpoint, not a
8
+ # pre-signed ActiveStorage URL — the controller streams bytes inline
9
+ # so the JWT auth flow runs on every download and works through the
10
+ # SPA's `/api/*`-only dev proxy.
11
+ class ExportSerializer < V3::ExportSerializer
12
+ typelize done: :boolean,
13
+ download_url: [:string, nullable: true],
14
+ filename: [:string, nullable: true],
15
+ byte_size: [:number, nullable: true]
16
+
17
+ attribute(:done) { |export| export.done? }
18
+
19
+ # Safe-nav on `blob` — `attachment.attached?` can stay true while a
20
+ # background job purges the underlying blob (e.g. retention sweeps).
21
+ attribute :filename do |export|
22
+ export.attachment.blob&.filename&.to_s if export.done?
23
+ end
24
+
25
+ attribute :byte_size do |export|
26
+ export.attachment.blob&.byte_size if export.done?
27
+ end
28
+
29
+ attribute :download_url do |export|
30
+ next nil unless export.done?
31
+
32
+ Spree::Core::Engine.routes.url_helpers.download_api_v3_admin_export_path(
33
+ id: export.prefixed_id
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,19 +3,15 @@ module Spree
3
3
  module V3
4
4
  module Admin
5
5
  class FulfillmentSerializer < V3::FulfillmentSerializer
6
- typelize metadata: 'Record<string, unknown> | null',
6
+ typelize metadata: 'Record<string, unknown>',
7
7
  order_id: [:string, nullable: true],
8
8
  stock_location_id: [:string, nullable: true],
9
9
  adjustment_total: :string,
10
10
  pre_tax_amount: :string
11
11
 
12
- attributes :adjustment_total, :pre_tax_amount,
12
+ attributes :metadata, :adjustment_total, :pre_tax_amount,
13
13
  created_at: :iso8601, updated_at: :iso8601
14
14
 
15
- attribute :metadata do |shipment|
16
- shipment.metadata.presence
17
- end
18
-
19
15
  attribute :order_id do |shipment|
20
16
  shipment.order&.prefixed_id
21
17
  end
@@ -2,7 +2,7 @@ module Spree
2
2
  module Api
3
3
  module V3
4
4
  module Admin
5
- class AssetSerializer < V3::AssetSerializer
5
+ class GiftCardBatchSerializer < V3::GiftCardBatchSerializer
6
6
  end
7
7
  end
8
8
  end
@@ -2,14 +2,49 @@ module Spree
2
2
  module Api
3
3
  module V3
4
4
  module Admin
5
+ # Admin serializer extends the store-facing one with operational
6
+ # context the admin UI needs: who the card was issued to (customer),
7
+ # who issued it (admin), and the orders that consumed it.
5
8
  class GiftCardSerializer < V3::GiftCardSerializer
6
- typelize metadata: 'Record<string, unknown> | null'
9
+ typelize customer_id: [:string, nullable: true],
10
+ created_by_id: [:string, nullable: true]
7
11
 
8
- attribute :metadata do |gift_card|
9
- gift_card.metadata.presence
12
+ attributes created_at: :iso8601, updated_at: :iso8601
13
+
14
+ attribute :customer_id do |gift_card|
15
+ gift_card.user&.prefixed_id
10
16
  end
11
17
 
12
- attributes created_at: :iso8601, updated_at: :iso8601
18
+ attribute :created_by_id do |gift_card|
19
+ gift_card.created_by&.prefixed_id
20
+ end
21
+
22
+ # Customer the card was issued to. Gated behind `expand?` to keep
23
+ # the list payload thin — the SPA's list view passes
24
+ # `expand=customer,created_by` to populate the row chips.
25
+ one :user,
26
+ key: :customer,
27
+ resource: Spree.api.admin_customer_serializer,
28
+ if: proc { expand?('customer') }
29
+
30
+ # Admin who issued the card.
31
+ one :created_by,
32
+ resource: Spree.api.admin_admin_user_serializer,
33
+ if: proc { expand?('created_by') }
34
+
35
+ # Batch the card was issued as part of (bulk-issue flow). The
36
+ # `Spree::GiftCard#batch` association is keyed off
37
+ # `gift_card_batch_id`; we rename the JSON field to match that
38
+ # column for read/write symmetry.
39
+ one :batch,
40
+ key: :gift_card_batch,
41
+ resource: Spree.api.admin_gift_card_batch_serializer,
42
+ if: proc { expand?('gift_card_batch') }
43
+
44
+ # Orders that consumed the card. Detail-only — pass `expand=orders`.
45
+ many :orders,
46
+ resource: Spree.api.admin_order_serializer,
47
+ if: proc { expand?('orders') }
13
48
  end
14
49
  end
15
50
  end
@@ -0,0 +1,64 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin API serializer for {Spree::Invitation}. Used on the staff
6
+ # settings page to list pending invitations and to surface the result
7
+ # of `POST /admin/invitations`. Inviter and invitee are flattened to
8
+ # email-only — the full polymorphic identities aren't useful to the UI.
9
+ class InvitationSerializer < V3::BaseSerializer
10
+ typelize email: :string,
11
+ status: :string,
12
+ role_id: :string,
13
+ role_name: :string,
14
+ inviter_email: :string,
15
+ expires_at: :string,
16
+ acceptance_url: :string,
17
+ invitee_exists: :boolean,
18
+ store: '{ id: string; name: string }'
19
+
20
+ attributes :email, :status,
21
+ created_at: :iso8601, updated_at: :iso8601, expires_at: :iso8601
22
+
23
+ # `role`, `inviter` are `validates ... presence: true` on the model,
24
+ # so they're guaranteed non-null for any persisted invitation.
25
+ attribute :role_id do |invitation|
26
+ invitation.role.prefixed_id
27
+ end
28
+
29
+ attribute :role_name do |invitation|
30
+ invitation.role.name
31
+ end
32
+
33
+ attribute :inviter_email do |invitation|
34
+ invitation.inviter.email
35
+ end
36
+
37
+ # Absolute URL when `Spree::Config[:admin_url]` is set, otherwise
38
+ # the path so the SPA can prepend `window.location.origin`.
39
+ attribute :acceptance_url do |invitation|
40
+ if Spree::Config[:admin_url].present?
41
+ Rails.application.routes.url_helpers.admin_invitation_acceptance_url(invitation)
42
+ else
43
+ "/accept-invitation/#{invitation.prefixed_id}?token=#{invitation.token}"
44
+ end
45
+ end
46
+
47
+ # Drives the SPA's sign-in vs sign-up branch on the acceptance page.
48
+ # Looked up by email so an admin who's already on another store sees
49
+ # the password prompt, not a fresh account form.
50
+ attribute :invitee_exists do |invitation|
51
+ Spree.admin_user_class.exists?(email: invitation.email)
52
+ end
53
+
54
+ # Minimal store identity for the unauthenticated acceptance page's
55
+ # title ("Join <store>"). Full Store would over-expose internals to
56
+ # a public landing page; this is the smallest shape that renders.
57
+ attribute :store do |invitation|
58
+ { id: invitation.store.prefixed_id, name: invitation.store.name }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -5,16 +5,12 @@ module Spree
5
5
  # Admin API Line Item Serializer
6
6
  # Extends the store serializer with metadata visibility
7
7
  class LineItemSerializer < V3::LineItemSerializer
8
- typelize metadata: 'Record<string, unknown> | null',
8
+ typelize metadata: 'Record<string, unknown>',
9
9
  cost_price: [:string, nullable: true],
10
- tax_category_id: [:string, nullable: true],
11
- order_id: [:string, nullable: true]
10
+ tax_category_id: [:string, nullable: true]
12
11
 
13
- attributes created_at: :iso8601, updated_at: :iso8601
14
-
15
- attribute :metadata do |line_item|
16
- line_item.metadata.presence
17
- end
12
+ attributes :metadata,
13
+ created_at: :iso8601, updated_at: :iso8601
18
14
 
19
15
  attribute :cost_price do |line_item|
20
16
  line_item.cost_price&.to_s
@@ -24,18 +20,10 @@ module Spree
24
20
  line_item.tax_category&.prefixed_id
25
21
  end
26
22
 
27
- attribute :order_id do |line_item|
28
- line_item.order&.prefixed_id
29
- end
30
-
31
23
  # Override inherited associations to use admin serializers
32
24
  many :option_values, resource: Spree.api.admin_option_value_serializer
33
25
  many :digital_links, resource: Spree.api.admin_digital_link_serializer
34
26
 
35
- one :order,
36
- resource: Spree.api.admin_order_serializer,
37
- if: proc { expand?('order') }
38
-
39
27
  one :variant,
40
28
  resource: Spree.api.admin_variant_serializer,
41
29
  if: proc { expand?('variant') }