spree_cm_commissioner 2.13.7 → 2.14.1

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 (232) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test_and_build_gem.yml +5 -3
  3. data/Gemfile.lock +1 -1
  4. data/app/assets/javascripts/spree_cm_commissioner/backend.js +1 -1
  5. data/app/assets/javascripts/spree_cm_commissioner/promotion_rule_pickers.js +12 -0
  6. data/app/assets/stylesheets/spree_cm_commissioner/backend/calendar.scss +0 -39
  7. data/app/controllers/concerns/spree/admin/rates_availability_cells.rb +16 -0
  8. data/app/controllers/concerns/spree/api/v2/product_list_includes_decorator.rb +1 -0
  9. data/app/controllers/spree/admin/guests_controller.rb +2 -1
  10. data/app/controllers/spree/admin/integrations_controller.rb +16 -7
  11. data/app/controllers/spree/admin/inventory_items_controller.rb +33 -62
  12. data/app/controllers/spree/admin/product_option_types_controller.rb +88 -0
  13. data/app/controllers/spree/admin/rate_plans_controller.rb +125 -0
  14. data/app/controllers/spree/admin/rates_availabilities_controller.rb +142 -0
  15. data/app/controllers/spree/admin/stock_managements_controller.rb +1 -53
  16. data/app/controllers/spree/api/switch_cm/base_controller.rb +109 -0
  17. data/app/controllers/spree/api/switch_cm/tokens_controller.rb +68 -0
  18. data/app/controllers/spree/api/switch_cm/xml/availabilities_controller.rb +39 -0
  19. data/app/controllers/spree/api/switch_cm/xml/base_controller.rb +37 -0
  20. data/app/controllers/spree/api/switch_cm/xml/reservation_summaries_controller.rb +28 -0
  21. data/app/controllers/spree/api/switch_cm/xml/reservations_controller.rb +57 -0
  22. data/app/controllers/spree/api/switch_cm/xml/room_rate_availabilities_controller.rb +56 -0
  23. data/app/controllers/spree/api/switch_cm/xml/room_rates_controller.rb +45 -0
  24. data/app/controllers/spree/api/v2/operator/guests_controller.rb +1 -1
  25. data/app/controllers/spree/api/v2/platform/homepage_section_relatable_options_controller.rb +1 -0
  26. data/app/controllers/spree/api/v2/storefront/accommodations/variants_controller.rb +31 -5
  27. data/app/controllers/spree/api/v2/storefront/inventory_items_controller.rb +2 -2
  28. data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +0 -1
  29. data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +1 -1
  30. data/app/finders/spree_cm_commissioner/accommodations/find.rb +10 -8
  31. data/app/finders/spree_cm_commissioner/accommodations/find_variant.rb +5 -11
  32. data/app/finders/spree_cm_commissioner/rates_availabilities/find_bookings.rb +116 -0
  33. data/app/helpers/spree_cm_commissioner/admin/inventory_items_helper.rb +36 -0
  34. data/app/helpers/spree_cm_commissioner/admin/rates_availabilities_helper.rb +247 -0
  35. data/app/interactors/spree_cm_commissioner/intercity_taxi_order_creator.rb +0 -1
  36. data/app/jobs/spree_cm_commissioner/option_type_variants_public_metadata_updater_job.rb +1 -0
  37. data/app/jobs/spree_cm_commissioner/option_value_variants_public_metadata_updater_job.rb +1 -0
  38. data/app/jobs/spree_cm_commissioner/product_variants_public_metadata_updater_job.rb +12 -0
  39. data/app/models/concerns/spree_cm_commissioner/kyc_bitwise.rb +4 -0
  40. data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +1 -10
  41. data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +33 -3
  42. data/app/models/concerns/spree_cm_commissioner/line_item_rate_plannable.rb +55 -0
  43. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +0 -16
  44. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +22 -4
  45. data/app/models/concerns/spree_cm_commissioner/rate_plan_options_concern.rb +62 -0
  46. data/app/models/concerns/spree_cm_commissioner/scoped_fee_rule.rb +1 -6
  47. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +12 -0
  48. data/app/models/concerns/spree_cm_commissioner/variant_rate_plannable.rb +81 -0
  49. data/app/models/spree_cm_commissioner/address_decorator.rb +2 -0
  50. data/app/models/spree_cm_commissioner/configuration.rb +0 -6
  51. data/app/models/spree_cm_commissioner/exports/import_order_guests_csv.rb +2 -1
  52. data/app/models/spree_cm_commissioner/exports/operator_guest_json_gzip.rb +8 -2
  53. data/app/models/spree_cm_commissioner/guest.rb +1 -1
  54. data/app/models/spree_cm_commissioner/integration.rb +15 -0
  55. data/app/models/spree_cm_commissioner/integrations/switch_cm.rb +96 -0
  56. data/app/models/spree_cm_commissioner/inventory_item.rb +32 -3
  57. data/app/models/spree_cm_commissioner/line_item_decorator.rb +7 -7
  58. data/app/models/spree_cm_commissioner/maintenance_tasks/cache_invalidation.rb +20 -4
  59. data/app/models/spree_cm_commissioner/maintenance_tasks/event.rb +3 -2
  60. data/app/models/spree_cm_commissioner/option_type_decorator.rb +2 -1
  61. data/app/models/spree_cm_commissioner/option_value_decorator.rb +3 -0
  62. data/app/models/spree_cm_commissioner/option_value_rate_plan.rb +12 -0
  63. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
  64. data/app/models/spree_cm_commissioner/price_decorator.rb +13 -0
  65. data/app/models/spree_cm_commissioner/product_decorator.rb +23 -2
  66. data/app/models/spree_cm_commissioner/product_option_type_decorator.rb +42 -0
  67. data/app/models/spree_cm_commissioner/rate_plan.rb +139 -0
  68. data/app/models/spree_cm_commissioner/rate_plan_options.rb +36 -0
  69. data/app/models/spree_cm_commissioner/rate_restriction.rb +45 -0
  70. data/app/models/spree_cm_commissioner/relatable_option_label.rb +20 -0
  71. data/app/models/spree_cm_commissioner/taxon_decorator.rb +5 -2
  72. data/app/models/spree_cm_commissioner/variant_decorator.rb +36 -5
  73. data/app/models/spree_cm_commissioner/variant_options.rb +3 -2
  74. data/app/models/spree_cm_commissioner/vendor_decorator.rb +16 -0
  75. data/app/overrides/spree/admin/option_types/_form/checkbox.html.erb.deface +0 -8
  76. data/app/overrides/spree/admin/option_types/_form/name.html.erb.deface +1 -1
  77. data/app/overrides/spree/admin/products/_filters/vendor_filter.html.erb.deface +6 -0
  78. data/app/overrides/spree/admin/shared/_product_tabs/rates_availabilities.html.erb.deface +8 -0
  79. data/app/overrides/spree/admin/shared/_product_tabs/stock_managements.html.erb.deface +1 -1
  80. data/app/overrides/spree/admin/variants/index/discontinued_badge.html.erb.deface +13 -0
  81. data/app/overrides/spree/admin/variants/index/option_types_empty_state_link.html.erb.deface +3 -0
  82. data/app/overrides/spree/admin/variants/index/option_types_summary.html.erb.deface +24 -0
  83. data/app/presenters/spree/variants/visible_options_presenter.rb +34 -1
  84. data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +40 -22
  85. data/app/queries/spree_cm_commissioner/single_leg_trips_query.rb +28 -14
  86. data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_update_schema.rb +0 -1
  87. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +2 -0
  88. data/app/serializers/spree/v2/storefront/option_type_serializer_decorator.rb +1 -1
  89. data/app/serializers/spree/v2/storefront/product_option_type_serializer.rb +15 -0
  90. data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +15 -0
  91. data/app/serializers/spree/v2/tenant/line_item_serializer.rb +1 -1
  92. data/app/serializers/spree/v2/tenant/option_type_serializer.rb +1 -1
  93. data/app/serializers/spree_cm_commissioner/api/v2/platform/homepage_section_relatable_options_serializer.rb +2 -2
  94. data/app/serializers/spree_cm_commissioner/v2/operator/dynamic_field_option_serializer.rb +9 -0
  95. data/app/serializers/spree_cm_commissioner/v2/operator/dynamic_field_serializer.rb +11 -0
  96. data/app/serializers/spree_cm_commissioner/v2/operator/guest_dynamic_field_serializer.rb +14 -0
  97. data/app/serializers/spree_cm_commissioner/v2/operator/guest_serializer.rb +1 -0
  98. data/app/serializers/spree_cm_commissioner/v2/storefront/accommodation_variant_serializer.rb +44 -0
  99. data/app/serializers/spree_cm_commissioner/v2/storefront/inventory_item_serializer.rb +4 -4
  100. data/app/serializers/spree_cm_commissioner/v2/storefront/rate_plan_serializer.rb +16 -0
  101. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/client.rb +11 -0
  102. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_items.rb +16 -3
  103. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/unstock_inventory.rb +35 -5
  104. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_item_availability.rb +1 -1
  105. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_locations.rb +39 -7
  106. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_product.rb +1 -1
  107. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_routes.rb +31 -13
  108. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_seat_layout.rb +20 -2
  109. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_service_calendars.rb +18 -10
  110. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +7 -2
  111. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_variant.rb +9 -2
  112. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_vehicle_types.rb +12 -1
  113. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/trip.rb +8 -1
  114. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/trip_location.rb +32 -0
  115. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/full_sync_strategy.rb +2 -1
  116. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_item_availability.rb +1 -1
  117. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/accept_push.rb +111 -0
  118. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/accommodation_lock.rb +43 -0
  119. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/apply_updates.rb +133 -0
  120. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/availability_snapshot.rb +127 -0
  121. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/inventory_update.rb +35 -0
  122. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/pushed_nights.rb +82 -0
  123. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/unseen_room_nights.rb +49 -0
  124. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/validate_updates.rb +153 -0
  125. data/app/services/spree_cm_commissioner/integrations/switch_cm/reservations/feed.rb +209 -0
  126. data/app/services/spree_cm_commissioner/integrations/switch_cm/reservations/mark_seen.rb +62 -0
  127. data/app/services/spree_cm_commissioner/integrations/switch_cm/reservations/reservation.rb +171 -0
  128. data/app/services/spree_cm_commissioner/integrations/switch_cm/room_catalog.rb +88 -0
  129. data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_manager.rb +16 -0
  130. data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/full_sync_strategy.rb +39 -0
  131. data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/incremental_sync_strategy.rb +51 -0
  132. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/availability_parser.rb +103 -0
  133. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/push_response.rb +45 -0
  134. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/reservation_serializer.rb +83 -0
  135. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/response_builder.rb +19 -0
  136. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_item_availability.rb +1 -1
  137. data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +1 -3
  138. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_by_variant.rb +4 -0
  139. data/app/services/spree_cm_commissioner/inventory_items/reset.rb +4 -0
  140. data/app/services/spree_cm_commissioner/rate_plans/save.rb +65 -0
  141. data/app/services/spree_cm_commissioner/rates_availabilities/bulk_update.rb +225 -0
  142. data/app/services/spree_cm_commissioner/reschedule_requests/split_line_item.rb +5 -0
  143. data/app/services/spree_cm_commissioner/room_types/create.rb +94 -0
  144. data/app/services/spree_cm_commissioner/room_types/update_properties.rb +32 -0
  145. data/app/services/spree_cm_commissioner/room_types/update_room_variant.rb +22 -0
  146. data/app/services/spree_cm_commissioner/room_types.rb +49 -0
  147. data/app/services/spree_cm_commissioner/route_prices/update_price_range.rb +3 -1
  148. data/app/services/spree_cm_commissioner/trips/search.rb +14 -7
  149. data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +5 -1
  150. data/app/views/spree/admin/guests/_check_in_status.html.erb +28 -22
  151. data/app/views/spree/admin/guests/edit.html.erb +29 -26
  152. data/app/views/spree/admin/integrations/_form.html.erb +4 -0
  153. data/app/views/spree/admin/integrations/_switch_cm_fields.html.erb +53 -0
  154. data/app/views/spree/admin/inventory_items/_details.html.erb +56 -0
  155. data/app/views/spree/admin/product_option_types/index.html.erb +79 -0
  156. data/app/views/spree/admin/promotions/rules/_product.html.erb +25 -0
  157. data/app/views/spree/admin/promotions/rules/_user.html.erb +21 -0
  158. data/app/views/spree/admin/promotions/rules/_vendors.html.erb +14 -1
  159. data/app/views/spree/admin/rate_plans/_form.html.erb +134 -0
  160. data/app/views/spree/admin/rate_plans/_order_form.html.erb +37 -0
  161. data/app/views/spree/admin/rates_availabilities/_block.html.erb +34 -0
  162. data/app/views/spree/admin/rates_availabilities/_bookings.html.erb +52 -0
  163. data/app/views/spree/admin/rates_availabilities/_filters.html.erb +48 -0
  164. data/app/views/spree/admin/rates_availabilities/_legend.html.erb +27 -0
  165. data/app/views/spree/admin/rates_availabilities/_list_pages.html.erb +14 -0
  166. data/app/views/spree/admin/rates_availabilities/_modal_header.html.erb +11 -0
  167. data/app/views/spree/admin/rates_availabilities/_names.html.erb +62 -0
  168. data/app/views/spree/admin/rates_availabilities/_script.html.erb +676 -0
  169. data/app/views/spree/admin/rates_availabilities/_styles.html.erb +159 -0
  170. data/app/views/spree/admin/rates_availabilities/index.html.erb +112 -0
  171. data/app/views/spree/admin/shared/sub_menu/_homepage.html.erb +0 -1
  172. data/app/views/spree/admin/stock_managements/_header.html.erb +0 -14
  173. data/app/views/spree/admin/stock_managements/_inventory_item_stats.html.erb +28 -0
  174. data/app/views/spree/admin/stock_managements/index.html.erb +1 -56
  175. data/config/initializers/spree_permitted_attributes.rb +1 -0
  176. data/config/locales/en.yml +246 -3
  177. data/config/locales/km.yml +246 -4
  178. data/config/routes.rb +36 -17
  179. data/db/migrate/20260928000000_add_oauth_application_id_to_cm_integrations.rb +5 -0
  180. data/db/migrate/20260929000000_create_cm_rate_plans.rb +22 -0
  181. data/db/migrate/20260929000001_add_rate_plan_to_spree_prices.rb +24 -0
  182. data/db/migrate/20260929000002_add_rate_plan_to_spree_line_items.rb +7 -0
  183. data/db/migrate/20260929000003_create_cm_option_value_rate_plans.rb +16 -0
  184. data/db/migrate/20261009000000_create_cm_rate_restrictions.rb +29 -0
  185. data/db/migrate/20261013000001_add_hidden_and_presentation_to_spree_product_option_types.rb +6 -0
  186. data/db/migrate/20261013000002_move_hidden_from_spree_option_types_to_product_option_types.rb +36 -0
  187. data/db/migrate/20261016000001_add_index_to_spree_line_items_number.rb +8 -0
  188. data/lib/spree_cm_commissioner/booking_dates.rb +43 -0
  189. data/lib/spree_cm_commissioner/calendar_event.rb +0 -25
  190. data/lib/spree_cm_commissioner/rate.rb +74 -0
  191. data/lib/spree_cm_commissioner/rate_closings.rb +48 -0
  192. data/lib/spree_cm_commissioner/rates_availability_grid.rb +113 -0
  193. data/lib/spree_cm_commissioner/stay_price.rb +30 -0
  194. data/lib/spree_cm_commissioner/stay_prices.rb +58 -0
  195. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +6 -0
  196. data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +6 -0
  197. data/lib/spree_cm_commissioner/test_helper/factories/option_value_rate_plan_factory.rb +6 -0
  198. data/lib/spree_cm_commissioner/test_helper/factories/rate_plan_factory.rb +14 -0
  199. data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +9 -0
  200. data/lib/spree_cm_commissioner/version.rb +1 -1
  201. data/lib/spree_cm_commissioner.rb +6 -0
  202. metadata +100 -32
  203. data/app/assets/javascripts/spree_cm_commissioner/calendar.js +0 -59
  204. data/app/controllers/spree/admin/calendars/base_controller.rb +0 -27
  205. data/app/controllers/spree/admin/calendars/orders_controller.rb +0 -27
  206. data/app/controllers/spree/admin/homepage_feed_controller.rb +0 -36
  207. data/app/controllers/spree/admin/metafields_controller.rb +0 -69
  208. data/app/models/concerns/spree_cm_commissioner/metafield.rb +0 -15
  209. data/app/models/spree_cm_commissioner/homepage_feed.rb +0 -4
  210. data/app/overrides/spree/admin/shared/_main_menu/sidebar_calendars.html.erb.deface +0 -5
  211. data/app/overrides/spree/admin/shared/_product_tabs/metafields.html.erb.deface +0 -15
  212. data/app/serializers/spree_cm_commissioner/v2/storefront/taxon_include_product_serializer.rb +0 -13
  213. data/app/serializers/spree_cm_commissioner/v2/storefront/vendor_include_product_serializer.rb +0 -14
  214. data/app/services/spree_cm_commissioner/feed/taxon_include_product.rb +0 -20
  215. data/app/services/spree_cm_commissioner/feed/taxon_product.rb +0 -54
  216. data/app/services/spree_cm_commissioner/feed/vendor_include_product.rb +0 -20
  217. data/app/services/spree_cm_commissioner/feed/vendor_product.rb +0 -52
  218. data/app/services/spree_cm_commissioner/homepage_data_loader.rb +0 -112
  219. data/app/services/spree_cm_commissioner/metafields/product_metadata_service.rb +0 -32
  220. data/app/views/shared/_calendar.html.erb +0 -27
  221. data/app/views/shared/calendar/_order_popover.html.erb +0 -18
  222. data/app/views/shared/calendar/_orders_sidebar.html.erb +0 -41
  223. data/app/views/spree/admin/calendars/orders/index.html.erb +0 -22
  224. data/app/views/spree/admin/homepage_feed/_form.html.erb +0 -24
  225. data/app/views/spree/admin/homepage_feed/edit.html.erb +0 -17
  226. data/app/views/spree/admin/inventory_items/prices.html.erb +0 -43
  227. data/app/views/spree/admin/inventory_items/stocks.html.erb +0 -34
  228. data/app/views/spree/admin/metafields/_private_metafields_form.html.erb +0 -71
  229. data/app/views/spree/admin/metafields/_public_metafields_form.html.erb +0 -64
  230. data/app/views/spree/admin/metafields/edit.html.erb +0 -27
  231. data/app/views/spree/admin/shared/sub_menu/_calendar.html.erb +0 -3
  232. data/app/views/spree/admin/stock_managements/calendar.html.erb +0 -310
