spree_api 4.3.0 → 4.4.0

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 (203) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v2/caching.rb +7 -3
  3. data/app/controllers/concerns/spree/api/v2/coupon_codes_helper.rb +29 -0
  4. data/app/controllers/concerns/spree/api/v2/number_resource.rb +11 -0
  5. data/app/controllers/concerns/spree/api/v2/platform/nested_set_reposition_concern.rb +37 -0
  6. data/app/controllers/concerns/spree/api/v2/platform/promotion_calculator_params.rb +17 -0
  7. data/app/controllers/concerns/spree/api/v2/platform/promotion_rule_params.rb +16 -0
  8. data/app/controllers/concerns/spree/api/v2/storefront/metadata_controller_concern.rb +18 -0
  9. data/app/controllers/spree/api/v1/checkouts_controller.rb +1 -1
  10. data/app/controllers/spree/api/v2/base_controller.rb +7 -5
  11. data/app/controllers/spree/api/v2/platform/adjustments_controller.rb +19 -0
  12. data/app/controllers/spree/api/v2/platform/classifications_controller.rb +1 -22
  13. data/app/controllers/spree/api/v2/platform/cms_pages_controller.rb +4 -0
  14. data/app/controllers/spree/api/v2/platform/cms_sections_controller.rb +13 -18
  15. data/app/controllers/spree/api/v2/platform/digital_links_controller.rb +25 -0
  16. data/app/controllers/spree/api/v2/platform/digitals_controller.rb +19 -0
  17. data/app/controllers/spree/api/v2/platform/line_items_controller.rb +59 -0
  18. data/app/controllers/spree/api/v2/platform/menu_items_controller.rb +5 -19
  19. data/app/controllers/spree/api/v2/platform/menus_controller.rb +0 -4
  20. data/app/controllers/spree/api/v2/platform/orders_controller.rb +163 -0
  21. data/app/controllers/spree/api/v2/platform/payment_methods_controller.rb +27 -0
  22. data/app/controllers/spree/api/v2/platform/payments_controller.rb +17 -0
  23. data/app/controllers/spree/api/v2/platform/promotion_actions_controller.rb +30 -0
  24. data/app/controllers/spree/api/v2/platform/promotion_categories_controller.rb +19 -0
  25. data/app/controllers/spree/api/v2/platform/promotion_rules_controller.rb +25 -0
  26. data/app/controllers/spree/api/v2/platform/promotions_controller.rb +31 -0
  27. data/app/controllers/spree/api/v2/platform/resource_controller.rb +47 -16
  28. data/app/controllers/spree/api/v2/platform/roles_controller.rb +15 -0
  29. data/app/controllers/spree/api/v2/platform/shipments_controller.rb +143 -0
  30. data/app/controllers/spree/api/v2/platform/shipping_categories_controller.rb +15 -0
  31. data/app/controllers/spree/api/v2/platform/shipping_methods_controller.rb +24 -0
  32. data/app/controllers/spree/api/v2/platform/states_controller.rb +19 -0
  33. data/app/controllers/spree/api/v2/platform/stock_items_controller.rb +19 -0
  34. data/app/controllers/spree/api/v2/platform/stock_locations_controller.rb +19 -0
  35. data/app/controllers/spree/api/v2/platform/store_credit_categories_controller.rb +15 -0
  36. data/app/controllers/spree/api/v2/platform/store_credit_types_controller.rb +15 -0
  37. data/app/controllers/spree/api/v2/platform/store_credits_controller.rb +19 -0
  38. data/app/controllers/spree/api/v2/platform/tax_categories_controller.rb +19 -0
  39. data/app/controllers/spree/api/v2/platform/tax_rates_controller.rb +23 -0
  40. data/app/controllers/spree/api/v2/platform/taxonomies_controller.rb +19 -0
  41. data/app/controllers/spree/api/v2/platform/taxons_controller.rb +25 -0
  42. data/app/controllers/spree/api/v2/platform/users_controller.rb +4 -0
  43. data/app/controllers/spree/api/v2/platform/variants_controller.rb +19 -0
  44. data/app/controllers/spree/api/v2/platform/webhooks/events_controller.rb +21 -0
  45. data/app/controllers/spree/api/v2/platform/webhooks/subscribers_controller.rb +21 -0
  46. data/app/controllers/spree/api/v2/platform/wished_items_controller.rb +19 -0
  47. data/app/controllers/spree/api/v2/platform/wishlists_controller.rb +19 -0
  48. data/app/controllers/spree/api/v2/platform/zones_controller.rb +19 -0
  49. data/app/controllers/spree/api/v2/resource_controller.rb +3 -3
  50. data/app/controllers/spree/api/v2/storefront/account/addresses_controller.rb +2 -2
  51. data/app/controllers/spree/api/v2/storefront/account/credit_cards_controller.rb +4 -1
  52. data/app/controllers/spree/api/v2/storefront/cart_controller.rb +18 -28
  53. data/app/controllers/spree/api/v2/storefront/checkout_controller.rb +24 -0
  54. data/app/controllers/spree/api/v2/storefront/digitals_controller.rb +54 -0
  55. data/app/controllers/spree/api/v2/storefront/wishlists_controller.rb +171 -0
  56. data/app/helpers/spree/api/v2/collection_options_helpers.rb +1 -1
  57. data/app/jobs/spree/webhooks/subscribers/make_request_job.rb +17 -0
  58. data/app/models/concerns/spree/webhooks/has_webhooks.rb +60 -0
  59. data/app/models/spree/api/webhooks/order_decorator.rb +43 -0
  60. data/app/models/spree/api/webhooks/payment_decorator.rb +26 -0
  61. data/app/models/spree/api/webhooks/product_decorator.rb +27 -0
  62. data/app/models/spree/api/webhooks/shipment_decorator.rb +21 -0
  63. data/app/models/spree/api/webhooks/stock_item_decorator.rb +43 -0
  64. data/app/models/spree/api/webhooks/stock_movement_decorator.rb +52 -0
  65. data/app/models/spree/api/webhooks/variant_decorator.rb +26 -0
  66. data/app/models/spree/oauth_access_grant.rb +7 -0
  67. data/app/models/spree/oauth_access_token.rb +7 -0
  68. data/app/models/spree/oauth_application.rb +15 -0
  69. data/app/models/spree/webhooks/base.rb +11 -0
  70. data/app/models/spree/webhooks/event.rb +12 -0
  71. data/app/models/spree/webhooks/subscriber.rb +57 -0
  72. data/app/serializers/concerns/spree/api/v2/resource_serializer_concern.rb +19 -1
  73. data/app/serializers/spree/api/v2/base_serializer.rb +11 -4
  74. data/app/serializers/spree/api/v2/platform/address_serializer.rb +1 -1
  75. data/app/serializers/spree/api/v2/platform/adjustment_serializer.rb +20 -0
  76. data/app/serializers/spree/api/v2/platform/asset_serializer.rb +13 -0
  77. data/app/serializers/spree/api/v2/platform/calculator_serializer.rb +17 -0
  78. data/app/serializers/spree/api/v2/platform/classification_serializer.rb +1 -1
  79. data/app/serializers/spree/api/v2/platform/cms_page_serializer.rb +1 -1
  80. data/app/serializers/spree/api/v2/platform/cms_section_serializer.rb +8 -1
  81. data/app/serializers/spree/api/v2/platform/country_serializer.rb +1 -1
  82. data/app/serializers/spree/api/v2/platform/credit_card_serializer.rb +14 -0
  83. data/app/serializers/spree/api/v2/platform/customer_return_serializer.rb +17 -0
  84. data/app/serializers/spree/api/v2/platform/digital_link_serializer.rb +16 -0
  85. data/app/serializers/spree/api/v2/platform/digital_serializer.rb +30 -0
  86. data/app/serializers/spree/api/v2/platform/feature_page_serializer.rb +11 -0
  87. data/app/serializers/spree/api/v2/platform/homepage_serializer.rb +11 -0
  88. data/app/serializers/spree/api/v2/platform/inventory_unit_serializer.rb +19 -0
  89. data/app/serializers/spree/api/v2/platform/line_item_serializer.rb +19 -0
  90. data/app/serializers/spree/api/v2/platform/log_entry_serializer.rb +13 -0
  91. data/app/serializers/spree/api/v2/platform/menu_item_serializer.rb +1 -1
  92. data/app/serializers/spree/api/v2/platform/menu_serializer.rb +1 -1
  93. data/app/serializers/spree/api/v2/platform/option_type_serializer.rb +1 -1
  94. data/app/serializers/spree/api/v2/platform/option_value_serializer.rb +1 -1
  95. data/app/serializers/spree/api/v2/platform/order_promotion_serializer.rb +14 -0
  96. data/app/serializers/spree/api/v2/platform/order_serializer.rb +31 -0
  97. data/app/serializers/spree/api/v2/platform/payment_capture_event_serializer.rb +13 -0
  98. data/app/serializers/spree/api/v2/platform/payment_method_serializer.rb +18 -0
  99. data/app/serializers/spree/api/v2/platform/payment_serializer.rb +21 -0
  100. data/app/serializers/spree/api/v2/platform/price_serializer.rb +19 -0
  101. data/app/serializers/spree/api/v2/platform/product_property_serializer.rb +1 -1
  102. data/app/serializers/spree/api/v2/platform/product_serializer.rb +7 -3
  103. data/app/serializers/spree/api/v2/platform/promotion_action_line_item_serializer.rb +14 -0
  104. data/app/serializers/spree/api/v2/platform/promotion_action_serializer.rb +19 -0
  105. data/app/serializers/spree/api/v2/platform/promotion_category_serializer.rb +13 -0
  106. data/app/serializers/spree/api/v2/platform/promotion_rule_serializer.rb +21 -0
  107. data/app/serializers/spree/api/v2/platform/promotion_serializer.rb +17 -0
  108. data/app/serializers/spree/api/v2/platform/property_serializer.rb +11 -0
  109. data/app/serializers/spree/api/v2/platform/prototype_serializer.rb +15 -0
  110. data/app/serializers/spree/api/v2/platform/refund_reason_serializer.rb +11 -0
  111. data/app/serializers/spree/api/v2/platform/refund_serializer.rb +16 -0
  112. data/app/serializers/spree/api/v2/platform/reimbursement_credit_serializer.rb +10 -0
  113. data/app/serializers/spree/api/v2/platform/reimbursement_serializer.rb +18 -0
  114. data/app/serializers/spree/api/v2/platform/reimbursement_type_serializer.rb +11 -0
  115. data/app/serializers/spree/api/v2/platform/return_authorization_reason_serializer.rb +11 -0
  116. data/app/serializers/spree/api/v2/platform/return_authorization_serializer.rb +17 -0
  117. data/app/serializers/spree/api/v2/platform/return_item_serializer.rb +16 -0
  118. data/app/serializers/spree/api/v2/platform/role_serializer.rb +11 -0
  119. data/app/serializers/spree/api/v2/platform/shipment_serializer.rb +22 -0
  120. data/app/serializers/spree/api/v2/platform/shipping_category_serializer.rb +11 -0
  121. data/app/serializers/spree/api/v2/platform/shipping_method_serializer.rb +16 -0
  122. data/app/serializers/spree/api/v2/platform/shipping_rate_serializer.rb +15 -0
  123. data/app/serializers/spree/api/v2/platform/standard_page_serializer.rb +11 -0
  124. data/app/serializers/spree/api/v2/platform/state_change_serializer.rb +13 -0
  125. data/app/serializers/spree/api/v2/platform/state_serializer.rb +1 -1
  126. data/app/serializers/spree/api/v2/platform/stock_item_serializer.rb +1 -3
  127. data/app/serializers/spree/api/v2/platform/stock_location_serializer.rb +2 -4
  128. data/app/serializers/spree/api/v2/platform/stock_movement_serializer.rb +11 -0
  129. data/app/serializers/spree/api/v2/platform/stock_transfer_serializer.rb +15 -0
  130. data/app/serializers/spree/api/v2/platform/store_credit_category_serializer.rb +12 -0
  131. data/app/serializers/spree/api/v2/platform/store_credit_event_serializer.rb +14 -0
  132. data/app/serializers/spree/api/v2/platform/store_credit_serializer.rb +18 -0
  133. data/app/serializers/spree/api/v2/platform/store_credit_type_serializer.rb +12 -0
  134. data/app/serializers/spree/api/v2/platform/store_serializer.rb +1 -1
  135. data/app/serializers/spree/api/v2/platform/tax_category_serializer.rb +2 -2
  136. data/app/serializers/spree/api/v2/platform/tax_rate_serializer.rb +14 -0
  137. data/app/serializers/spree/api/v2/platform/taxon_serializer.rb +1 -1
  138. data/app/serializers/spree/api/v2/platform/taxonomy_serializer.rb +1 -1
  139. data/app/serializers/spree/api/v2/platform/user_serializer.rb +1 -1
  140. data/app/serializers/spree/api/v2/platform/variant_serializer.rb +3 -2
  141. data/app/serializers/spree/api/v2/platform/webhooks/event_serializer.rb +15 -0
  142. data/app/serializers/spree/api/v2/platform/webhooks/subscriber_serializer.rb +13 -0
  143. data/app/serializers/spree/api/v2/platform/wished_item_serializer.rb +29 -0
  144. data/app/serializers/spree/api/v2/platform/wishlist_serializer.rb +19 -0
  145. data/app/serializers/spree/api/v2/platform/zone_member_serializer.rb +13 -0
  146. data/app/serializers/spree/api/v2/platform/zone_serializer.rb +13 -0
  147. data/app/serializers/spree/v2/storefront/address_serializer.rb +1 -1
  148. data/app/serializers/spree/v2/storefront/cart_serializer.rb +1 -1
  149. data/app/serializers/spree/v2/storefront/cms_section_serializer.rb +5 -1
  150. data/app/serializers/spree/v2/storefront/credit_card_serializer.rb +1 -1
  151. data/app/serializers/spree/v2/storefront/digital_link_serializer.rb +11 -0
  152. data/app/serializers/spree/v2/storefront/estimated_shipping_rate_serializer.rb +2 -2
  153. data/app/serializers/spree/v2/storefront/line_item_serializer.rb +2 -1
  154. data/app/serializers/spree/v2/storefront/option_type_serializer.rb +1 -1
  155. data/app/serializers/spree/v2/storefront/option_value_serializer.rb +1 -1
  156. data/app/serializers/spree/v2/storefront/payment_method_serializer.rb +1 -1
  157. data/app/serializers/spree/v2/storefront/payment_serializer.rb +1 -1
  158. data/app/serializers/spree/v2/storefront/product_serializer.rb +2 -2
  159. data/app/serializers/spree/v2/storefront/promotion_serializer.rb +1 -1
  160. data/app/serializers/spree/v2/storefront/shipment_serializer.rb +2 -1
  161. data/app/serializers/spree/v2/storefront/store_credit_serializer.rb +1 -1
  162. data/app/serializers/spree/v2/storefront/store_serializer.rb +1 -1
  163. data/app/serializers/spree/v2/storefront/taxon_serializer.rb +1 -1
  164. data/app/serializers/spree/v2/storefront/taxonomy_serializer.rb +1 -1
  165. data/app/serializers/spree/v2/storefront/user_serializer.rb +1 -1
  166. data/app/serializers/spree/v2/storefront/variant_serializer.rb +1 -1
  167. data/app/serializers/spree/v2/storefront/wished_item_serializer.rb +29 -0
  168. data/app/serializers/spree/v2/storefront/wishlist_serializer.rb +17 -0
  169. data/app/services/spree/webhooks/subscribers/handle_request.rb +73 -0
  170. data/app/services/spree/webhooks/subscribers/make_request.rb +82 -0
  171. data/app/services/spree/webhooks/subscribers/queue_requests.rb +17 -0
  172. data/app/services/spree/webhooks.rb +13 -0
  173. data/config/i18n-tasks.yml +40 -0
  174. data/config/initializers/doorkeeper.rb +12 -12
  175. data/config/initializers/rabl.rb +2 -2
  176. data/config/locales/en.yml +29 -27
  177. data/config/routes.rb +83 -59
  178. data/db/migrate/20210902162826_create_spree_webhooks_tables.rb +16 -0
  179. data/db/migrate/20210919183228_enable_polymorphic_resource_owner.rb +21 -0
  180. data/db/migrate/20211025162826_create_spree_webhooks_events.rb +14 -0
  181. data/docs/oauth/index.yml +126 -33
  182. data/docs/v2/platform/index.yaml +19099 -1736
  183. data/docs/v2/storefront/index.yaml +14801 -14628
  184. data/{app/models/spree → lib/spree/api}/api_dependencies.rb +56 -4
  185. data/lib/spree/api/engine.rb +19 -3
  186. data/lib/spree/api/testing_support/factories/oauth_application_factory.rb +6 -0
  187. data/lib/spree/api/testing_support/factories/webhook_event_factory.rb +27 -0
  188. data/lib/spree/api/testing_support/factories/webhook_subscriber_factory.rb +13 -0
  189. data/lib/spree/api/testing_support/factories.rb +3 -0
  190. data/lib/spree/api/testing_support/helpers.rb +1 -1
  191. data/lib/spree/api/testing_support/jobs.rb +18 -0
  192. data/lib/spree/api/testing_support/matchers/webhooks.rb +67 -0
  193. data/lib/spree/api/testing_support/serializers.rb +25 -0
  194. data/lib/spree/api/testing_support/spree_webhooks.rb +9 -0
  195. data/lib/spree/api/testing_support/v2/base.rb +1 -1
  196. data/lib/spree/api/testing_support/v2/current_order.rb +34 -1
  197. data/lib/spree/api/testing_support/v2/platform_contexts.rb +101 -52
  198. data/lib/spree/api/testing_support/v2/serializers_params.rb +3 -1
  199. data/lib/spree/api.rb +1 -0
  200. data/spec/fixtures/files/icon_256x256.jpg +0 -0
  201. data/spree_api.gemspec +16 -15
  202. metadata +175 -28
  203. data/app/controllers/spree/api/errors_controller.rb +0 -9
