spree_api 5.4.3 → 5.5.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +19 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
  4. data/app/controllers/concerns/spree/api/v3/admin/subclassed_resource.rb +149 -0
  5. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +54 -0
  6. data/app/controllers/concerns/spree/api/v3/bulk_operations.rb +103 -0
  7. data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +60 -0
  8. data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
  9. data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +84 -0
  10. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +88 -0
  11. data/app/controllers/concerns/spree/api/v3/store/search_provider_support.rb +35 -1
  12. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +97 -0
  13. data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +25 -0
  14. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +55 -0
  15. data/app/controllers/spree/api/v3/admin/auth_controller.rb +134 -0
  16. data/app/controllers/spree/api/v3/admin/base_controller.rb +3 -17
  17. data/app/controllers/spree/api/v3/admin/categories_controller.rb +25 -0
  18. data/app/controllers/spree/api/v3/admin/channels_controller.rb +65 -0
  19. data/app/controllers/spree/api/v3/admin/countries_controller.rb +38 -0
  20. data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +33 -0
  21. data/app/controllers/spree/api/v3/admin/custom_field_definitions_controller.rb +34 -0
  22. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +108 -0
  23. data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +31 -0
  24. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +88 -0
  25. data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +31 -0
  26. data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +93 -0
  27. data/app/controllers/spree/api/v3/admin/customers_controller.rb +119 -0
  28. data/app/controllers/spree/api/v3/admin/dashboard_controller.rb +44 -0
  29. data/app/controllers/spree/api/v3/admin/direct_uploads_controller.rb +40 -0
  30. data/app/controllers/spree/api/v3/admin/exports_controller.rb +89 -0
  31. data/app/controllers/spree/api/v3/admin/gift_card_batches_controller.rb +31 -0
  32. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +33 -0
  33. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +138 -0
  34. data/app/controllers/spree/api/v3/admin/invitations_controller.rb +70 -0
  35. data/app/controllers/spree/api/v3/admin/markets_controller.rb +42 -0
  36. data/app/controllers/spree/api/v3/admin/me_controller.rb +69 -0
  37. data/app/controllers/spree/api/v3/admin/media_controller.rb +119 -0
  38. data/app/controllers/spree/api/v3/admin/option_types_controller.rb +34 -0
  39. data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +27 -0
  40. data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +26 -0
  41. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +104 -0
  42. data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +79 -0
  43. data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +92 -0
  44. data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +90 -0
  45. data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +53 -0
  46. data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +59 -0
  47. data/app/controllers/spree/api/v3/admin/orders_controller.rb +190 -0
  48. data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +73 -0
  49. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +156 -0
  50. data/app/controllers/spree/api/v3/admin/prices_controller.rb +129 -0
  51. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +48 -0
  52. data/app/controllers/spree/api/v3/admin/products_controller.rb +237 -0
  53. data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +78 -0
  54. data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +56 -0
  55. data/app/controllers/spree/api/v3/admin/promotions_controller.rb +78 -0
  56. data/app/controllers/spree/api/v3/admin/resource_controller.rb +29 -11
  57. data/app/controllers/spree/api/v3/admin/roles_controller.rb +29 -0
  58. data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +35 -0
  59. data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +36 -0
  60. data/app/controllers/spree/api/v3/admin/stock_reservations_controller.rb +29 -0
  61. data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +75 -0
  62. data/app/controllers/spree/api/v3/admin/store_controller.rb +53 -0
  63. data/app/controllers/spree/api/v3/admin/store_credit_categories_controller.rb +21 -0
  64. data/app/controllers/spree/api/v3/admin/tags_controller.rb +51 -0
  65. data/app/controllers/spree/api/v3/admin/tax_categories_controller.rb +21 -0
  66. data/app/controllers/spree/api/v3/admin/variants_controller.rb +33 -0
  67. data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +49 -0
  68. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +75 -0
  69. data/app/controllers/spree/api/v3/resource_controller.rb +90 -8
  70. data/app/controllers/spree/api/v3/store/auth_controller.rb +8 -28
  71. data/app/controllers/spree/api/v3/store/base_controller.rb +6 -0
  72. data/app/controllers/spree/api/v3/store/carts_controller.rb +1 -0
  73. data/app/controllers/spree/api/v3/store/customers_controller.rb +6 -0
  74. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +77 -0
  75. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +2 -2
  76. data/app/controllers/spree/api/v3/store/products_controller.rb +3 -3
  77. data/app/controllers/spree/api/v3/store/resource_controller.rb +10 -2
  78. data/app/jobs/spree/webhook_delivery_job.rb +5 -0
  79. data/app/models/spree/api_key_ability.rb +16 -0
  80. data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -6
  81. data/app/serializers/spree/api/v3/admin/adjustment_serializer.rb +3 -15
  82. data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +19 -3
  83. data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +2 -6
  84. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +42 -0
  85. data/app/serializers/spree/api/v3/admin/category_serializer.rb +4 -3
  86. data/app/serializers/spree/api/v3/admin/channel_serializer.rb +15 -0
  87. data/app/serializers/spree/api/v3/admin/country_serializer.rb +1 -1
  88. data/app/serializers/spree/api/v3/admin/coupon_code_serializer.rb +30 -0
  89. data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +4 -2
  90. data/app/serializers/spree/api/v3/admin/custom_field_definition_serializer.rb +21 -0
  91. data/app/serializers/spree/api/v3/admin/custom_field_serializer.rb +8 -3
  92. data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +27 -0
  93. data/app/serializers/spree/api/v3/admin/customer_serializer.rb +58 -2
  94. data/app/serializers/spree/api/v3/admin/dashboard_analytics_serializer.rb +143 -0
  95. data/app/serializers/spree/api/v3/admin/export_serializer.rb +40 -0
  96. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -6
  97. data/app/serializers/spree/api/v3/admin/{asset_serializer.rb → gift_card_batch_serializer.rb} +1 -1
  98. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +39 -4
  99. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +64 -0
  100. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +4 -16
  101. data/app/serializers/spree/api/v3/admin/media_serializer.rb +24 -2
  102. data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +4 -1
  103. data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +4 -1
  104. data/app/serializers/spree/api/v3/admin/order_serializer.rb +21 -6
  105. data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +11 -2
  106. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +2 -6
  107. data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +4 -1
  108. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +51 -0
  109. data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +55 -0
  110. data/app/serializers/spree/api/v3/admin/price_serializer.rb +4 -0
  111. data/app/serializers/spree/api/v3/admin/product_publication_serializer.rb +11 -0
  112. data/app/serializers/spree/api/v3/admin/product_serializer.rb +34 -10
  113. data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +71 -0
  114. data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +85 -0
  115. data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +41 -0
  116. data/app/serializers/spree/api/v3/admin/refund_serializer.rb +4 -2
  117. data/app/serializers/spree/api/v3/admin/role_serializer.rb +17 -0
  118. data/app/serializers/spree/api/v3/admin/stock_item_serializer.rb +16 -1
  119. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +11 -2
  120. data/app/serializers/spree/api/v3/admin/stock_reservation_serializer.rb +46 -0
  121. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +37 -0
  122. data/app/serializers/spree/api/v3/admin/store_credit_category_serializer.rb +19 -0
  123. data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +11 -5
  124. data/app/serializers/spree/api/v3/admin/store_serializer.rb +55 -0
  125. data/app/serializers/spree/api/v3/admin/tax_category_serializer.rb +4 -2
  126. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +37 -6
  127. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +45 -0
  128. data/app/serializers/spree/api/v3/admin/webhook_endpoint_serializer.rb +69 -0
  129. data/app/serializers/spree/api/v3/channel_serializer.rb +14 -0
  130. data/app/serializers/spree/api/v3/custom_field_serializer.rb +9 -10
  131. data/app/serializers/spree/api/v3/customer_serializer.rb +5 -0
  132. data/app/serializers/spree/api/v3/market_serializer.rb +2 -1
  133. data/app/serializers/spree/api/v3/media_serializer.rb +8 -6
  134. data/app/serializers/spree/api/v3/order_serializer.rb +6 -1
  135. data/app/serializers/spree/api/v3/payment_method_serializer.rb +11 -2
  136. data/app/serializers/spree/api/v3/product_publication_serializer.rb +22 -0
  137. data/app/serializers/spree/api/v3/product_serializer.rb +6 -1
  138. data/app/serializers/spree/api/v3/stock_reservation_serializer.rb +10 -0
  139. data/config/locales/en.yml +2 -0
  140. data/config/routes.rb +235 -1
  141. data/lib/spree/api/configuration.rb +2 -2
  142. data/lib/spree/api/dependencies.rb +25 -1
  143. data/lib/spree/api/openapi/path_sorter.rb +126 -0
  144. data/lib/spree/api/openapi/schema_helper.rb +185 -6
  145. metadata +96 -8
  146. data/app/serializers/spree/api/v3/admin/shipping_category_serializer.rb +0 -14
