spree_cm_commissioner 2.13.1 → 2.13.3.pre.pre1

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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +1 -0
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/concerns/spree_cm_commissioner/operator/event_authorization.rb +19 -0
  5. data/app/controllers/spree/admin/homepage_section_controller.rb +32 -8
  6. data/app/controllers/spree/admin/integrations_controller.rb +8 -8
  7. data/app/controllers/spree/admin/inventory_items_controller.rb +12 -0
  8. data/app/controllers/spree/admin/notification_users_controller.rb +39 -7
  9. data/app/controllers/spree/admin/service_fee_rules_controller.rb +83 -0
  10. data/app/controllers/spree/admin/stock_managements_controller.rb +5 -5
  11. data/app/controllers/spree/api/switch_cm/base_controller.rb +98 -0
  12. data/app/controllers/spree/api/switch_cm/ota/avail_notifs_controller.rb +17 -0
  13. data/app/controllers/spree/api/switch_cm/ota/base_controller.rb +27 -0
  14. data/app/controllers/spree/api/switch_cm/ota/product_notifs_controller.rb +44 -0
  15. data/app/controllers/spree/api/switch_cm/ota/pushes_controller.rb +46 -0
  16. data/app/controllers/spree/api/switch_cm/ota/rate_amount_notifs_controller.rb +17 -0
  17. data/app/controllers/spree/api/switch_cm/ota/res_modify_notifs_controller.rb +31 -0
  18. data/app/controllers/spree/api/switch_cm/ota/res_notifs_controller.rb +29 -0
  19. data/app/controllers/spree/api/switch_cm/ota/reservation_streams_controller.rb +83 -0
  20. data/app/controllers/spree/api/switch_cm/tokens_controller.rb +68 -0
  21. data/app/controllers/spree/api/switch_cm/xml/availabilities_controller.rb +39 -0
  22. data/app/controllers/spree/api/switch_cm/xml/base_controller.rb +37 -0
  23. data/app/controllers/spree/api/switch_cm/xml/reservation_summaries_controller.rb +28 -0
  24. data/app/controllers/spree/api/switch_cm/xml/reservations_controller.rb +56 -0
  25. data/app/controllers/spree/api/switch_cm/xml/room_rate_availabilities_controller.rb +56 -0
  26. data/app/controllers/spree/api/switch_cm/xml/room_rates_controller.rb +32 -0
  27. data/app/controllers/spree/api/v2/operator/check_ins_controller.rb +7 -6
  28. data/app/controllers/spree/api/v2/operator/event_charts_controller.rb +8 -10
  29. data/app/controllers/spree/api/v2/operator/event_ticket_aggregators_controller.rb +14 -4
  30. data/app/controllers/spree/api/v2/operator/pie_chart_event_aggregators_controller.rb +15 -1
  31. data/app/controllers/spree/api/v2/platform/homepage_section_relatable_options_controller.rb +28 -0
  32. data/app/controllers/spree/api/v2/storefront/cross_app_handoff_tokens_controller.rb +41 -0
  33. data/app/controllers/spree/api/v2/storefront/homepage_sections_controller.rb +4 -2
  34. data/app/controllers/spree/api/v2/storefront/transit/item_availabilities_controller.rb +66 -0
  35. data/app/controllers/spree/api/v2/tenant/checkout_controller.rb +11 -0
  36. data/app/controllers/spree/api/v2/tenant/vendor_photos_controller.rb +25 -0
  37. data/app/controllers/spree_cm_commissioner/admin/orders_controller_decorator.rb +9 -1
  38. data/app/controllers/spree_cm_commissioner/api/v2/storefront/checkout_controller_decorator.rb +15 -0
  39. data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +6 -2
  40. data/app/helpers/spree/admin/customer_notifications_helper.rb +57 -0
  41. data/app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb +45 -23
  42. data/app/interactors/spree_cm_commissioner/customer_notification_sender.rb +18 -9
  43. data/app/interactors/spree_cm_commissioner/user_cross_app_handoff_authenticator.rb +17 -0
  44. data/app/jobs/spree_cm_commissioner/audit_event_job.rb +29 -0
  45. data/app/jobs/spree_cm_commissioner/integrations/switch_cm/unseen_reservations_alert_job.rb +7 -0
  46. data/app/models/concerns/spree_cm_commissioner/audit_wrapper.rb +139 -0
  47. data/app/models/concerns/spree_cm_commissioner/homepage_section_bitwise.rb +37 -1
  48. data/app/models/concerns/spree_cm_commissioner/integrations/inventory_sync_cachable.rb +9 -0
  49. data/app/models/concerns/spree_cm_commissioner/line_item_open_dated_trippable.rb +2 -2
  50. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +10 -0
  51. data/app/models/concerns/spree_cm_commissioner/phone_number_sanitizer.rb +5 -1
  52. data/app/models/concerns/spree_cm_commissioner/user_notification.rb +3 -1
  53. data/app/models/concerns/spree_cm_commissioner/variant_metadata.rb +24 -0
  54. data/app/models/concerns/spree_cm_commissioner/vehicle_kind.rb +1 -1
  55. data/app/models/spree_cm_commissioner/adjustable/adjuster/service_fee.rb +31 -0
  56. data/app/models/spree_cm_commissioner/adjustment_decorator.rb +1 -0
  57. data/app/models/spree_cm_commissioner/audit_event.rb +11 -0
  58. data/app/models/spree_cm_commissioner/calculators/service_fee_flat_amount.rb +17 -0
  59. data/app/models/spree_cm_commissioner/calculators/service_fee_percent.rb +20 -0
  60. data/app/models/spree_cm_commissioner/currency_rate.rb +15 -0
  61. data/app/models/spree_cm_commissioner/customer_notification.rb +15 -1
  62. data/app/models/spree_cm_commissioner/homepage_section.rb +25 -0
  63. data/app/models/spree_cm_commissioner/integration.rb +58 -0
  64. data/app/models/spree_cm_commissioner/integrations/book_me_bus_v1.rb +64 -0
  65. data/app/models/spree_cm_commissioner/integrations/larryta.rb +81 -0
  66. data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +4 -10
  67. data/app/models/spree_cm_commissioner/integrations/switch_cm.rb +80 -0
  68. data/app/models/spree_cm_commissioner/integrations/vireak_buntham.rb +82 -0
  69. data/app/models/spree_cm_commissioner/inventory_item.rb +18 -0
  70. data/app/models/spree_cm_commissioner/line_item_decorator.rb +2 -0
  71. data/app/models/spree_cm_commissioner/order_decorator.rb +13 -1
  72. data/app/models/spree_cm_commissioner/order_updater_decorator.rb +31 -0
  73. data/app/models/spree_cm_commissioner/place.rb +2 -0
  74. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -1
  75. data/app/models/spree_cm_commissioner/route.rb +1 -0
  76. data/app/models/spree_cm_commissioner/seat_layout.rb +1 -0
  77. data/app/models/spree_cm_commissioner/service_fee_rule.rb +183 -0
  78. data/app/models/spree_cm_commissioner/stock/availability_checker.rb +1 -1
  79. data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +1 -1
  80. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +4 -0
  81. data/app/models/spree_cm_commissioner/trip.rb +2 -1
  82. data/app/models/spree_cm_commissioner/trip_stop.rb +2 -0
  83. data/app/models/spree_cm_commissioner/user_decorator.rb +4 -0
  84. data/app/models/spree_cm_commissioner/variant_decorator.rb +5 -0
  85. data/app/models/spree_cm_commissioner/vehicle.rb +3 -0
  86. data/app/models/spree_cm_commissioner/vehicle_type.rb +1 -0
  87. data/app/models/spree_cm_commissioner/vendor_decorator.rb +17 -0
  88. data/app/models/spree_cm_commissioner/vendor_place.rb +2 -0
  89. data/app/overrides/spree/admin/orders/index/line_items_row.html.erb.deface +14 -0
  90. data/app/overrides/spree/admin/orders/index/row_actions.html.erb.deface +17 -0
  91. data/app/overrides/spree/admin/shared/sub_menu/_configuration/menu_items.html.erb.deface +5 -0
  92. data/app/queries/spree_cm_commissioner/users_by_event_fetcher_query.rb +6 -7
  93. data/app/serializables/spree_cm_commissioner/item_availability_sync.rb +10 -0
  94. data/app/serializers/spree/api/v2/platform/service_fee_rule_serializer.rb +17 -0
  95. data/app/serializers/spree/v2/storefront/homepage_section_serializer.rb +1 -1
  96. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +8 -2
  97. data/app/serializers/spree/v2/tenant/cart_serializer.rb +2 -1
  98. data/app/serializers/spree/v2/tenant/line_item_serializer.rb +7 -1
  99. data/app/serializers/spree_cm_commissioner/v2/operator/guest_serializer.rb +2 -1
  100. data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +2 -1
  101. data/app/serializers/spree_cm_commissioner/v2/storefront/cross_app_handoff_token_serializer.rb +9 -0
  102. data/app/serializers/spree_cm_commissioner/v2/storefront/item_availability_sync_serializer.rb +10 -0
  103. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +1 -1
  104. data/app/services/spree_cm_commissioner/audit_logger.rb +59 -0
  105. data/app/services/spree_cm_commissioner/checkout/advance_to_payment.rb +27 -0
  106. data/app/services/spree_cm_commissioner/cross_app_handoff_tokens/decode.rb +42 -0
  107. data/app/services/spree_cm_commissioner/cross_app_handoff_tokens/encode.rb +47 -0
  108. data/app/services/spree_cm_commissioner/currency_converter/create.rb +27 -0
  109. data/app/services/spree_cm_commissioner/currency_rates/manage.rb +99 -0
  110. data/app/services/spree_cm_commissioner/draft_orders/create.rb +1 -0
  111. data/app/services/spree_cm_commissioner/google_maps_geocoder.rb +41 -0
  112. data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +1 -2
  113. data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +11 -0
  114. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/client.rb +230 -0
  115. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/connection.rb +147 -0
  116. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_item_status.rb +52 -0
  117. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_items.rb +91 -0
  118. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/unstock_inventory.rb +188 -0
  119. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_inventory.rb +79 -0
  120. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_item_availability.rb +354 -0
  121. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_locations.rb +103 -0
  122. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_nationality_pricing.rb +93 -0
  123. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_product.rb +70 -0
  124. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_route_stops.rb +121 -0
  125. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_routes.rb +128 -0
  126. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_seat_layout.rb +412 -0
  127. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_service_calendars.rb +59 -0
  128. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +224 -0
  129. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trips.rb +769 -0
  130. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_variant.rb +69 -0
  131. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_vehicle_types.rb +92 -0
  132. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/time_parser.rb +19 -0
  133. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/base.rb +40 -0
  134. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/inventory_item_status.rb +148 -0
  135. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/location.rb +55 -0
  136. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/reservation_cart.rb +85 -0
  137. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat.rb +128 -0
  138. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb +386 -0
  139. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/stop.rb +84 -0
  140. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/trip.rb +237 -0
  141. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/wallet_payment.rb +48 -0
  142. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_manager.rb +22 -0
  143. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/full_sync_strategy.rb +116 -0
  144. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/incremental_sync_strategy.rb +63 -0
  145. data/app/services/spree_cm_commissioner/integrations/larryta/external_client/client.rb +314 -0
  146. data/app/services/spree_cm_commissioner/integrations/larryta/external_client/connection.rb +74 -0
  147. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/external_inventory_item_status.rb +53 -0
  148. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/external_inventory_items.rb +106 -0
  149. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/hold_seats.rb +143 -0
  150. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/release_seats.rb +90 -0
  151. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/unstock_inventory.rb +143 -0
  152. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_item_availability.rb +325 -0
  153. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_product.rb +72 -0
  154. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_routes.rb +142 -0
  155. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_seat_layout.rb +255 -0
  156. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_stock_item.rb +55 -0
  157. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_trip_stop.rb +171 -0
  158. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_trips.rb +397 -0
  159. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_variant.rb +63 -0
  160. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_vehicle_type.rb +79 -0
  161. data/app/services/spree_cm_commissioner/integrations/larryta/resources/base.rb +39 -0
  162. data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking.rb +113 -0
  163. data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking_session.rb +43 -0
  164. data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking_transaction.rb +236 -0
  165. data/app/services/spree_cm_commissioner/integrations/larryta/resources/direction.rb +71 -0
  166. data/app/services/spree_cm_commissioner/integrations/larryta/resources/passenger.rb +48 -0
  167. data/app/services/spree_cm_commissioner/integrations/larryta/resources/schedule.rb +150 -0
  168. data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat.rb +31 -0
  169. data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat_availability.rb +49 -0
  170. data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat_layout.rb +211 -0
  171. data/app/services/spree_cm_commissioner/integrations/larryta/sync_manager.rb +17 -0
  172. data/app/services/spree_cm_commissioner/integrations/larryta/sync_strategies/full_sync_strategy.rb +79 -0
  173. data/app/services/spree_cm_commissioner/integrations/larryta/sync_strategies/incremental_sync_strategy.rb +58 -0
  174. data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +1 -1
  175. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +14 -15
  176. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +7 -4
  177. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +6 -3
  178. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +0 -3
  179. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +1 -3
  180. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +1 -3
  181. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +1 -2
  182. data/app/services/spree_cm_commissioner/integrations/switch_cm/accept_push.rb +101 -0
  183. data/app/services/spree_cm_commissioner/integrations/switch_cm/accommodation_lock.rb +41 -0
  184. data/app/services/spree_cm_commissioner/integrations/switch_cm/acknowledge_reservations.rb +43 -0
  185. data/app/services/spree_cm_commissioner/integrations/switch_cm/apply_ari.rb +119 -0
  186. data/app/services/spree_cm_commissioner/integrations/switch_cm/ari_update.rb +24 -0
  187. data/app/services/spree_cm_commissioner/integrations/switch_cm/availability_readback.rb +86 -0
  188. data/app/services/spree_cm_commissioner/integrations/switch_cm/mark_seen.rb +53 -0
  189. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/avail_notif_parser.rb +60 -0
  190. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/base_parser.rb +60 -0
  191. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/push_response.rb +38 -0
  192. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/rate_amount_notif_parser.rb +46 -0
  193. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/reservation_presenter.rb +99 -0
  194. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/response_builder.rb +52 -0
  195. data/app/services/spree_cm_commissioner/integrations/switch_cm/pushed_nights.rb +69 -0
  196. data/app/services/spree_cm_commissioner/integrations/switch_cm/reservation.rb +118 -0
  197. data/app/services/spree_cm_commissioner/integrations/switch_cm/reservation_feed.rb +143 -0
  198. data/app/services/spree_cm_commissioner/integrations/switch_cm/room_catalog.rb +56 -0
  199. data/app/services/spree_cm_commissioner/integrations/switch_cm/unseen_reservations_alert.rb +45 -0
  200. data/app/services/spree_cm_commissioner/integrations/switch_cm/unseen_room_nights.rb +39 -0
  201. data/app/services/spree_cm_commissioner/integrations/switch_cm/validate_ari_updates.rb +136 -0
  202. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/availability_parser.rb +103 -0
  203. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/push_response.rb +45 -0
  204. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/reservation_presenter.rb +74 -0
  205. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/response_builder.rb +19 -0
  206. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/external_client/client.rb +170 -0
  207. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/external_client/connection.rb +59 -0
  208. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/external_inventory_item_status.rb +48 -0
  209. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/external_inventory_items.rb +92 -0
  210. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/unstock_inventory.rb +230 -0
  211. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_item_availability.rb +270 -0
  212. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_locations.rb +96 -0
  213. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_nationality_pricing.rb +76 -0
  214. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_product.rb +69 -0
  215. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_routes.rb +110 -0
  216. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_seat_layout.rb +207 -0
  217. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_stock_item.rb +43 -0
  218. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_trip_stop.rb +192 -0
  219. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_trips.rb +377 -0
  220. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_variant.rb +56 -0
  221. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_vehicle_type.rb +51 -0
  222. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/base.rb +32 -0
  223. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/booking.rb +48 -0
  224. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/booking_transaction.rb +46 -0
  225. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/cell.rb +53 -0
  226. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/confirm_booking_request.rb +43 -0
  227. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/location.rb +37 -0
  228. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/schedule.rb +138 -0
  229. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/seat.rb +48 -0
  230. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/seat_layout.rb +79 -0
  231. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_manager.rb +17 -0
  232. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_strategies/full_sync_strategy.rb +87 -0
  233. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_strategies/incremental_sync_strategy.rb +53 -0
  234. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_by_variant.rb +4 -0
  235. data/app/services/spree_cm_commissioner/inventory_items/generate_permanent_item.rb +48 -0
  236. data/app/services/spree_cm_commissioner/inventory_items/reset.rb +4 -0
  237. data/app/services/spree_cm_commissioner/order_holds/hold.rb +17 -0
  238. data/app/services/spree_cm_commissioner/order_holds/release.rb +17 -0
  239. data/app/services/spree_cm_commissioner/place_resolver.rb +95 -0
  240. data/app/services/spree_cm_commissioner/transit_order/create.rb +1 -0
  241. data/app/services/spree_cm_commissioner/user_authenticator.rb +20 -12
  242. data/app/services/spree_cm_commissioner/waiting_room/admission/call_guests.rb +12 -0
  243. data/app/views/spree/admin/customer_notifications/_form.html.erb +9 -9
  244. data/app/views/spree/admin/customer_notifications/index.html.erb +36 -19
  245. data/app/views/spree/admin/homepage_section/_form.html.erb +19 -2
  246. data/app/views/spree/admin/homepage_section/_tabs.html.erb +7 -7
  247. data/app/views/spree/admin/homepage_section/index.html.erb +73 -6
  248. data/app/views/spree/admin/integration_sessions/index.html.erb +8 -6
  249. data/app/views/spree/admin/integrations/_book_me_bus_v1_fields.html.erb +47 -0
  250. data/app/views/spree/admin/integrations/_form.html.erb +16 -0
  251. data/app/views/spree/admin/integrations/_larryta_fields.html.erb +58 -0
  252. data/app/views/spree/admin/integrations/_switch_cm_fields.html.erb +53 -0
  253. data/app/views/spree/admin/integrations/_vireak_buntham_fields.html.erb +75 -0
  254. data/app/views/spree/admin/integrations/index.html.erb +1 -1
  255. data/app/views/spree/admin/notifications/index.html.erb +2 -1
  256. data/app/views/spree/admin/orders/line_items.html.erb +53 -0
  257. data/app/views/spree/admin/service_fee_rules/_form.html.erb +119 -0
  258. data/app/views/spree/admin/service_fee_rules/edit.html.erb +11 -0
  259. data/app/views/spree/admin/service_fee_rules/index.html.erb +91 -0
  260. data/app/views/spree/admin/service_fee_rules/new.html.erb +11 -0
  261. data/app/views/spree/admin/shared/_send_all_form.html.erb +1 -0
  262. data/app/views/spree/admin/shared/_send_test_form.html.erb +3 -3
  263. data/app/views/spree/admin/stock_managements/index.html.erb +14 -0
  264. data/config/locales/en.yml +37 -0
  265. data/config/locales/km.yml +33 -0
  266. data/config/routes.rb +34 -0
  267. data/db/migrate/20260327143200_create_cm_audit_events.rb +21 -0
  268. data/db/migrate/20260410045815_create_cm_currency_rates.rb +23 -0
  269. data/db/migrate/20260411102139_add_metadata_to_cm_inventory_items.rb +6 -0
  270. data/db/migrate/20260914000000_add_mini_app_to_homepage_sections.rb +5 -0
  271. data/db/migrate/20260917085551_create_cm_service_fee_rules.rb +16 -0
  272. data/db/migrate/20260917085552_add_service_fee_total_to_spree_line_items_and_orders.rb +6 -0
  273. data/db/migrate/20260928000000_add_oauth_application_id_to_cm_integrations.rb +5 -0
  274. data/db/migrate/20260928000001_add_closed_to_cm_inventory_items.rb +5 -0
  275. data/lib/spree_cm_commissioner/cached_inventory_item.rb +7 -1
  276. data/lib/spree_cm_commissioner/engine.rb +3 -0
  277. data/lib/spree_cm_commissioner/test_helper/factories/homepage_section_factory.rb +7 -0
  278. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +17 -0
  279. data/lib/spree_cm_commissioner/version.rb +1 -1
  280. metadata +186 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 709925b9130644b3e39ef1bb01415692019bc72643176593877c42f1a710ff0f