@@ -20,13 +20,25 @@ module Spree
20
20
  :storefront_account_create_address_service, :storefront_account_update_address_service, :storefront_address_finder,
21
21
  :storefront_account_create_service, :storefront_account_update_service, :storefront_collection_sorter, :error_handler,
22
22
  :storefront_cart_empty_service, :storefront_cart_destroy_service, :storefront_credit_cards_destroy_service, :platform_products_sorter,
23
- :storefront_cart_change_currency_service
23
+ :storefront_cart_change_currency_service, :storefront_payment_serializer,
24
+ :storefront_payment_create_service, :storefront_address_create_service, :storefront_address_update_service,
25
+ :storefront_checkout_select_shipping_method_service,
26
+
27
+ :platform_admin_user_serializer, :platform_coupon_handler, :platform_order_update_service,
28
+ :platform_order_use_store_credit_service, :platform_order_remove_store_credit_service,
29
+ :platform_order_complete_service, :platform_order_empty_service, :platform_order_destroy_service,
30
+ :platform_order_next_service, :platform_order_advance_service,
31
+ :platform_line_item_create_service, :platform_line_item_update_service, :platform_line_item_destroy_service,
32
+ :platform_order_approve_service, :platform_order_cancel_service,
33
+ :platform_shipment_change_state_service, :platform_shipment_create_service, :platform_shipment_update_service,
34
+ :platform_shipment_add_item_service, :platform_shipment_remove_item_service
24
35
  ].freeze