@@ -0,0 +1,142 @@
1
+ module Spree
2
+ module Admin
3
+ # Stock, prices and sale status of a product's variants and their rate plans, per date.
4
+ # StockManagementsController keeps per-variant setup: stock locations, track inventory, purchases, resets.
5
+ class RatesAvailabilitiesController < Spree::Admin::BaseController
6
+ include RatesAvailabilityCells
7
+
8
+ MIN_DAYS_AHEAD = 14
9
+ MAX_BLOCK_DAYS = 62
10
+ VARIANTS_PER_PAGE = 25
11
+ BOOKINGS_PER_PAGE = 25
12
+
13
+ before_action :load_product
14
+ before_action :load_grid, only: %i[index dates]
15
+
16
+ helper SpreeCmCommissioner::Admin::RatesAvailabilitiesHelper
17
+ helper_method :filter_params
18
+
19
+ # GET /products/:product_id/rates_availabilities?from=&currency=&option_value_ids[]=&archived=&page=
20
+ def index
21
+ @option_types = Spree::Variants::OptionTypesFinder.new(variant_ids: @product.variants.ids).execute.select(&:variant?)
22
+ @currencies = supported_currencies_for_all_stores.map(&:iso_code)
23
+ # A channel manager sets these variants' stock, prices and closings, so BulkUpdate skips them; the page says why.
24
+ @managing_integration = @product.vendor&.integration if @grid.variants.any?(&:inventory_external_wins?)
25
+ end
26
+
27
+ # GET /products/:product_id/rates_availabilities/dates?from=&to=&<index filters>
28
+ #
29
+ # The page adds this block next to the dates it already shows, so a range across months doesn't need a reload.
30
+ # It asks for whole months, so a month never spans two blocks.
31
+ def dates
32
+ render partial: 'block', locals: { grid: @grid }
33
+ end
34
+
35
+ # PATCH /products/:product_id/rates_availabilities/bulk_update, JSON: { bulk_action:, cells: [...], values: {...} }
36
+ #
37
+ # Answers with the counts, and the page then reloads its loaded dates.
38
+ def bulk_update
39
+ result = SpreeCmCommissioner::RatesAvailabilities::BulkUpdate.call(
40
+ product: @product,
41
+ store: current_store,
42
+ action: params[:bulk_action].to_s,
43
+ cells: selected_cells,
44
+ values: params.fetch(:values, {}).permit(:amount, :compare_at_amount, :quantity, :currency),
45
+ user: try_spree_current_user
46
+ )
47
+
48
+ if result.success?
49
+ render json: result.value
50
+ else
51
+ render json: { error: result.error.to_s }, status: :unprocessable_entity
52
+ end
53
+ end
54
+
55
+ # POST /products/:product_id/rates_availabilities/bookings, JSON: { cells: [...], filter:, q:, page: }
56
+ #
57
+ # The bookings modal for the selected cells. POST, as cells travel in the body.
58
+ def bookings
59
+ finder = SpreeCmCommissioner::RatesAvailabilities::FindBookings.new(
60
+ product: @product,
61
+ cells: selected_cells,
62
+ filter: params[:filter].to_s,
63
+ query: params[:q]
64
+ )
65
+
66
+ line_items = finder.execute.page(params[:page]).per(BOOKINGS_PER_PAGE)
67
+ render partial: 'bookings', formats: [:html], locals: {
68
+ finder: finder,
69
+ line_items: line_items,
70
+ cells: selected_cells,
71
+ held_order_ids: finder.held_order_ids_among(line_items)
72
+ }
73
+ end
74
+
75
+ private
76
+
77
+ def filter_params
78
+ params.permit(:currency, :archived, option_value_ids: []).to_h
79
+ end
80
+
81
+ def load_product
82
+ @product = Spree::Product.find_by!(slug: params[:product_id])
83
+ end
84
+
85
+ def load_grid
86
+ @include_archived = params[:archived] == '1'
87
+ @variants = variants.page(params[:page]).per(VARIANTS_PER_PAGE)
88
+ @currency = (params[:currency].presence || current_currency).upcase
89
+ @from = parse_date(params[:from]) || Time.zone.today
90
+
91
+ dates = @product.permanent_stock? ? date_window.to_a : []
92
+
93
+ @grid = SpreeCmCommissioner::RatesAvailabilityGrid.new(
94
+ variants: @variants.to_a,
95
+ dates: dates,
96
+ currency: @currency,
97
+ include_archived: @include_archived
98
+ )
99
+ end
100
+
101
+ def variants
102
+ rate_plans = @include_archived ? :rate_plans : :active_rate_plans
103
+ scope = @product.variants.exists? ? @product.variants : Spree::Variant.where(id: @product.master.id)
104
+ scope = scope.where(id: matching_variant_ids) if option_value_ids.present?
105
+ scope.includes(:stock_items, { rate_plans => { option_values: :option_type } }, { vendor: :integration }, option_values: :option_type)
106
+ .order(:position, :id)
107
+ end
108
+
109
+ def option_value_ids
110
+ @option_value_ids ||= Array(params[:option_value_ids]).compact_blank.map(&:to_i)
111
+ end
112
+
113
+ def matching_variant_ids
114
+ SpreeCmCommissioner::Variants::FindByOptionValueIds.new(product: @product, option_value_ids: option_value_ids).execute.ids
115
+ end
116
+
117
+ # The first window is `from`'s month, plus the next one when fewer than MIN_DAYS_AHEAD days are left, so a
118
+ # late `from` still shows weeks ahead. Whole months, so the blocks added next to it are whole months too.
119
+ # Kept to about a month: every day is a column on every row, and a dated product can have many variants.
120
+ # The page scrolls to `from`.
121
+ def date_window
122
+ to = parse_date(params[:to])
123
+ return @from..[to, @from + (MAX_BLOCK_DAYS - 1)].min if to
124
+
125
+ last = @from.end_of_month
126
+ last = @from.next_month.end_of_month if (last - @from).to_i + 1 < MIN_DAYS_AHEAD
127
+ @from.beginning_of_month..last
128
+ end
129
+
130
+ def parse_date(value)
131
+ Date.parse(value.to_s) if value.present?
132
+ rescue Date::Error
133
+ nil
134
+ end
135
+
136
+ # Same permission as the stock calendar this replaces.
137
+ def model_class
138
+ Spree::StockItem
139
+ end
140
+ end
141
+ end
142
+ end
@@ -5,7 +5,7 @@ module Spree
5
5
 