4
- data.tar.gz: a336bae1ac6555051a4d6c718d8cce7bb1fcbed67b8bea0a2accb5b96cb8ae1b
3
+ metadata.gz: 6debbf57a54a0a2fcbc3ba4ee4c3e07045c520377afbd0ad13d6f6fde2120654
4
+ data.tar.gz: 3c66791879a8187925ac7ecba67437a4e049bce327f7d19ff9f435d0acddd54e
5
5
  SHA512:
6
- metadata.gz: '0769de948ff5436227d88a348a051a7992956deca52f0a165c4cfa885750b9d672c9192482070d922b52921ded1b080a08c004eb6d33206ae67638ee4eec3a61'
7
- data.tar.gz: '09affaee452bf8a04579f43999a2beb71282b0d369109085fa59b63a8851d0c6fb80ad67b9ffc3930542ab6fe9e6b9c500c17b243380b5d33440225e30d8cc02'
6
+ metadata.gz: f5a7c7f1e396e46ae559484c92900a1180ede361e562b192a0b13abc922779f521fbc093a86d695b760abcc8609e0563785e5fadd285fb919670c0c8816bf5d9
7
+ data.tar.gz: 124912777c8bacddd0a1f067ab1e6faaa245af600da5343150f4862fd5c9cabdea60e54efddf64e636903a99cb2e0a7143c91647ff34e05a5c0d9d705ff8a5da
data/.env.example CHANGED
@@ -87,3 +87,4 @@ PAYMENT_INCOMPLETE_NOTIFICATION_DELAY_IN_MINUTES=5 # Delay before notifying a c
87
87
 