25
36
 
26
37
  attr_accessor *INJECTION_POINTS
27
38
 
28
39
  def initialize
29
40
  set_storefront_defaults
41
+ set_platform_defaults
30
42
  end
31
43
 
32
44
  private
@@ -58,16 +70,22 @@ module Spree
58
70
  @storefront_checkout_add_store_credit_service = Spree::Dependencies.checkout_add_store_credit_service
59
71
  @storefront_checkout_remove_store_credit_service = Spree::Dependencies.checkout_remove_store_credit_service
60
72
  @storefront_checkout_get_shipping_rates_service = Spree::Dependencies.checkout_get_shipping_rates_service
73
+ @storefront_checkout_select_shipping_method_service = Spree::Dependencies.checkout_select_shipping_method_service
61
74
 
62
75
  # account services
63
76
  @storefront_account_create_service = Spree::Dependencies.account_create_service
64
77
  @storefront_account_update_service = Spree::Dependencies.account_update_service
65
- @storefront_account_create_address_service = Spree::Dependencies.account_create_address_service
66
- @storefront_account_update_address_service = Spree::Dependencies.account_update_address_service
67
78
 
68
- # credit cards
79
+ # address services
80
+ @storefront_address_create_service = Spree::Dependencies.address_create_service
81
+ @storefront_address_update_service = Spree::Dependencies.address_update_service
82
+
83
+ # credit card services
69
84
  @storefront_credit_cards_destroy_service = Spree::Dependencies.credit_cards_destroy_service