6
6
  before_action :load_parent
7
7
 
8
- helper_method :inventory_item_message, :inventory_item_hold_message
8
+ helper SpreeCmCommissioner::Admin::InventoryItemsHelper
9
9
 
10
10
  def load_parent
11
11
  @product = Spree::Product.find_by(slug: params[:product_id])
@@ -40,58 +40,6 @@ module Spree
40
40
  redirect_back fallback_location: admin_product_stock_managements_path(@product)
41
41
  end
42
42
 
43
- # GET /products/:slug/stock_managements/calendar?year=&selected_option_value_ids[]=
44
- def calendar
45
- @year = params[:year].present? ? params[:year].to_i : Time.zone.today.year
46
-
47
- from_date = Date.new(@year, 1, 1).beginning_of_year
48
- to_date = Date.new(@year, 1, 1).end_of_year
49
-
50
- @variant_kind_option_types = Spree::Variants::OptionTypesFinder.new(variant_ids: @product.variants.ids)
51
- .execute.select(&:variant?)
52
- @selected_option_value_ids = Array(params[:selected_option_value_ids]).map(&:to_i)
53
-
54
- inventory_items = @product.inventory_items.includes(:variant, :prices).where(
55
- inventory_date: from_date..to_date
56
- )
57
-
58
- if @selected_option_value_ids.present?
59
- variant_ids = SpreeCmCommissioner::Variants::FindByOptionValueIds.new(
60
- product: @product, option_value_ids: @selected_option_value_ids
61
- ).execute.ids
62
- inventory_items = inventory_items.where(variant_id: variant_ids)
63
- @matched_variants_count = variant_ids.size
64
- else
65
- @matched_variants_count = @product.variants.count
66
- end
67
-
68
- @inventory_items = inventory_items.to_a
69
-
70
- @cached_inventory_items = ::SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(@inventory_items)
71
- .call
72
- .index_by(&:inventory_item_id)
73
- @events = SpreeCmCommissioner::CalendarEvent.from_inventory_items(@inventory_items)
74
- end
75
-
76
- def inventory_item_message(inventory_item, cached_inventory_item)
77
- synced = inventory_item.quantity_available == cached_inventory_item.quantity_available
78
-
79
- if synced
80
- "Synced: Quantity available matches in both DB and Redis (#{cached_inventory_item.quantity_available})."
81
- else
82
- "Out of sync: Redis shows #{cached_inventory_item.quantity_available} available, which doesn't match the database."
83
- end
84
- end
85
-
86
- def inventory_item_hold_message(inventory_item, cached_inventory_item)
87
- synced = inventory_item.quantity_on_hold == cached_inventory_item.quantity_on_hold
88
- if synced
89
- "Synced: Quantity on hold matches in both DB and Redis (#{cached_inventory_item.quantity_on_hold})."
90
- else
91
- "Out of sync: Redis shows #{cached_inventory_item.quantity_on_hold} on hold, which doesn't match the database."
92
- end
93
- end
94
-
95
43
  private