88
88
  # See: app/jobs/spree_cm_commissioner/telegram_alerts/order_integrity_check_job.rb
89
89
  ORDER_INTEGRITY_TELEGRAM_ALERT_ENABLED="no" # Set to yes to enable the order integrity Telegram alert job (off by default to avoid flooding Telegram rate limits during traffic spikes)
90
+
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.13.1)
37
+ spree_cm_commissioner (2.13.3.pre.pre1)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -0,0 +1,19 @@
1
+ module SpreeCmCommissioner
2
+ module Operator
3
+ module EventAuthorization
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+
8
+ # True only when the current user holds the organizer role AND owns the
9
+ # vendor that `taxon`'s event belongs to — the organizer role alone has no
10
+ # link to any specific event, so it must be paired with vendor ownership.
11
+ def organizer_for_event?(taxon)
12
+ return false if taxon.nil? || taxon.vendor_id.nil? || spree_current_user.nil?
13
+ return false unless spree_current_user.organizer?
14
+
15
+ spree_current_user.vendors.exists?(id: taxon.vendor_id)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -5,6 +5,8 @@ module Spree
5
5
 
6
6
  before_action :load_tenants, only: [:index]
7
7
 
8
+ helper_method :selected_tenant
9
+
8
10
  def model_class
9
11
  SpreeCmCommissioner::HomepageSection