70
85
 
86
+ # payment services
87
+ @storefront_payment_create_service = Spree::Dependencies.payment_create_service
88
+
71
89
  # serializers
72
90
  @storefront_address_serializer = 'Spree::V2::Storefront::AddressSerializer'
73
91
  @storefront_cart_serializer = 'Spree::V2::Storefront::CartSerializer'
@@ -79,6 +97,7 @@ module Spree
79
97
  @storefront_shipment_serializer = 'Spree::V2::Storefront::ShipmentSerializer'
80
98
  @storefront_taxon_serializer = 'Spree::V2::Storefront::TaxonSerializer'
81
99
  @storefront_payment_method_serializer = 'Spree::V2::Storefront::PaymentMethodSerializer'
100
+ @storefront_payment_serializer = 'Spree::V2::Storefront::PaymentSerializer'
82
101
  @storefront_product_serializer = 'Spree::V2::Storefront::ProductSerializer'
83
102
  @storefront_estimated_shipment_serializer = 'Spree::V2::Storefront::EstimatedShippingRateSerializer'
84
103
  @storefront_store_serializer = 'Spree::V2::Storefront::StoreSerializer'
@@ -107,5 +126,38 @@ module Spree
107
126
 
108
127
  @error_handler = 'Spree::Api::ErrorHandler'
109
128
  end
129
+
130
+ def set_platform_defaults
131
+ # serializers
132
+ @platform_admin_user_serializer = 'Spree::Api::V2::Platform::UserSerializer'
133
+
134
+ # coupon code handler
135
+ @platform_coupon_handler = Spree::Dependencies.coupon_handler
136
+
137
+ # order services
138
+ @platform_order_recalculate_service = Spree::Dependencies.cart_recalculate_service
139
+ @platform_order_update_service = Spree::Dependencies.checkout_update_service
140
+ @platform_order_empty_service = Spree::Dependencies.cart_empty_service
141
+ @platform_order_destroy_service = Spree::Dependencies.cart_destroy_service
142
+ @platform_order_next_service = Spree::Dependencies.checkout_next_service
143
+ @platform_order_advance_service = Spree::Dependencies.checkout_advance_service
144
+ @platform_order_complete_service = Spree::Dependencies.checkout_complete_service
145
+ @platform_order_use_store_credit_service = Spree::Dependencies.checkout_add_store_credit_service
146
+ @platform_order_remove_store_credit_service = Spree::Dependencies.checkout_remove_store_credit_service
147
+ @platform_order_approve_service = Spree::Dependencies.order_approve_service
148
+ @platform_order_cancel_service = Spree::Dependencies.order_cancel_service
149
+
150
+ # line item services
151
+ @platform_line_item_create_service = Spree::Dependencies.line_item_create_service
152
+ @platform_line_item_update_service = Spree::Dependencies.line_item_update_service
153
+ @platform_line_item_destroy_service = Spree::Dependencies.line_item_destroy_service
154
+
155
+ # shipment services
156
+ @platform_shipment_create_service = Spree::Dependencies.shipment_create_service
157
+ @platform_shipment_update_service = Spree::Dependencies.shipment_update_service
158
+ @platform_shipment_change_state_service = Spree::Dependencies.shipment_change_state_service
159
+ @platform_shipment_add_item_service = Spree::Dependencies.shipment_add_item_service
160
+ @platform_shipment_remove_item_service = Spree::Dependencies.shipment_remove_item_service
161
+ end
110
162
  end
111
163
  end
@@ -15,15 +15,31 @@ module Spree
15
15
  Migrations.new(config, engine_name).check
16
16
  end
17
17
 