@@ -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') }
@@ -3,7 +3,9 @@ module Spree
3
3
  module V3
4
4
  module Admin
5
5
  class MediaSerializer < V3::MediaSerializer
6
- typelize viewable_type: :string, viewable_id: :string
6
+ typelize viewable_type: :string, viewable_id: :string,
7
+ metadata: 'Record<string, unknown>',
8
+ download_url: [:string, nullable: true]
7
9
 
8
10
  attributes created_at: :iso8601, updated_at: :iso8601
9
11
 
@@ -11,7 +13,27 @@ module Spree
11
13
  asset.viewable&.prefixed_id
12
14
  end
13
15
 
14
- attributes :viewable_type
16
+ # Forces Content-Disposition: attachment so admins downloading from
17
+ # cloud storage (S3) get a save-as instead of an inline view. Mirrors
18
+ # the host resolution from the `:cdn_image` direct route since
19
+ # rails_blob_url itself doesn't fall back to Spree.cdn_host or the
20
+ # current store's domain.
21
+ attribute :download_url do |asset|
22
+ next nil unless asset.attachment&.attached?
23
+
24
+ host = Spree.cdn_host.presence ||
25
+ Rails.application.routes.default_url_options[:host] ||
26
+ Spree::Store.current&.url_or_custom_domain
27
+ helpers = Rails.application.routes.url_helpers
28
+
29
+ if host.present?
30
+ helpers.rails_blob_url(asset.attachment.blob, disposition: 'attachment', host: host)
31
+ else
32
+ helpers.rails_blob_path(asset.attachment.blob, disposition: 'attachment')
33
+ end
34
+ end
35
+
36
+ attributes :metadata, :viewable_type
15
37
  end
16
38
  end
17
39
  end