10
12
  end
@@ -14,7 +16,13 @@ module Spree
14
16
  end
15
17
 
16
18
  def collection_url
17
- admin_homepage_feed_homepage_section_index_url
19
+ admin_homepage_feed_homepage_section_index_url(tenant_id: params[:tenant_id].presence || @object&.tenant_id)
20
+ end
21
+
22
+ # A new section joins the storefront you were viewing. Reading it from the URL keeps
23
+ # `tenant_id` out of permitted_resource_params, so the form cannot reassign a storefront.
24
+ def build_resource
25
+ model_class.new(tenant_id: params[:tenant_id].presence)
18
26
  end
19
27
 
20
28
  def edit
@@ -41,25 +49,41 @@ module Spree
41
49
  @collection = super.order(position: :asc)
42
50
  @search = @collection.ransack(params[:q])
43
51
 
44
- @collection = @search.result
45
- .where(tenant_id: params[:tenant_id].presence || nil)
46
- .page(params[:page])
47
- .per(params[:per_page])
52
+ # An unknown segment would compare against a nil bit and silently return nothing.
53
+ scoped = @search.result.where(tenant_id: selected_tenant&.id)
54
+ scoped = scoped.filter_by_segment(params[:segment]) if valid_segment?(params[:segment])
55
+ scoped = scoped.filter_by_mini_app(params[:mini_app]) if params[:mini_app].present?
56
+
57
+ @collection = scoped.page(params[:page]).per(params[:per_page])
48
58
  end