18
- initializer 'spree.api.checking_deprecated_preferences' do
19
- Spree::Api::Config.deprecated_preferences.each do |pref|
20
- warn "[DEPRECATION] Spree::Api::Config[:#{pref[:name]}] is deprecated. #{pref[:message]}"
18
+ def self.activate
19
+ [
20
+ Spree::Address, Spree::Asset, Spree::CmsPage, Spree::CreditCard, Spree::CustomerReturn,
21
+ Spree::DigitalLink, Spree::Digital, Spree::InventoryUnit, Spree::LineItem, Spree::MenuItem,
22
+ Spree::Menu, Spree::OptionType, Spree::OptionValue, Spree::Order, Spree::PaymentCaptureEvent,
23
+ Spree::Payment, Spree::Price, Spree::Product, Spree::Promotion, Spree::Property, Spree::Prototype,
24
+ Spree::Refund, Spree::Reimbursement, Spree::ReturnAuthorization, Spree::ReturnItem, Spree::Role,
25
+ Spree::Shipment, Spree::ShippingCategory, Spree::ShippingMethod, Spree::ShippingRate,
26
+ Spree::StockItem, Spree::StockLocation, Spree::StockMovement, Spree::StockTransfer,
27
+ Spree::StoreCredit, Spree::Store, Spree::TaxCategory, Spree::TaxRate, Spree::Taxonomy,
28
+ Spree::Taxon, Spree::Variant, Spree::WishedItem, Spree::Wishlist, Spree::Zone
29
+ ].each do |webhookable_class|
30
+ webhookable_class.include(Spree::Webhooks::HasWebhooks)
31
+ end
32
+
33
+ Dir.glob(File.join(File.dirname(__FILE__), '../../../app/models/spree/api/webhooks/*_decorator*.rb')) do |c|
34
+ Rails.application.config.cache_classes ? require(c) : load(c)
21
35
  end
22
36
  end
23
37
 
24
38
  def self.root
25
39
  @root ||= Pathname.new(File.expand_path('../../..', __dir__))
26
40
  end
41
+
42
+ config.to_prepare &method(:activate).to_proc
27
43
  end
28
44
  end
29
45
  end
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ factory :oauth_application, class: Spree::OauthApplication do
3
+ name { "Admin Panel" }
4
+ scopes { "admin" }
5
+ end
6
+ end
@@ -0,0 +1,27 @@
1
+ FactoryBot.define do
2
+ factory :webhook_event, aliases: [:event], class: Spree::Webhooks::Event do
3
+ subscriber
4
+
5
+ execution_time { rand(1..99_999) }
6
+ name { 'order.canceled' }
7
+ request_errors { '' }
8
+ sequence(:url) { |n| "https://www.url#{n}.com/" }
9
+
10
+ trait :failed do
11
+ response_code { '500' }
12
+ success { false }
13
+ end
14
+
15
+ trait :successful do
16
+ response_code { '200' }
17
+ success { true }
18
+ end
19
+
20
+ trait :blank do
21
+ execution_time { nil }
22
+ request_errors { nil }
23
+ subscriber_id { nil }
24
+ url { nil }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ FactoryBot.define do
2
+ factory :webhook_subscriber, aliases: [:subscriber], class: Spree::Webhooks::Subscriber do
3
+ sequence(:url) { |n| "https://www.url#{n}.com/" }
4
+
5
+ trait :active do
6
+ active { true }
7
+ end
8
+
9
+ trait :inactive do
10
+ active { false }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ Dir["#{File.dirname(__FILE__)}/factories/**"].each do |f|
2
+ load File.expand_path(f)
3
+ end
@@ -25,7 +25,7 @@ module Spree
25
25
  allow(Spree.user_class).to receive(:find_by).with(hash_including(:spree_api_key)) { current_api_user }
26
26
  end
27
27
 
28
- # This method can be overriden (with a let block) inside a context
28
+ # This method can be overridden (with a let block) inside a context
29
29
  # For instance, if you wanted to have an admin user instead.
30
30
  def current_api_user
31
31
  @current_api_user ||= stub_model(Spree.user_class, email: 'spree@example.com')