96
44
 
97
45
  def load_inventories
@@ -0,0 +1,109 @@
1
+ module Spree::Api::SwitchCm
2
+ # The Booking.com-compatible API channel managers (switch.cm) call under /api/cm.
3
+ # It's built on ActionController::API rather than Spree's v2 base, whose rescue handlers
4
+ # answer in JSON:API: a Booking.com connector parses every answer, errors included, as its
5
+ # family's XML, which Xml::BaseController renders.
6
+ #
7
+ # One Doorkeeper app per channel manager, presented as a Bearer token from the token
8
+ # exchange or as HTTP Basic with the app's client id and secret. Booking.com accepted Basic
9
+ # until 31 Dec 2025, and an older connector may still send it.
10
+ class BaseController < ActionController::API
11
+ include ActionController::HttpAuthentication::Basic::ControllerMethods
12
+
13
+ # Bodies are parsed whole and pushes applied in the request, so both are capped; the cap on
14
+ # updates is AcceptPush's.
15
+ MAX_BODY_BYTES = 1.megabyte
16
+
17
+ before_action :authenticate_switch_cm!
18
+ before_action :reject_oversized_body!
19
+
20
+ rescue_from Nokogiri::XML::SyntaxError do
21
+ render_fault(:bad_request, 'Request body is not well-formed XML')
22
+ end
23
+
24
+ private
25
+
26
+ def authenticate_switch_cm!
27
+ @oauth_application = application_from_bearer || application_from_basic
28
+ return if @oauth_application.present? && switch_cm_integrations.exists?
29
+
30
+ render_fault(:unauthorized, 'Authorization Required')
31
+ end
32
+
33
+ def reject_oversized_body!
34
+ return if request.raw_post.bytesize <= MAX_BODY_BYTES
35
+
36
+ render_fault(:payload_too_large, "Request body must not exceed #{MAX_BODY_BYTES / 1.megabyte} MB")
37
+ end
38
+
39
+ def application_from_bearer
40
+ doorkeeper_token.application if doorkeeper_token&.accessible?
41
+ end
42
+
43
+ def application_from_basic
44
+ authenticate_with_http_basic do |client_id, client_secret|
45
+ application = Spree::OauthApplication.find_by(uid: client_id)
46
+ application if application&.secret_matches?(client_secret)
47
+ end
48
+ end
49
+
50
+ def switch_cm_integrations
51
+ SpreeCmCommissioner::Integrations::SwitchCm.readable.where(oauth_application_id: @oauth_application.id)
52
+ end
53
+
54
+ # nil unless the requested hotel is one of this channel manager's connected hotels.
55
+ def current_integration
56
+ return @current_integration if defined?(@current_integration)
57
+
58
+ accommodation_id = accommodation_id_from_request
59
+ @current_integration = accommodation_id.present? ? switch_cm_integrations.find_by(vendor_id: accommodation_id) : nil
60
+ end
61
+
62
+ def require_integration!
63
+ render_fault(:forbidden, 'Not authorized for this hotel') if current_integration.nil?
64
+ end
65
+
66
+ # Pushes need an active connection: a paused one is still being set up, and the hotel keeps
67
+ # selling on its own counts until it's activated.
68
+ def require_active_integration!
69
+ return render_fault(:forbidden, 'Not authorized for this hotel') if current_integration.nil?
70
+
71
+ render_fault(:forbidden, 'This hotel is not active on BookMe+ yet') unless current_integration.active?
72
+ end
73
+
74
+ # Pushes may leave out the hotel id (B.XML never sends one); Booking.com derives it from the
75
+ # rooms. A room of another channel manager's hotel resolves to a hotel this app can't access.
76
+ def vendor_id_of_room(room_id)
77
+ Spree::Variant.where(id: room_id.to_i).pick(:vendor_id) if room_id.present?
78
+ end
79
+
80
+ # Reservation pulls take a comma-separated list of hotel ids, or none for all of this
81
+ # channel manager's hotels. Unknown ids come back separately, for a warning each, as at
82
+ # Booking.com; a list with no known hotel at all is refused.
83
+ def requested_integrations(hotel_ids)
84
+ ids = hotel_ids.to_s.split(',').map(&:strip).compact_blank
85
+ scope = switch_cm_integrations.includes(:vendor).order(:vendor_id)
86
+ return [scope.to_a, []] if ids.empty?
87
+
88
+ integrations = scope.where(vendor_id: ids).to_a
89
+ [integrations, ids - integrations.map { |integration| integration.vendor_id.to_s }]
90
+ end
91
+
92
+ def accommodation_id_from_request
93
+ raise NotImplementedError, "#{self.class.name} must implement accommodation_id_from_request"
94
+ end
95
+
96
+ def render_fault(_status, _text, code: nil)
97
+ raise NotImplementedError, "#{self.class.name} must implement render_fault"
98
+ end
99
+
100
+ # Rails parses neither XML nor text/xml bodies into params, so both families read the raw body.
101
+ def request_document
102
+ @request_document ||= Nokogiri::XML(request.raw_post) { |config| config.strict.nonet }.tap(&:remove_namespaces!)
103
+ end
104
+
105
+ def ruid
106
+ request.request_id
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,68 @@
1
+ module Spree::Api::SwitchCm
2
+ class TokensController < BaseController
3
+ # Booking.com's tokens also last an hour; connectors refresh on that schedule.
4
+ TOKEN_LIFETIME = 1.hour
5
+
6
+ skip_before_action :authenticate_switch_cm!
7
+
8
+ # POST /api/cm/token-based-authentication/exchange
9
+ # Client id and secret in (JSON or XML), { jwt, ruid } out, then `Authorization: Bearer` on every
10
+ # other call. The value is a Doorkeeper access token rather than a signed JWT, so a connector
11
+ # that only forwards it works but one that decodes it won't.
12
+ def create
13
+ client_id, client_secret = credentials
14
+ return render_fault(:bad_request, 'client_id and client_secret are required') if client_id.blank? || client_secret.blank?
15
+
16
+ @oauth_application = Spree::OauthApplication.find_by(uid: client_id)
17
+ unless @oauth_application&.secret_matches?(client_secret) && switch_cm_integrations.exists?
18
+ return render_fault(:unauthorized, 'Invalid client credentials')
19
+ end
20
+
21
+ render_token(issue_token.plaintext_token)
22
+ end
23
+
24
+ private
25
+
26
+ def issue_token
27
+ Spree::OauthAccessToken.create_for(
28
+ application: @oauth_application,
29
+ resource_owner: nil,
30
+ scopes: '',
31
+ expires_in: TOKEN_LIFETIME.to_i,
32
+ use_refresh_token: false
33
+ )
34
+ end
35
+
36
+ def credentials
37
+ return [params[:client_id], params[:client_secret]] unless xml_request?
38
+
39
+ %w[client_id client_secret].map { |name| request_document.at_xpath("/request/#{name}")&.text&.strip }
40
+ end
41
+
42
+ def xml_request?
43
+ request.content_mime_type&.symbol == :xml
44
+ end
45
+
46
+ def render_token(token)
47
+ return render json: { jwt: token, ruid: ruid } unless xml_request?
48
+
49
+ document = SpreeCmCommissioner::Integrations::SwitchCm::Xml::ResponseBuilder.build('response', ruid: ruid) do |xml|
50
+ xml.jwt token
51
+ xml.ruid ruid
52
+ end
53
+ render xml: document
54
+ end
55
+
56
+ # override
57
+ def render_fault(status, text, code: nil)
58
+ code ||= Rack::Utils.status_code(status)
59
+
60
+ if xml_request?
61
+ render xml: SpreeCmCommissioner::Integrations::SwitchCm::Xml::ResponseBuilder.fault('response', code: code, text: text, ruid: ruid),
62
+ status: status
63
+ else
64
+ render json: { errors: [{ code: code, message: text }], ruid: ruid }, status: status
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,39 @@
1
+ module Spree::Api::SwitchCm
2
+ module Xml
3
+ class AvailabilitiesController < BaseController
4
+ before_action :require_active_integration!
5
+
6
+ # POST /api/cm/hotels/xml/availability
7
+ # Rooms to sell, prices and closed in one message, validated and applied in the request
8
+ # (Inventory::AcceptPush); the answer is the real outcome.
9
+ def create
10
+ result = SpreeCmCommissioner::Integrations::SwitchCm::Inventory::AcceptPush.call(
11
+ integration: current_integration,
12
+ message: 'availability',
13
+ updates: parser.updates,
14
+ request_id: ruid
15
+ )
16
+
17
+ push_response = SpreeCmCommissioner::Integrations::SwitchCm::Xml::PushResponse.new(result: result.value, ruid: ruid)
18
+ render xml: push_response.to_xml, status: push_response.http_status
19
+ end
20
+
21
+ private
22
+
23
+ # override
24
+ def response_root
25
+ 'availability'
26
+ end
27
+
28
+ def parser
29
+ @parser ||= SpreeCmCommissioner::Integrations::SwitchCm::Xml::AvailabilityParser.new(request_document)
30
+ end
31
+
32
+ # B.XML pushes carry no hotel id; the rooms name it.
33
+ # override
34
+ def accommodation_id_from_request
35
+ vendor_id_of_room(parser.first_room_id)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ module Spree::Api::SwitchCm
2
+ module Xml
3
+ # Booking.com's B.XML family (/hotels/xml/...): a request that fails as a whole answers with
4
+ # a <fault code string/> under the call's root element. Requests carry their parameters
5
+ # in a <request> body.
6
+ class BaseController < Spree::Api::SwitchCm::BaseController
7
+ private
8
+
9
+ def response_root
10
+ raise NotImplementedError, "#{self.class.name} must implement response_root"
11
+ end
12
+
13
+ # override
14
+ def render_fault(status, text, code: nil)
15
+ code ||= Rack::Utils.status_code(status)
16
+ render xml: SpreeCmCommissioner::Integrations::SwitchCm::Xml::ResponseBuilder.fault(response_root, code: code, text: text, ruid: ruid),
17
+ status: status
18
+ end
19
+
20
+ def render_document(&block)
21
+ render xml: SpreeCmCommissioner::Integrations::SwitchCm::Xml::ResponseBuilder.build(response_root, ruid: ruid, &block)
22
+ end
23
+
24
+ # override
25
+ def accommodation_id_from_request
26
+ request_value('hotel_id')
27
+ end
28
+
29
+ # A parameter from the <request> body, falling back to the query string.
30
+ def request_value(name)
31
+ return params[name] if request.raw_post.blank?
32
+
33
+ request_document.at_xpath("/request/#{name}")&.text&.strip.presence || params[name]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ module Spree::Api::SwitchCm
2
+ module Xml
3
+ class ReservationSummariesController < BaseController
4
+ before_action :require_integration!
5
+
6
+ # POST /api/cm/hotels/xml/reservationssummary with <request><hotel_id/></request>
7
+ # The hotel's confirmed stays checking out from tomorrow on, however long ago they were
8
+ # booked. The channel manager imports these while setting the hotel up, so they count against
9
+ # its availability before its first push. Read-only: nothing is marked seen.
10
+ def create
11
+ reservations = SpreeCmCommissioner::Integrations::SwitchCm::Reservations::Feed.new([current_integration]).summary
12
+
13
+ render_document do |xml|
14
+ reservations.each do |reservation|
15
+ SpreeCmCommissioner::Integrations::SwitchCm::Xml::ReservationSerializer.new(reservation, summary: true).build(xml)
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ # override
23
+ def response_root
24
+ 'reservations'
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ module Spree::Api::SwitchCm
2
+ module Xml
3
+ class ReservationsController < BaseController
4
+ # POST /api/cm/hotels/xml/reservations with <request><hotel_id/><last_change/><limit/><id/></request>
5
+ # New, modified and cancelled reservations in one stream. B.XML has no acknowledgement, so returning a
6
+ # reservation is what marks it seen; `last_change` re-reads recent ones regardless.
7
+ def create
8
+ integrations, = requested_integrations(request_value('hotel_id') || request_value('hotel_ids'))
9
+ return render_fault(:forbidden, 'Not authorized for these hotels') if integrations.empty?
10
+
11
+ reservations = SpreeCmCommissioner::Integrations::SwitchCm::Reservations::Feed.new(integrations).changed_reservations(
12
+ id: request_value('id'),
13
+ last_change: SpreeCmCommissioner::Integrations::SwitchCm::Reservations::Feed.parse_last_change(request_value('last_change')),
14
+ limit: request_value('limit')
15
+ )
16
+
17
+ document = SpreeCmCommissioner::Integrations::SwitchCm::Xml::ResponseBuilder.build(response_root, ruid: ruid) do |xml|
18
+ reservations.each { |reservation| SpreeCmCommissioner::Integrations::SwitchCm::Xml::ReservationSerializer.new(reservation).build(xml) }
19
+ end
20
+
21
+ # Marked only once the document is built. One that fails to mark is simply returned
22
+ # again next time, which is harmless: its status is the same.
23
+ failed = mark_seen(reservations)
24
+ log_mark_seen_failures(failed) if failed.any?
25
+
26
+ render xml: document
27
+ end
28
+
29
+ private
30
+
31
+ # override
32
+ def response_root
33
+ 'reservations'
34
+ end
35
+
36
+ def mark_seen(reservations)
37
+ reservations.each_with_object([]) do |reservation, failed|
38
+ SpreeCmCommissioner::Integrations::SwitchCm::Reservations::MarkSeen.call(
39
+ integration: reservation.integration, order: reservation.order, status: reservation.status,
40
+ modified_at: reservation.modified_at
41
+ )
42
+ rescue StandardError => e
43
+ failed << {
44
+ order_id: reservation.id,
45
+ integration_id: reservation.integration.id,
46
+ error_class: e.class.name,
47
+ error_message: e.message
48
+ }
49
+ end
50
+ end
51
+
52
+ def log_mark_seen_failures(failed)
53
+ CmAppLogger.error(label: "#{self.class.name}#mark_seen failed", data: { ruid: ruid, failed: failed })
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,56 @@
1
+ module Spree::Api::SwitchCm
2
+ module Xml
3
+ class RoomRateAvailabilitiesController < BaseController
4
+ DAYS_RANGE = (1..31)
5
+
6
+ before_action :require_integration!
7
+
8
+ # POST /api/cm/hotels/xml/roomrateavailability with <request><hotel_id/><start_date/><number_of_days/><room_id/></request>
9
+ # What we hold per room and night, so the channel manager can check its pushes landed.
10
+ # number_of_days is 1-31, 31 by default, as at Booking.com.
11
+ def create
12
+ start_date = parse_date(request_value('start_date')) || Time.zone.today
13
+ days = (request_value('number_of_days').presence || DAYS_RANGE.max).to_i
14
+ return render_fault(:bad_request, "number_of_days must be between #{DAYS_RANGE.min} and #{DAYS_RANGE.max}") unless DAYS_RANGE.cover?(days)
15
+
16
+ rows = SpreeCmCommissioner::Integrations::SwitchCm::Inventory::AvailabilitySnapshot.new(
17
+ integration: current_integration,
18
+ dates: (start_date...(start_date + days)).to_a,
19
+ room_id: request_value('room_id')
20
+ ).rows
21
+
22
+ render_document { |xml| rows.each { |row| build_row(xml, row) } }
23
+ end
24
+
25
+ private
26
+
27
+ # override
28
+ def response_root
29
+ 'result'
30
+ end
31
+
32
+ def build_row(xml, row)
33
+ xml.roomrate(
34
+ {
35
+ date: row.date.iso8601,
36
+ room_id: row.room_id,
37
+ rate_id: row.rate_id,
38
+ rooms_to_sell: row.rooms_to_sell,
39
+ booked: row.booked,
40
+ cancelled: row.cancelled,
41
+ closed: row.closed ? 1 : 0,
42
+ price: row.price && format('%.2f', row.price),
43
+ currency: row.currency,
44
+ pushed_at: row.pushed_at&.utc&.iso8601
45
+ }.compact
46
+ )
47
+ end
48
+
49
+ def parse_date(value)
50
+ Date.iso8601(value) if value.present?
51
+ rescue Date::Error
52
+ nil
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,45 @@
1
+ module Spree::Api::SwitchCm
2
+ module Xml
3
+ class RoomRatesController < BaseController
4
+ before_action :require_integration!
5
+
6
+ # POST /api/cm/hotels/xml/roomrates with <request><hotel_id/></request>
7
+ # The hotel's rooms, each with its rates, which the channel manager maps to its own
8
+ # ("Refresh Room" in switch.cm). A derived rate plan is a child rate of the room's base rate.
9
+ def create
10
+ vendor = current_integration.vendor
11
+ rooms = SpreeCmCommissioner::Integrations::SwitchCm::RoomCatalog.new(vendor).rooms
12
+
13
+ render_document do |xml|
14
+ rooms.each do |room|
15
+ xml.room(id: room.id, hotel_id: vendor.id, hotel_name: vendor.name, room_name: room.name, max_children: room.max_children) do
16
+ xml.rates { room.rates.each { |rate| build_rate(xml, room, rate) } }
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def build_rate(xml, room, rate)
25
+ attributes = { id: rate.id, rate_name: rate.name, max_persons: room.max_persons, is_child_rate: rate.child? ? 1 : 0, readonly: 0 }
26
+ return xml.rate(attributes) unless rate.child?
27
+
28
+ # Restrictions other than closing aren't stored, so the child rate follows only its parent's price and closings.
29
+ xml.rate(attributes) do
30
+ xml.rate_relation(
31
+ {
32
+ parent_rate_id: rate.parent_id, percentage: rate.percentage&.to_f, additional: rate.additional&.to_f,
33
+ follows_price: 1, follows_closed: 3, follows_restrictions: 0
34
+ }.compact
35
+ )
36
+ end
37
+ end
38
+
39
+ # override
40
+ def response_root
41
+ 'rooms'
42
+ end
43
+ end
44
+ end
45
+ end
@@ -12,7 +12,7 @@ module Spree
12
12
  end
13
13
 
14
14
  def collection
15
- SpreeCmCommissioner::GuestSearcherQuery.new(params).call
15
+ SpreeCmCommissioner::GuestSearcherQuery.new(params).call.merge(SpreeCmCommissioner::Guest.with_dynamic_fields_and_options)
16
16
  end
17
17
 
18
18
  # override
@@ -7,6 +7,7 @@ module Spree
7
7
  return Spree::Vendor if params['model_class'] == 'Spree::Vendor'
8
8
  return Spree::Product if params['model_class'] == 'Spree::Product'
9
9
  return Spree::Taxon if params['model_class'] == 'Spree::Taxon'
10
+ return Spree.user_class if params['model_class'] == 'Spree::User'
10
11
 
11
12
  nil
12
13
  end