49
59
 
50
60
  # overrided
51
61
  def permitted_resource_params
52
- segment_value = helpers.calculate_segment_value(params[:spree_cm_commissioner_homepage_section])
62
+ object_params = params[:spree_cm_commissioner_homepage_section]
63
+ segment_value = helpers.calculate_segment_value(object_params)
64
+ mini_app_value = helpers.calculate_mini_app_value(object_params)
53
65
 
54
66
  params
55
67
  .require(:spree_cm_commissioner_homepage_section)
56
- .permit(:title, :description, :active, :tenant_id, :preview)
57
- .merge(segment: segment_value)
68
+ .permit(:title, :description, :active, :preview)
69
+ .merge(segment: segment_value, mini_app: mini_app_value)
70
+ end
71
+
72
+ # The storefront being viewed; no tenant in the URL means BookMe+. Also gates the mini app
73
+ # column and form field, which apply to BookMe+ only.
74
+ def selected_tenant
75
+ return @selected_tenant if defined?(@selected_tenant)
76
+
77
+ @selected_tenant = params[:tenant_id].presence && SpreeCmCommissioner::Tenant.find_by(id: params[:tenant_id])
58
78
  end
59
79
 
60
80
  def load_tenants
61
81
  @tenants ||= SpreeCmCommissioner::Tenant.order(:name)
62
82
  end
83
+
84
+ def valid_segment?(segment)
85
+ segment.present? && SpreeCmCommissioner::HomepageSectionBitwise::BIT_SEGMENT.key?(segment.to_sym)
86
+ end
63
87
  end
64
88
  end
65
89
  end
@@ -12,7 +12,9 @@ module Spree
12
12
  def enqueue_polling
13
13
  sync_type = params[:sync_type].presence || :full
14
14
 
15
- if @integration.active?
15
+ if !@integration.supports_polling?
16
+ flash[:error] = Spree.t('admin.integration_polling_not_supported')
17
+ elsif @integration.active?
16
18
  SpreeCmCommissioner::Integrations::PollingJob.perform_later(
17
19
  integration_id: @integration.id,
18
20
  sync_type: sync_type
@@ -47,6 +49,7 @@ module Spree
47
49
  :full_sync_interval_hours,
48
50
  :tenant_id,
49
51
  :vendor_id,
52
+ :oauth_application_id,
50
53
  public_metadata: {},
51
54
  private_metadata: {}
52
55
  )
@@ -62,6 +65,7 @@ module Spree
62
65
  @conflict_strategy_options ||= enum_to_options(SpreeCmCommissioner::Integration.conflict_strategies)
63
66
  @tenants ||= SpreeCmCommissioner::Tenant.select(:id, :name).order(:name)
64
67
  @vendors ||= Spree::Vendor.select(:id, :name).order(:name)
68
+ @oauth_applications ||= Spree::OauthApplication.select(:id, :name).order(:name)
65
69
  end
66
70
 
67
71
  def enum_to_options(enum_hash)
@@ -69,14 +73,10 @@ module Spree
69
73
  end
70
74
 
71
75
  def integration_type_options
72
- files = Dir[SpreeCmCommissioner::Engine.root.join('app/models/spree_cm_commissioner/integrations/*.rb')]
76
+ types = SpreeCmCommissioner::Integration.types
77
+ return [[Spree.t(:type), 'SpreeCmCommissioner::Integration']] if types.blank?
73
78
 
74
- return [[Spree.t(:type), 'SpreeCmCommissioner::Integration']] if files.blank?
75
-
76
- files.map do |path|
77
- class_name = "SpreeCmCommissioner::Integrations::#{File.basename(path, '.rb').camelize}"
78
- [class_name, class_name]
79
- end.sort_by(&:first)
79
+ types.map { |class_name| [class_name, class_name] }
80
80
  end
81
81
  end
82
82
  end
@@ -2,6 +2,7 @@ module Spree
2
2
  module Admin
3
3
  class InventoryItemsController < StockManagementsController
4
4
  before_action :load_parent
5
+ before_action :reject_external_inventory_edits, only: %i[bulk_update_prices bulk_update_stocks]
5
6
 
6
7
  # GET /products/:slug/inventory_items/prices?inventory_item_ids=1,2,3
7
8
  def prices
@@ -78,6 +79,17 @@ module Spree
78
79
 
79
80
  redirect_back fallback_location: admin_product_stock_managements_path(@product)
80
81
  end
82
+
83
+ private
84
+
85
+ # The partner owns these per-night stocks and prices; its next update would silently overwrite
86
+ # an edit made here.
87
+ def reject_external_inventory_edits
88
+ return unless @product.variants.includes(vendor: :integration).any?(&:inventory_external_wins?)
89
+
90
+ flash[:error] = Spree.t('admin.inventory_external_wins', name: @product.vendor.integration.name)
91
+ redirect_back fallback_location: admin_product_stock_managements_path(@product)
92
+ end
81
93
  end
82
94
  end