@@ -0,0 +1,18 @@
1
+ RSpec.configure do |config|
2
+ # Force jobs to be executed in a synchronous way (see http://archive.today/xcb1E)
3
+ config.around do |example|
4
+ (ActiveJob::Base.descendants << ActiveJob::Base).each(&:disable_test_adapter)
5
+ ActiveJob::Base.queue_adapter = :inline
6
+ example.run
7
+ (ActiveJob::Base.descendants << ActiveJob::Base).each { |a| a.enable_test_adapter(ActiveJob::QueueAdapters::TestAdapter.new) }
8
+ ActiveJob::Base.queue_adapter = :test
9
+ end
10
+
11
+ config.before(:each, :job) do
12
+ ActiveJob::Base.queue_adapter = :test
13
+ end
14
+
15
+ config.after(:each, :job) do
16
+ ActiveJob::Base.queue_adapter = :inline
17
+ end
18
+ end
@@ -0,0 +1,67 @@
1
+ # Passes if executing the code in the block there is a
2
+ # `Spree::Webhooks::Subscribers::QueueRequests.call` method
3
+ # call with the given `event` and `webhook_payload_body` arguments just once.
4
+ #
5
+ # @example
6
+ # expect { order.complete }.to emit_webhook_event('order.paid')
7
+ # expect do
8
+ # order.start_processing
9
+ # order.complete
10
+ # end.to emit_webhook_event('order.paid')
11
+ #
12
+ # It can also be negated, resulting in the expectation
13
+ # waiting to not receive a `call` method call with the
14
+ # given `event` and `webhook_payload_body` (`once` isn't taken into consideration).
15
+ #
16
+ # @example
17
+ # expect { order.complete }.not_to emit_webhook_event('order.paid')
18
+ # expect do
19
+ # order.start_processing
20
+ # order.complete
21
+ # end.not_to emit_webhook_event('order.paid')
22
+ #
23
+ # == Notes
24
+ #
25
+ # The matcher relies on a `webhook_payload_body` method previously defined which
26
+ # isn't added to the matcher definition, because it acts in a different
27
+ # way depending on what's the resource being tested.
28
+ #
29
+ RSpec::Matchers.define :emit_webhook_event do |event_to_emit|
30
+ match do |block|
31
+ queue_requests = instance_double(Spree::Webhooks::Subscribers::QueueRequests)
32
+
33
+ allow(Spree::Webhooks::Subscribers::QueueRequests).to receive(:new).and_return(queue_requests)
34
+ allow(queue_requests).to receive(:call).with(any_args)
35
+
36
+ with_webhooks_enabled { Timecop.freeze { block.call } }
37
+
38
+ expect(queue_requests).to(
39
+ have_received(:call).with(event_name: event_to_emit, webhook_payload_body: webhook_payload_body.to_json).once
40
+ )
41
+ end
42
+
43
+ def block_definition(obj_method)
44
+ # positive look-behinds must have a fixed length, using a straightforward match instead
45
+ obj_method.source.squish[/(expect *({|do) *)(.*?)( *(}|end).(not_)*to)/, 3]
46
+ end
47
+
48
+ failure_message do |obj_method|
49
+ block_def = block_definition(obj_method)
50
+ "Expected that executing `#{block_def}` emits the `#{event_to_emit}` Webhook event.\n" \
51
+ "Check that `#{block_def}` does implement `queue_webhooks_requests!` for " \
52
+ "`#{event_to_emit}` with the following webhook_payload_body: \n\n#{webhook_payload_body}."
53
+ end
54
+
55
+ failure_message_when_negated do |obj_method|
56
+ "Expected that executing `#{block_definition(obj_method)}` does not " \
57
+ "emit the `#{event_to_emit}` Webhook event with the following webhook_payload_body: #{webhook_payload_body}."
58
+ end
59
+
60
+ supports_block_expectations
61
+ end
62
+
63
+ def with_webhooks_enabled
64
+ ENV['DISABLE_SPREE_WEBHOOKS'] = nil
65
+ yield
66
+ ENV['DISABLE_SPREE_WEBHOOKS'] = 'true'
67
+ end
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ class TestArgumentsJob < Spree::BaseJob
3
+ def perform(serializer); end
4
+ end
5
+ end
6
+
7
+ shared_examples 'an ActiveJob serializable hash' do
8
+ context 'Rails < 6', if: Rails::VERSION::MAJOR < 6 do
9
+ it 'can not be serialized by ActiveJob' do
10
+ expect { Spree::TestArgumentsJob.perform_later(subject) }.to(
11
+ raise_error(ActiveJob::SerializationError, 'Unsupported argument type: Symbol')
12
+ )
13
+ end
14
+ end
15
+
16
+ context 'Rails >= 6', if: Rails::VERSION::MAJOR >= 6 do
17
+ it 'can be serialized by ActiveJob' do
18
+ # It should fail if subject contains any custom instance (e.g Spree::Money)
19
+ expect { Spree::TestArgumentsJob.perform_later(subject) }.not_to raise_error
20
+ expect { Spree::TestArgumentsJob.perform_later(subject.merge(price: Spree::Money.new(0))) }.to(
21
+ raise_error(ActiveJob::SerializationError)
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ RSpec.configure do |config|
2
+ config.before(:each, :spree_webhooks) do
3
+ ENV['DISABLE_SPREE_WEBHOOKS'] = nil
4
+ end
5
+
6
+ config.after(:each, :spree_webhooks) do
7
+ ENV['DISABLE_SPREE_WEBHOOKS'] = 'true'
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  shared_context 'API v2 tokens' do
2
- let(:token) { Doorkeeper::AccessToken.create!(resource_owner_id: user.id, expires_in: nil) }
2
+ let(:token) { Spree::OauthAccessToken.create!(resource_owner: user, expires_in: nil) }
3
3
  let(:headers_bearer) { { 'Authorization' => "Bearer #{token.token}" } }
4
4
  let(:headers_order_token) { { 'X-Spree-Order-Token' => order.token } }
5
5
  end
@@ -7,7 +7,40 @@ shared_context 'creates order with line item' do
7
7
  let!(:line_item) { create(:line_item, order: order, currency: currency) }
8
8
  let!(:headers) { headers_bearer }
9
9
 
10
- before { ensure_order_totals }
10
+ before do
11
+ order.reload
12
+ ensure_order_totals
13
+ end
14
+ end
15
+
16
+ shared_context 'order with a physical line item' do
17
+ include_context 'creates order with line item'
18
+ end
19
+
20
+ shared_context 'order with a digital line item' do
21
+ let!(:digital) { create(:digital) }
22
+ let!(:variant_digital) { digital.variant }
23
+ let!(:line_item) { create(:line_item, variant: variant_digital, order: order, currency: currency) }
24
+ let!(:headers) { headers_bearer }
25
+
26
+ before do
27
+ order.reload
28
+ ensure_order_totals
29
+ end
30
+ end
31
+
32
+ shared_context 'order with a physical and digital line item' do
33
+ let!(:digital) { create(:digital) }
34
+ let!(:variant_digital) { digital.variant }
35
+ let!(:digital_line_item) { create(:line_item, variant: variant_digital, order: order, currency: currency) }
36
+ let!(:physical_line_item) { create(:line_item, order: order, currency: currency) }
37
+
38
+ let!(:headers) { headers_bearer }
39
+
40
+ before do
41
+ order.reload
42
+ ensure_order_totals
43
+ end
11
44
  end
12
45
 
13
46
  shared_context 'creates guest order with guest token' do
@@ -1,36 +1,48 @@
1
1
  class String
2
2
  def articleize
3
- %w(a e i o u).include?(self[0].downcase) ? "an #{self}" : "a #{self}"
3
+ if split.first == 'User'
4
+ "a #{self}"
5
+ else
6
+ %w(a e i o u).include?(self[0].downcase) ? "an #{self}" : "a #{self}"
7
+ end
4
8
  end
5
9
  end
6
10
 
7
11
  shared_context 'Platform API v2' do
8
12
  let(:store) { Spree::Store.default }
9
- let(:admin_app) { Doorkeeper::Application.find_or_create_by!(name: 'Admin Panel', scopes: 'admin', redirect_uri: '') }
10
- let(:read_app) { Doorkeeper::Application.find_or_create_by!(name: 'Read App', scopes: 'read', redirect_uri: '') }
13
+ let(:admin_app) { Spree::OauthApplication.find_or_create_by!(name: 'Admin Panel', scopes: 'admin', redirect_uri: '') }
14
+ let(:read_app) { Spree::OauthApplication.find_or_create_by!(name: 'Read App', scopes: 'read', redirect_uri: '') }
11
15
  let(:oauth_token) do
12
- Doorkeeper::AccessToken.create!(
16
+ Spree::OauthAccessToken.create!(
13
17
  application_id: admin_app.id,
14
18
  scopes: admin_app.scopes
15
19
  )
16
20
  end
17
21
  let(:read_oauth_token) do
18
- Doorkeeper::AccessToken.create!(
22
+ Spree::OauthAccessToken.create!(
19
23
  application_id: read_app.id,
20
24
  scopes: read_app.scopes
21
25
  )
22
26
  end
23
27
  let(:user_oauth_token) do
24
- Doorkeeper::AccessToken.create!(
25
- resource_owner_id: user.id,
28
+ Spree::OauthAccessToken.create!(
29
+ resource_owner: user,
26
30
  application_id: admin_app.id,
27
31
  scopes: admin_app.scopes
28
32
  )
29
33
  end
34
+ let(:user_oauth_token_without_app) do
35
+ Spree::OauthAccessToken.create!(
36
+ resource_owner: user,
37
+ scopes: 'admin'
38
+ )
39
+ end
40
+
30
41
 
31
42
  let(:valid_authorization) { "Bearer #{oauth_token.token}" }
32
43
  let(:valid_read_authorization) { "Bearer #{read_oauth_token.token}" }
33
44
  let(:valid_user_authorization) { "Bearer #{user_oauth_token.token}" }
45
+ let(:valid_user_authorization_without_app) { "Bearer #{user_oauth_token_without_app.token}" }
34
46
  let(:bogus_authorization) { "Bearer #{::Base64.strict_encode64('bogus:bogus')}" }
35
47
 
36
48
  let(:Authorization) { valid_authorization }
@@ -53,9 +65,14 @@ def json_api_include_parameter(example = '')
53
65
  parameter name: :include, in: :query, type: :string, description: JSON_API_INCLUDES_DESCRIPTION, example: example
54
66
  end
55
67
 
56
- def json_api_filter_parameter(example = '')
57
- let(:filter) { nil }
58
- parameter name: :filter, in: :query, type: :string, description: JSON_API_FILTER_DESCRIPTION, example: example
68
+ def json_api_filter_parameter(examples = [])
69
+ examples.each do |api_filter|
70
+ name = api_filter[:name].to_sym
71
+ example = api_filter[:example]
72
+ let(name) { nil }
73
+
74
+ parameter name: name, in: :query, type: :string, description: JSON_API_FILTER_DESCRIPTION, example: example
75
+ end
59
76
  end
60
77
 
61
78
  shared_examples 'authentication failed' do
@@ -95,21 +112,21 @@ shared_examples 'records returned' do
95
112
  end
96
113
 
97
114
  shared_examples 'record created' do
98
- response '201', 'record created' do
115
+ response '201', 'Record created' do
99
116
  schema '$ref' => '#/components/schemas/resource'
100
117
  run_test!
101
118
  end
102
119
  end
103
120
 
104
121
  shared_examples 'record updated' do
105
- response '200', 'record updated' do
122
+ response '200', 'Record updated' do
106
123
  schema '$ref' => '#/components/schemas/resource'
107
124
  run_test!
108
125
  end
109
126
  end
110
127
 
111
128
  shared_examples 'invalid request' do |param_name|
112
- response '422', 'invalid request' do
129
+ response '422', 'Invalid request' do
113
130
  let(param_name) { invalid_param_value }
114
131
  schema '$ref' => '#/components/schemas/validation_errors'
115
132
  run_test!
@@ -117,15 +134,17 @@ shared_examples 'invalid request' do |param_name|
117
134
  end
118
135
 
119
136
  # index action
120
- shared_examples 'GET records list' do |resource_name, include_example, filter_example|
121
- get "Returns a list of #{resource_name.pluralize}" do
122
- tags resource_name.pluralize
137
+ shared_examples 'GET records list' do |resource_name, **options|
138
+ endpoint_name = options[:custom_endpoint_name] || resource_name
139
+
140
+ get "Return a list of #{endpoint_name.pluralize}" do
141
+ tags endpoint_name.pluralize
123
142
  security [ bearer_auth: [] ]
124
- description "Returns a list of #{resource_name.pluralize}"
143
+ description "Returns a list of #{endpoint_name.pluralize}"
125
144
  operationId "#{resource_name.parameterize.pluralize.to_sym}-list"
126
145
  include_context 'jsonapi pagination'
127
- json_api_include_parameter(include_example)
128
- json_api_filter_parameter(filter_example)
146
+ json_api_include_parameter(options[:include_example]) unless options[:include_example].nil?
147
+ json_api_filter_parameter(options[:filter_examples]) unless options[:filter_examples].nil?
129
148
 
130
149
  before { records_list }
131
150
 
@@ -135,14 +154,16 @@ shared_examples 'GET records list' do |resource_name, include_example, filter_ex
135
154
  end
136
155
 
137
156
  # show
138
- shared_examples 'GET record' do |resource_name, include_example|
139
- get "Returns #{resource_name.articleize}" do
140
- tags resource_name.pluralize
157
+ shared_examples 'GET record' do |resource_name, **options|
158
+ endpoint_name = options[:custom_endpoint_name] || resource_name
159
+
160
+ get "Return #{endpoint_name.articleize}" do
161
+ tags endpoint_name.pluralize
141
162
  security [ bearer_auth: [] ]
142
- description "Returns #{resource_name.articleize}"
163
+ description "Returns #{endpoint_name.articleize}"
143
164
  operationId "show-#{resource_name.parameterize.to_sym}"
144
165
  parameter name: :id, in: :path, type: :string
145
- json_api_include_parameter(include_example)
166
+ json_api_include_parameter(options[:include_example]) unless options[:include_example].nil?
146
167
 
147
168
  it_behaves_like 'record found'
148
169
  it_behaves_like 'record not found'
@@ -151,54 +172,82 @@ shared_examples 'GET record' do |resource_name, include_example|
151
172
  end
152
173
 
153
174
  # create
154
- shared_examples 'POST create record' do |resource_name, include_example|
175
+ shared_examples 'POST create record' do |resource_name, **options|
176
+ custom_create_params = options[:custom_create_params] || nil
177
+ endpoint_name = options[:custom_endpoint_name] || resource_name
155
178
  param_name = resource_name.parameterize(separator: '_').to_sym
156
-
157
- post "Creates #{resource_name.articleize}" do
158
- tags resource_name.pluralize
159
- consumes 'application/json'
179
+ consumes_kind = options[:consumes_kind] || 'application/json'
180
+ request_data_type = case consumes_kind
181
+ when 'multipart/form-data'
182
+ :formData
183
+ else
184
+ :body
185
+ end
186
+
187
+ post "Create #{endpoint_name.articleize}" do
188
+ tags endpoint_name.pluralize
189
+ consumes consumes_kind
160
190
  security [ bearer_auth: [] ]
161
- description "Creates #{resource_name.articleize}"
191
+ description "Creates #{endpoint_name.articleize}"
162
192
  operationId "create-#{resource_name.parameterize.to_sym}"
163
- parameter name: param_name, in: :body, schema: { '$ref' => "#/components/schemas/#{param_name}_params" }
164
- json_api_include_parameter(include_example)
193
+ if custom_create_params
194
+ parameter name: param_name, in: request_data_type, schema: custom_create_params
195
+ else
196
+ parameter name: param_name, in: request_data_type, schema: { '$ref' => "#/components/schemas/create_#{param_name}_params" }
197
+ end
198
+ json_api_include_parameter(options[:include_example]) unless options[:include_example].nil?
165
199
 
166
200
  let(param_name) { valid_create_param_value }
167
201
 
168
202
  it_behaves_like 'record created'
169
- it_behaves_like 'invalid request', param_name
203
+ it_behaves_like 'invalid request', param_name unless options[:skip_invalid_params] == true
170
204
  end
171
205
  end
172
206
 
173
207
  # update
174
- shared_examples 'PUT update record' do |resource_name, include_example|
208
+ shared_examples 'PATCH update record' do |resource_name, **options|
209
+ custom_update_params = options[:custom_update_params] || nil
210
+ endpoint_name = options[:custom_endpoint_name] || resource_name
175
211
  param_name = resource_name.parameterize(separator: '_').to_sym
176
-
177
- put "Updates #{resource_name.articleize}" do
178
- tags resource_name.pluralize
212
+ consumes_kind = options[:consumes_kind] || 'application/json'
213
+ request_data_type = case consumes_kind
214
+ when 'multipart/form-data'
215
+ :formData
216
+ else
217
+ :body
218
+ end
219
+
220
+ patch "Update #{endpoint_name.articleize}" do
221
+ tags endpoint_name.pluralize
179
222
  security [ bearer_auth: [] ]
180
- description "Updates #{resource_name.articleize}"
223
+ description "Updates #{endpoint_name.articleize}"
181
224
  operationId "update-#{resource_name.parameterize.to_sym}"
182
- consumes 'application/json'
225
+ consumes consumes_kind
183
226
  parameter name: :id, in: :path, type: :string
184
- parameter name: param_name, in: :body, schema: { '$ref' => "#/components/schemas/#{param_name}_params" }
185
- json_api_include_parameter(include_example)
227
+ if custom_update_params
228
+ parameter name: param_name, in: request_data_type, schema: custom_update_params
229
+ else
230
+ parameter name: param_name, in: request_data_type, schema: { '$ref' => "#/components/schemas/update_#{param_name}_params" }
231
+ end
232
+ json_api_include_parameter(options[:include_example]) unless options[:include_example].nil?
186
233
 
187
234
  let(param_name) { valid_update_param_value }
188
235
 
189
236
  it_behaves_like 'record updated'
190
- it_behaves_like 'invalid request', param_name
237
+ it_behaves_like 'invalid request', param_name unless options[:skip_invalid_params] == true
191
238
  it_behaves_like 'record not found'
192
239
  it_behaves_like 'authentication failed'
193
240
  end
194
241
  end
195
242
 
196
243
  # destroy
197
- shared_examples 'DELETE record' do |resource_name|
198
- delete "Deletes #{resource_name.articleize}" do
199
- tags resource_name.pluralize
244
+ shared_examples 'DELETE record' do |resource_name, **options|
245
+ endpoint_name = options[:custom_endpoint_name] || resource_name
246
+
247
+ delete "Delete #{endpoint_name.articleize}" do
248
+ tags endpoint_name.pluralize
200
249
  security [ bearer_auth: [] ]
201
- description "Deletes #{resource_name.articleize}"
250
+ description "Deletes #{endpoint_name.articleize}"
202
251
  operationId "delete-#{resource_name.parameterize.to_sym}"
203
252
  parameter name: :id, in: :path, type: :string
204
253
 
@@ -208,17 +257,17 @@ shared_examples 'DELETE record' do |resource_name|
208
257
  end
209
258
  end
210
259
 
211
- shared_examples 'CRUD examples' do |resource_name, include_example, filter_example|
260
+ shared_examples 'CRUD examples' do |resource_name, **options|
212
261
  resource_path = resource_name.parameterize(separator: '_').pluralize
213
262
 
214
263
  path "/api/v2/platform/#{resource_path}" do
215
- include_examples 'GET records list', resource_name, include_example, filter_example
216
- include_examples 'POST create record', resource_name, include_example
264
+ include_examples 'GET records list', resource_name, options
265
+ include_examples 'POST create record', resource_name, options
217
266
  end
218
267
 
219
268
  path "/api/v2/platform/#{resource_path}/{id}" do
220
- include_examples 'GET record', resource_name, include_example
221
- include_examples 'PUT update record', resource_name, include_example
222
- include_examples 'DELETE record', resource_name
269
+ include_examples 'GET record', resource_name, options
270
+ include_examples 'PATCH update record', resource_name, options
271
+ include_examples 'DELETE record', resource_name, options
223
272
  end
224
273
  end
@@ -2,13 +2,15 @@ shared_context 'API v2 serializers params' do
2
2
  let(:store) { Spree::Store.default || create(:store, default: true) }
3
3
  let(:currency) { store.default_currency }
4
4
  let(:locale) { store.default_locale }
5
+ let(:zone) { Spree::Zone.default_tax || create(:zone, default_tax: true) }
5
6
 
6
7
  let(:serializer_params) do
7
8
  {
8
9
  store: store,
9
10
  currency: currency,
10
11
  user: nil,
11
- locale: locale
12
+ locale: locale,
13
+ price_options: { tax_zone: zone }
12
14
  }
13
15
  end
14
16
  end