83
95
  end
@@ -1,26 +1,58 @@
1
+ # Looks up users targetable by a customer notification, for the "send test" user picker in the modal form.
2
+
1
3
  module Spree
2
4
  module Admin
3
5
  class NotificationUsersController < Spree::Admin::BaseController
6
+ RESULTS_LIMIT = 20
7
+
4
8
  def index
5
9
  notification = SpreeCmCommissioner::CustomerNotification.find(params[:customer_notification_id])
6
- users_with_device_token = fetch_users_with_device_token(notification)
10
+ scope = fetch_users_with_device_token(notification)
11
+ users = search(scope, params[:q])
7
12
 
8
- render json: users_with_device_token.as_json(only: %i[id], methods: %i[display_name])
13
+ render json: users.limit(RESULTS_LIMIT).as_json(only: %i[id], methods: %i[display_name])
9
14
  end
10
15
 
11
16
  private
12
17
 
18
+ # Filters scope by email or phone number matching query (case-insensitive)
19
+ def search(scope, query)
20
+ return scope.none if query.blank?
21
+
22
+ term = "%#{Spree::User.sanitize_sql_like(query)}%"
23
+ scope.where('spree_users.email ILIKE :term OR spree_users.phone_number ILIKE :term', term: term)
24
+ end
25
+
26
+ # Narrows the notification's target users down to those with a registered push device token
13
27
  def fetch_users_with_device_token(notification)
28
+ scope = users_scope_for(notification)
29
+
30
+ scope.where(id: registered_user_ids)
31
+ end
32
+
33
+ # User ids that have a device token registered for push notifications
34
+ def registered_user_ids
35
+ device_tokens = SpreeCmCommissioner::DeviceToken.where.not(registration_token: nil)
36
+ device_tokens.select(:user_id)
37
+ end
38
+
39
+ # Resolves the base user scope for a notification: by its taxons, by its vendor's own
40
+ # taxons (when the vendor has no tenant), or by tenant otherwise
41
+ def users_scope_for(notification)
42
+ tenant_id = notification.vendor&.tenant_id
43
+
14
44
  if notification.notification_taxons.present?
15
- users = users(notification.notification_taxons.pluck(:taxon_id))
16
- users.where(id: SpreeCmCommissioner::DeviceToken.where.not(registration_token: nil).select(:user_id))
45
+ users(notification.notification_taxons.pluck(:taxon_id))
46
+ elsif tenant_id.nil? && notification.vendor.present?
47
+ users(notification.vendor.taxons.ids)
17
48
  else
18
- Spree::User.where(id: SpreeCmCommissioner::DeviceToken.where.not(registration_token: nil).select(:user_id))
49
+ Spree::User.where(tenant_id: tenant_id)
19
50
  end
20
51
  end
21
52
 
22
- def users(taxon_id)
23
- SpreeCmCommissioner::UsersByEventFetcherQuery.new(taxon_id).call
53
+ # Fetches users who purchased under the given events
54
+ def users(event_id)
55
+ SpreeCmCommissioner::UsersByEventFetcherQuery.new(event_id: event_id).call
24
56
  end
25
57
  end
26
58
  end
@@ -0,0 +1,83 @@
1
+ module Spree
2
+ module Admin
3
+ class ServiceFeeRulesController < Spree::Admin::ResourceController
4
+ helper_method :selected_tenant
5
+
6
+ before_action :load_data
7
+
8
+ def index
9
+ @search = model_class.ransack(params[:q])
10
+ # Same 'chip button' status filter convention as
11
+ # admin/system/waiting_room_sessions#guests (see guests.html.erb) -- an unrecognized or
12
+ # missing param falls back to showing everything rather than silently hiding rows.
13
+ @status = %w[active inactive].include?(params[:status]) ? params[:status] : 'all'
14
+
15
+ scope = @search.result
16
+ scope = scope.where(active: true) if @status == 'active'
17
+ scope = scope.where(active: false) if @status == 'inactive'
18
+
19
+ # Same tenant-dropdown convention as admin/homepage_section#index. tenant_id only ever
20
+ # gets set on a ruleable-less row (see ServiceFeeRule's tenant_id absence validation), so
21
+ # picking a specific tenant narrows down to just that tenant's own default rule -- the
22
+ # default (no param, "BookMe+") view shows tenant_id: nil, i.e. everything except other
23
+ # tenants' own overrides: the true platform-wide default plus every product/event/vendor
24
+ # rule (their tenant is already implied by their ruleable, so they never set tenant_id).
25
+ scope = scope.where(tenant_id: selected_tenant&.id)
26
+
27
+ @service_fee_rules = scope.order(ruleable_type: :asc, ruleable_id: :asc, currency: :asc)
28
+ .page(params[:page])
29
+ .per(25)
30
+ end
31
+
32
+ # A new rule inherits the tenant you were browsing under (index's tenant dropdown). Reading
33
+ # it from the URL keeps `tenant_id` out of permitted_resource_params, so the form can't be
34
+ # used to reassign a rule to a different tenant after the fact -- same shape as
35
+ # HomepageSectionController#build_resource.
36
+ def build_resource
37
+ model_class.new(tenant_id: params[:tenant_id].presence)
38
+ end
39
+
40
+ private
41
+
42
+ def load_data
43
+ @calculators = model_class.available_calculator_types.sort_by(&:name)
44
+ @tenants ||= SpreeCmCommissioner::Tenant.order(:name)
45
+ end
46
+
47
+ def selected_tenant
48
+ return @selected_tenant if defined?(@selected_tenant)
49
+
50
+ @selected_tenant = params[:tenant_id].presence && SpreeCmCommissioner::Tenant.find_by(id: params[:tenant_id])
51
+ end
52
+
53
+ def model_class
54
+ SpreeCmCommissioner::ServiceFeeRule
55
+ end
56
+
57
+ def object_name
58
+ 'spree_cm_commissioner_service_fee_rule'
59
+ end
60
+
61
+ def collection_url
62
+ admin_service_fee_rules_url
63
+ end
64
+
65
+ def location_after_save
66
+ edit_admin_service_fee_rule_path(@object)
67
+ end
68
+
69
+ # tenant_id is deliberately excluded here -- see build_resource. It's set once from the URL
70
+ # at creation time and never reassignable via the form afterwards.
71
+ def permitted_resource_params
72
+ params.require(:spree_cm_commissioner_service_fee_rule).permit(
73
+ :ruleable_type,
74
+ :ruleable_id,
75
+ :active,
76
+ :currency,
77
+ :calculator_type,
78
+ calculator_attributes: %i[id preferred_percent preferred_flat_amount]
79
+ )
80
+ end
81
+ end
82
+ end
83
+ end
@@ -99,11 +99,11 @@ module Spree
99
99
  @cached_inventory_items = ::SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(@product.inventory_items)
100
100
  .call
101
101
  .index_by(&:inventory_item_id)
102
- @reserved_stocks = Spree::LineItem
103
- .affecting_inventory
104
- .where(variant_id: @variants.pluck(:id))
105
- .group('spree_line_items.variant_id')
106
- .sum(:quantity)
102
+ @purchased_quantities = Spree::LineItem
103
+ .affecting_inventory
104
+ .where(variant_id: @variants.pluck(:id))
105
+ .group('spree_line_items.variant_id')
106
+ .sum(:quantity)
107
107
  end
108
108
 
109
109
  def model_class
@@ -0,0 +1,98 @@
1
+ module Spree::Api::SwitchCm
2
+ # The Booking.com-compatible API channel managers (switch.cm) call under /api/switch_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 Ota::BaseController and Xml::BaseController render.
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
+ before_action :authenticate_switch_cm!
14
+
15
+ rescue_from Nokogiri::XML::SyntaxError do
16
+ render_fault(:bad_request, 'Request body is not well-formed XML')
17
+ end
18
+
19
+ private
20
+
21
+ def authenticate_switch_cm!
22
+ @oauth_application = application_from_bearer || application_from_basic
23
+ return if @oauth_application.present? && switch_cm_integrations.exists?
24
+
25
+ render_fault(:unauthorized, 'Authorization Required')
26
+ end
27
+
28
+ def application_from_bearer
29
+ doorkeeper_token.application if doorkeeper_token&.accessible?
30
+ end
31
+
32
+ def application_from_basic
33
+ authenticate_with_http_basic do |client_id, client_secret|
34
+ application = Spree::OauthApplication.find_by(uid: client_id)
35
+ application if application&.secret_matches?(client_secret)
36
+ end
37
+ end
38
+
39
+ def switch_cm_integrations
40
+ SpreeCmCommissioner::Integrations::SwitchCm.readable.where(oauth_application_id: @oauth_application.id)
41
+ end
42
+
43
+ # nil unless the requested hotel is one of this channel manager's connected hotels.
44
+ def current_integration
45
+ return @current_integration if defined?(@current_integration)
46
+
47
+ accommodation_id = accommodation_id_from_request
48
+ @current_integration = accommodation_id.present? ? switch_cm_integrations.find_by(vendor_id: accommodation_id) : nil
49
+ end
50
+
51
+ def require_integration!
52
+ render_fault(:forbidden, 'Not authorized for this hotel') if current_integration.nil?
53
+ end
54
+
55
+ # Pushes need an active connection: a paused one is still being set up, and the hotel keeps
56
+ # selling on its own counts until it's activated.
57
+ def require_active_integration!
58
+ return render_fault(:forbidden, 'Not authorized for this hotel') if current_integration.nil?
59
+
60
+ render_fault(:forbidden, 'This hotel is not active on BookMe+ yet') unless current_integration.active?
61
+ end
62
+
63
+ # Pushes may leave out the hotel id (B.XML never sends one); Booking.com derives it from the
64
+ # rooms. A room of another channel manager's hotel resolves to a hotel this app can't access.
65
+ def vendor_id_of_room(room_id)
66
+ Spree::Variant.where(id: room_id.to_i).pick(:vendor_id) if room_id.present?
67
+ end
68
+
69
+ # Reservation pulls take a comma-separated list of hotel ids, or none for all of this
70
+ # channel manager's hotels. Unknown ids come back separately, for a warning each, as at
71
+ # Booking.com; a list with no known hotel at all is refused.
72
+ def requested_integrations(hotel_ids)
73
+ ids = hotel_ids.to_s.split(',').map(&:strip).compact_blank
74
+ scope = switch_cm_integrations.includes(:vendor).order(:vendor_id)
75
+ return [scope.to_a, []] if ids.empty?
76
+
77
+ integrations = scope.where(vendor_id: ids).to_a
78
+ [integrations, ids - integrations.map { |integration| integration.vendor_id.to_s }]
79
+ end
80
+
81
+ def accommodation_id_from_request
82
+ raise NotImplementedError, "#{self.class.name} must implement accommodation_id_from_request"
83
+ end
84
+
85
+ def render_fault(_status, _text, code: nil)
86
+ raise NotImplementedError, "#{self.class.name} must implement render_fault"
87
+ end
88
+
89
+ # Rails parses neither XML nor text/xml bodies into params, so both families read the raw body.
90
+ def request_document
91
+ @request_document ||= Nokogiri::XML(request.raw_post) { |config| config.strict.nonet }.tap(&:remove_namespaces!)
92
+ end
93
+
94
+ def ruid
95
+ request.request_id
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,17 @@
1
+ module Spree::Api::SwitchCm
2
+ module Ota
3
+ # POST /api/switch_cm/hotels/ota/OTA_HotelAvailNotif, served by PushesController#create.
4
+ class AvailNotifsController < PushesController
5
+ private
6
+
7
+ # override
8
+ def parser_class = SpreeCmCommissioner::Integrations::SwitchCm::Ota::AvailNotifParser
9
+
10
+ # override
11
+ def message_name = 'OTA_HotelAvailNotif'
12
+
13
+ # override
14
+ def response_root = 'OTA_HotelAvailNotifRS'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ module Spree::Api::SwitchCm
2
+ module Ota
3
+ # Booking.com's OTA family (/hotels/ota/...): a failed request answers with its HTTP status
4
+ # and an RS document holding only <Errors>.
5
+ class BaseController < Spree::Api::SwitchCm::BaseController
6
+ # OTA EWT codes for the Error Type attribute.
7
+ ERROR_TYPES = { unauthorized: 4, forbidden: 6 }.freeze
8
+ PROCESSING_ERROR_TYPE = 12
9
+
10
+ private
11
+
12
+ def response_root
13
+ raise NotImplementedError, "#{self.class.name} must implement response_root"
14
+ end
15
+
16
+ # override
17
+ def render_fault(status, text, code: nil)
18
+ errors = [{ type: ERROR_TYPES.fetch(status, PROCESSING_ERROR_TYPE), code: code, text: text }]
19
+ render xml: SpreeCmCommissioner::Integrations::SwitchCm::Ota::ResponseBuilder.errors(response_root, errors, ruid: ruid), status: status
20
+ end
21
+
22
+ def render_success(warnings: [], &block)
23
+ render xml: SpreeCmCommissioner::Integrations::SwitchCm::Ota::ResponseBuilder.success(response_root, ruid: ruid, warnings: warnings, &block)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ module Spree::Api::SwitchCm
2
+ module Ota
3
+ class ProductNotifsController < BaseController
4
+ before_action :require_integration!
5
+
6
+ # GET /api/switch_cm/hotels/ota/OTA_HotelProductNotif?HotelCode=
7
+ # The hotel's rooms and rates, which the channel manager maps to its own ("Refresh Room" in
8
+ # switch.cm). One HotelProduct per room and rate, like Booking.com's roomrates.
9
+ def show
10
+ rooms = SpreeCmCommissioner::Integrations::SwitchCm::RoomCatalog.new(current_integration.vendor).rooms
11
+
12
+ render_success do |xml|
13
+ xml.HotelProducts(HotelCode: current_integration.vendor_id) do
14
+ rooms.each do |room|
15
+ xml.HotelProduct do
16
+ xml.RoomTypes do
17
+ xml.RoomType(
18
+ RoomTypeCode: room.id,
19
+ RoomTypeName: room.name,
20
+ MaxOccupancy: room.max_persons,
21
+ MaxChildOccupancy: room.max_children
22
+ )
23
+ end
24
+ xml.RatePlans { xml.RatePlan(RatePlanCode: room.rate_id, RatePlanName: room.rate_name) }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # override
34
+ def response_root
35
+ 'OTA_HotelProductNotifRS'
36
+ end
37
+
38
+ # override
39
+ def accommodation_id_from_request
40
+ params[:HotelCode]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ module Spree::Api::SwitchCm
2
+ module Ota
3
+ # The two OTA push endpoints; subclasses name the parser.
4
+ class PushesController < BaseController
5
+ before_action :require_active_integration!
6
+
7
+ # POST /api/switch_cm/hotels/ota/OTA_HotelAvailNotif (AvailNotifsController): rooms to sell and open/close
8
+ # POST /api/switch_cm/hotels/ota/OTA_HotelRateAmountNotif (RateAmountNotifsController): per-night prices
9
+ # Validated and applied in the request (AcceptPush); the answer is the real outcome.
10
+ def create
11
+ result = SpreeCmCommissioner::Integrations::SwitchCm::AcceptPush.call(
12
+ integration: current_integration,
13
+ message: message_name,
14
+ updates: updates,
15
+ request_id: ruid
16
+ )
17
+
18
+ push_response = SpreeCmCommissioner::Integrations::SwitchCm::Ota::PushResponse.new(root: response_root, result: result.value, ruid: ruid)
19
+ render xml: push_response.to_xml, status: push_response.http_status
20
+ end
21
+
22
+ private
23
+
24
+ def parser_class
25
+ raise NotImplementedError, "#{self.class.name} must implement parser_class"
26
+ end
27
+
28
+ def message_name
29
+ raise NotImplementedError, "#{self.class.name} must implement message_name"
30
+ end
31
+
32
+ def parser
33
+ @parser ||= parser_class.new(request_document)
34
+ end
35
+
36
+ def updates
37
+ @updates ||= parser.updates
38
+ end
39
+
40
+ # override
41
+ def accommodation_id_from_request
42
+ parser.accommodation_id.presence || vendor_id_of_room(updates.first&.room_id)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ module Spree::Api::SwitchCm
2
+ module Ota
3
+ # POST /api/switch_cm/hotels/ota/OTA_HotelRateAmountNotif, served by PushesController#create.
4
+ class RateAmountNotifsController < PushesController
5
+ private
6
+
7
+ # override
8
+ def parser_class = SpreeCmCommissioner::Integrations::SwitchCm::Ota::RateAmountNotifParser
9
+
10
+ # override
11
+ def message_name = 'OTA_HotelRateAmountNotif'
12
+
13
+ # override
14
+ def response_root = 'OTA_HotelRateAmountNotifRS'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ module Spree::Api::SwitchCm
2
+ module Ota
3
+ # GET and POST /api/switch_cm/hotels/ota/OTA_HotelResModifyNotif, served by
4
+ # ReservationStreamsController: cancelled reservations, and their acknowledgements.
5
+ # BookMe+ doesn't modify a reservation's rooms or dates, so this stream only carries
6
+ # cancellations, including those cancelled before their new-reservation acknowledgement.
7
+ class ResModifyNotifsController < ReservationStreamsController
8
+ private
9
+
10
+ # override
11
+ def feed_method = :cancelled_reservations
12
+
13
+ # override
14
+ def acknowledged_status = :cancelled
15
+
16
+ # override
17
+ def notification_root = 'OTA_HotelResModifyNotifRQ'
18
+
19
+ # override
20
+ def response_root = 'OTA_HotelResModifyNotifRS'
21
+
22
+ # override
23
+ def container = :HotelResModifies
24
+
25
+ # override
26
+ def present(presenter, xml)
27
+ presenter.build_modification(xml)
28
+ end
29
+ end
30
+ end
31
+ end