spree_cm_commissioner 2.13.2 → 2.13.3.pre.pre2

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 (278) 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 +5 -7
  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/models/concerns/spree_cm_commissioner/audit_wrapper.rb +139 -0
  46. data/app/models/concerns/spree_cm_commissioner/homepage_section_bitwise.rb +37 -1
  47. data/app/models/concerns/spree_cm_commissioner/integrations/inventory_sync_cachable.rb +9 -0
  48. data/app/models/concerns/spree_cm_commissioner/line_item_open_dated_trippable.rb +2 -2
  49. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +10 -0
  50. data/app/models/concerns/spree_cm_commissioner/phone_number_sanitizer.rb +5 -1
  51. data/app/models/concerns/spree_cm_commissioner/user_notification.rb +3 -1
  52. data/app/models/concerns/spree_cm_commissioner/variant_metadata.rb +24 -0
  53. data/app/models/concerns/spree_cm_commissioner/vehicle_kind.rb +1 -1
  54. data/app/models/spree_cm_commissioner/adjustable/adjuster/service_fee.rb +31 -0
  55. data/app/models/spree_cm_commissioner/adjustment_decorator.rb +1 -0
  56. data/app/models/spree_cm_commissioner/audit_event.rb +11 -0
  57. data/app/models/spree_cm_commissioner/calculators/service_fee_flat_amount.rb +17 -0
  58. data/app/models/spree_cm_commissioner/calculators/service_fee_percent.rb +20 -0
  59. data/app/models/spree_cm_commissioner/currency_rate.rb +15 -0
  60. data/app/models/spree_cm_commissioner/customer_notification.rb +15 -1
  61. data/app/models/spree_cm_commissioner/homepage_section.rb +25 -0
  62. data/app/models/spree_cm_commissioner/integration.rb +46 -0
  63. data/app/models/spree_cm_commissioner/integrations/book_me_bus_v1.rb +64 -0
  64. data/app/models/spree_cm_commissioner/integrations/larryta.rb +81 -0
  65. data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +4 -10
  66. data/app/models/spree_cm_commissioner/integrations/switch_cm.rb +86 -0
  67. data/app/models/spree_cm_commissioner/integrations/vireak_buntham.rb +82 -0
  68. data/app/models/spree_cm_commissioner/inventory_item.rb +18 -0
  69. data/app/models/spree_cm_commissioner/line_item_decorator.rb +2 -0
  70. data/app/models/spree_cm_commissioner/order_decorator.rb +13 -1
  71. data/app/models/spree_cm_commissioner/order_updater_decorator.rb +31 -0
  72. data/app/models/spree_cm_commissioner/place.rb +2 -0
  73. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -1
  74. data/app/models/spree_cm_commissioner/route.rb +1 -0
  75. data/app/models/spree_cm_commissioner/seat_layout.rb +1 -0
  76. data/app/models/spree_cm_commissioner/service_fee_rule.rb +183 -0
  77. data/app/models/spree_cm_commissioner/stock/availability_checker.rb +1 -1
  78. data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +1 -1
  79. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +4 -0
  80. data/app/models/spree_cm_commissioner/trip.rb +2 -1
  81. data/app/models/spree_cm_commissioner/trip_stop.rb +2 -0
  82. data/app/models/spree_cm_commissioner/user_decorator.rb +4 -0
  83. data/app/models/spree_cm_commissioner/variant_decorator.rb +5 -0
  84. data/app/models/spree_cm_commissioner/vehicle.rb +3 -0
  85. data/app/models/spree_cm_commissioner/vehicle_type.rb +1 -0
  86. data/app/models/spree_cm_commissioner/vendor_decorator.rb +17 -0
  87. data/app/models/spree_cm_commissioner/vendor_place.rb +2 -0
  88. data/app/overrides/spree/admin/orders/index/line_items_row.html.erb.deface +14 -0
  89. data/app/overrides/spree/admin/orders/index/row_actions.html.erb.deface +17 -0
  90. data/app/overrides/spree/admin/shared/sub_menu/_configuration/menu_items.html.erb.deface +5 -0
  91. data/app/queries/spree_cm_commissioner/users_by_event_fetcher_query.rb +6 -7
  92. data/app/serializables/spree_cm_commissioner/item_availability_sync.rb +10 -0
  93. data/app/serializers/spree/api/v2/platform/service_fee_rule_serializer.rb +17 -0
  94. data/app/serializers/spree/v2/storefront/homepage_section_serializer.rb +1 -1
  95. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +8 -2
  96. data/app/serializers/spree/v2/tenant/cart_serializer.rb +2 -1
  97. data/app/serializers/spree/v2/tenant/line_item_serializer.rb +7 -1
  98. data/app/serializers/spree_cm_commissioner/v2/operator/guest_serializer.rb +2 -1
  99. data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +2 -1
  100. data/app/serializers/spree_cm_commissioner/v2/storefront/cross_app_handoff_token_serializer.rb +9 -0
  101. data/app/serializers/spree_cm_commissioner/v2/storefront/item_availability_sync_serializer.rb +10 -0
  102. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +1 -1
  103. data/app/services/spree_cm_commissioner/audit_logger.rb +59 -0
  104. data/app/services/spree_cm_commissioner/checkout/advance_to_payment.rb +27 -0
  105. data/app/services/spree_cm_commissioner/cross_app_handoff_tokens/decode.rb +42 -0
  106. data/app/services/spree_cm_commissioner/cross_app_handoff_tokens/encode.rb +47 -0
  107. data/app/services/spree_cm_commissioner/currency_converter/create.rb +27 -0
  108. data/app/services/spree_cm_commissioner/currency_rates/manage.rb +99 -0
  109. data/app/services/spree_cm_commissioner/draft_orders/create.rb +1 -0
  110. data/app/services/spree_cm_commissioner/google_maps_geocoder.rb +41 -0
  111. data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +1 -2
  112. data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +11 -0
  113. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/client.rb +230 -0
  114. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/connection.rb +147 -0
  115. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_item_status.rb +52 -0
  116. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_items.rb +91 -0
  117. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/unstock_inventory.rb +188 -0
  118. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_inventory.rb +79 -0
  119. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_item_availability.rb +354 -0
  120. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_locations.rb +103 -0
  121. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_nationality_pricing.rb +93 -0
  122. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_product.rb +70 -0
  123. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_route_stops.rb +121 -0
  124. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_routes.rb +128 -0
  125. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_seat_layout.rb +412 -0
  126. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_service_calendars.rb +59 -0
  127. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +224 -0
  128. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trips.rb +769 -0
  129. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_variant.rb +69 -0
  130. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_vehicle_types.rb +92 -0
  131. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/time_parser.rb +19 -0
  132. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/base.rb +40 -0
  133. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/inventory_item_status.rb +148 -0
  134. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/location.rb +55 -0
  135. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/reservation_cart.rb +85 -0
  136. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat.rb +128 -0
  137. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb +386 -0
  138. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/stop.rb +84 -0
  139. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/trip.rb +237 -0
  140. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/wallet_payment.rb +48 -0
  141. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_manager.rb +22 -0
  142. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/full_sync_strategy.rb +116 -0
  143. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/incremental_sync_strategy.rb +63 -0
  144. data/app/services/spree_cm_commissioner/integrations/larryta/external_client/client.rb +314 -0
  145. data/app/services/spree_cm_commissioner/integrations/larryta/external_client/connection.rb +74 -0
  146. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/external_inventory_item_status.rb +53 -0
  147. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/external_inventory_items.rb +106 -0
  148. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/hold_seats.rb +143 -0
  149. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/release_seats.rb +90 -0
  150. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/unstock_inventory.rb +143 -0
  151. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_item_availability.rb +325 -0
  152. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_product.rb +72 -0
  153. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_routes.rb +142 -0
  154. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_seat_layout.rb +255 -0
  155. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_stock_item.rb +55 -0
  156. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_trip_stop.rb +171 -0
  157. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_trips.rb +397 -0
  158. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_variant.rb +63 -0
  159. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_vehicle_type.rb +79 -0
  160. data/app/services/spree_cm_commissioner/integrations/larryta/resources/base.rb +39 -0
  161. data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking.rb +113 -0
  162. data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking_session.rb +43 -0
  163. data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking_transaction.rb +236 -0
  164. data/app/services/spree_cm_commissioner/integrations/larryta/resources/direction.rb +71 -0
  165. data/app/services/spree_cm_commissioner/integrations/larryta/resources/passenger.rb +48 -0
  166. data/app/services/spree_cm_commissioner/integrations/larryta/resources/schedule.rb +150 -0
  167. data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat.rb +31 -0
  168. data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat_availability.rb +49 -0
  169. data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat_layout.rb +211 -0
  170. data/app/services/spree_cm_commissioner/integrations/larryta/sync_manager.rb +17 -0
  171. data/app/services/spree_cm_commissioner/integrations/larryta/sync_strategies/full_sync_strategy.rb +79 -0
  172. data/app/services/spree_cm_commissioner/integrations/larryta/sync_strategies/incremental_sync_strategy.rb +58 -0
  173. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +14 -15
  174. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +7 -4
  175. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +6 -3
  176. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +0 -3
  177. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +1 -3
  178. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +1 -3
  179. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +1 -2
  180. data/app/services/spree_cm_commissioner/integrations/switch_cm/accept_push.rb +101 -0
  181. data/app/services/spree_cm_commissioner/integrations/switch_cm/accommodation_lock.rb +41 -0
  182. data/app/services/spree_cm_commissioner/integrations/switch_cm/acknowledge_reservations.rb +43 -0
  183. data/app/services/spree_cm_commissioner/integrations/switch_cm/apply_ari.rb +119 -0
  184. data/app/services/spree_cm_commissioner/integrations/switch_cm/ari_update.rb +24 -0
  185. data/app/services/spree_cm_commissioner/integrations/switch_cm/availability_readback.rb +86 -0
  186. data/app/services/spree_cm_commissioner/integrations/switch_cm/mark_seen.rb +53 -0
  187. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/avail_notif_parser.rb +60 -0
  188. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/base_parser.rb +60 -0
  189. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/push_response.rb +38 -0
  190. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/rate_amount_notif_parser.rb +46 -0
  191. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/reservation_presenter.rb +99 -0
  192. data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/response_builder.rb +52 -0
  193. data/app/services/spree_cm_commissioner/integrations/switch_cm/pushed_nights.rb +79 -0
  194. data/app/services/spree_cm_commissioner/integrations/switch_cm/reservation.rb +118 -0
  195. data/app/services/spree_cm_commissioner/integrations/switch_cm/reservation_feed.rb +143 -0
  196. data/app/services/spree_cm_commissioner/integrations/switch_cm/room_catalog.rb +56 -0
  197. data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_manager.rb +16 -0
  198. data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/full_sync_strategy.rb +39 -0
  199. data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/incremental_sync_strategy.rb +51 -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/views/spree/admin/customer_notifications/_form.html.erb +9 -9
  243. data/app/views/spree/admin/customer_notifications/index.html.erb +36 -19
  244. data/app/views/spree/admin/homepage_section/_form.html.erb +19 -2
  245. data/app/views/spree/admin/homepage_section/_tabs.html.erb +7 -7
  246. data/app/views/spree/admin/homepage_section/index.html.erb +73 -6
  247. data/app/views/spree/admin/integrations/_book_me_bus_v1_fields.html.erb +47 -0
  248. data/app/views/spree/admin/integrations/_form.html.erb +16 -0
  249. data/app/views/spree/admin/integrations/_larryta_fields.html.erb +58 -0
  250. data/app/views/spree/admin/integrations/_switch_cm_fields.html.erb +53 -0
  251. data/app/views/spree/admin/integrations/_vireak_buntham_fields.html.erb +75 -0
  252. data/app/views/spree/admin/integrations/index.html.erb +1 -1
  253. data/app/views/spree/admin/notifications/index.html.erb +2 -1
  254. data/app/views/spree/admin/orders/line_items.html.erb +53 -0
  255. data/app/views/spree/admin/service_fee_rules/_form.html.erb +119 -0
  256. data/app/views/spree/admin/service_fee_rules/edit.html.erb +11 -0
  257. data/app/views/spree/admin/service_fee_rules/index.html.erb +91 -0
  258. data/app/views/spree/admin/service_fee_rules/new.html.erb +11 -0
  259. data/app/views/spree/admin/shared/_send_all_form.html.erb +1 -0
  260. data/app/views/spree/admin/shared/_send_test_form.html.erb +3 -3
  261. data/app/views/spree/admin/stock_managements/index.html.erb +14 -0
  262. data/config/locales/en.yml +36 -0
  263. data/config/locales/km.yml +32 -0
  264. data/config/routes.rb +34 -0
  265. data/db/migrate/20260327143200_create_cm_audit_events.rb +21 -0
  266. data/db/migrate/20260410045815_create_cm_currency_rates.rb +23 -0
  267. data/db/migrate/20260411102139_add_metadata_to_cm_inventory_items.rb +6 -0
  268. data/db/migrate/20260914000000_add_mini_app_to_homepage_sections.rb +5 -0
  269. data/db/migrate/20260917085551_create_cm_service_fee_rules.rb +16 -0
  270. data/db/migrate/20260917085552_add_service_fee_total_to_spree_line_items_and_orders.rb +6 -0
  271. data/db/migrate/20260928000000_add_oauth_application_id_to_cm_integrations.rb +5 -0
  272. data/db/migrate/20260928000001_add_closed_to_cm_inventory_items.rb +5 -0
  273. data/lib/spree_cm_commissioner/cached_inventory_item.rb +7 -1
  274. data/lib/spree_cm_commissioner/engine.rb +3 -0
  275. data/lib/spree_cm_commissioner/test_helper/factories/homepage_section_factory.rb +7 -0
  276. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +17 -0
  277. data/lib/spree_cm_commissioner/version.rb +1 -1
  278. metadata +187 -4
@@ -8,8 +8,10 @@ module SpreeCmCommissioner
8
8
 
9
9
  scope :push_notificable, -> { where('device_tokens_count > 0') }
10
10
 
11
+ # Previously chained with `end_users`, which excludes users who are assigned to a vendor (organizers),
12
+ # so organizers couldn't test notifications on their own account. Dropped that scope here.
11
13
  def self.end_users_push_notificable
12
- end_users.push_notificable
14
+ push_notificable
13
15
  end
14
16
  end
15
17
  end
@@ -0,0 +1,24 @@
1
+ module SpreeCmCommissioner
2
+ module VariantMetadata
3
+ extend ActiveSupport::Concern
4
+
5
+ def manual_generate_inventory_items?
6
+ ActiveModel::Type::Boolean.new.cast(private_metadata['manual_generate_inventory_items'])
7
+ end
8
+
9
+ def manual_generate_inventory_items=(value)
10
+ boolean_value = ActiveModel::Type::Boolean.new.cast(value)
11
+ set_private_metadata_value('manual_generate_inventory_items', boolean_value)
12
+ end
13
+
14
+ private
15
+
16
+ def set_private_metadata_value(key, value)
17
+ if value.nil? || value == false
18
+ private_metadata.delete(key)
19
+ else
20
+ private_metadata[key] = value.to_s
21
+ end
22
+ end
23
+ end
24
+ end
@@ -2,7 +2,7 @@ module SpreeCmCommissioner
2
2
  module VehicleKind
3
3
  extend ActiveSupport::Concern
4
4
 
5
- VEHICLE_KINDS = %i[suv sedan minivan van sleeping_bus luxury_van air_bus bus ferry].freeze
5
+ VEHICLE_KINDS = %i[suv sedan minivan van sleeping_bus luxury_van air_bus bus ferry boat train].freeze
6
6
 
7
7
  included do
8
8
  enum kind: VEHICLE_KINDS if table_exists? && column_names.include?('kind')
@@ -0,0 +1,31 @@
1
+ module SpreeCmCommissioner
2
+ module Adjustable
3
+ module Adjuster
4
+ class ServiceFee < Spree::Adjustable::Adjuster::Base
5
+ def update
6
+ service_fee_total = adjustments.service_fee.eligible.reload.sum(&:amount)
7
+
8
+ update_totals(service_fee_total)
9
+ end
10
+
11
+ private
12
+
13
+ def adjustments
14
+ adjustable.try(:all_adjustments) || adjustable.adjustments
15
+ end
16
+
17
+ # Folds into the existing non_taxable_adjustment_total bucket (same as the
18
+ # PricingAction adjuster) so order.total/adjustment_total are correct with no core
19
+ # decoration needed. `service_fee_total` is set as its own key only for line items —
20
+ # it becomes its own DB column there (for a clean "Service Fee: $X" display total),
21
+ # but Spree::Shipment has no such column, and this same adjuster instance also runs
22
+ # for shipments (adjusters are global, not adjustable-type-specific).
23
+ def update_totals(service_fee_total)
24
+ service_fee_total ||= 0.0
25
+ @totals[:non_taxable_adjustment_total] += service_fee_total
26
+ @totals[:service_fee_total] = service_fee_total if adjustable.is_a?(Spree::LineItem)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -3,6 +3,7 @@ module SpreeCmCommissioner
3
3
  def self.prepended(base)
4
4
  base.belongs_to :payable, polymorphic: true, optional: true
5
5
  base.scope :pricing_action, -> { where(source_type: 'SpreeCmCommissioner::PricingAction') }
6
+ base.scope :service_fee, -> { where(source_type: 'SpreeCmCommissioner::ServiceFeeRule') }
6
7
  base.whitelisted_ransackable_attributes |= %w[payable_id]
7
8
  end
8
9
 
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ class AuditEvent < Base
3
+ belongs_to :auditable, polymorphic: true, optional: true
4
+ belongs_to :user, class_name: 'Spree::User', optional: true
5
+
6
+ validates :event_type, presence: true
7
+
8
+ scope :success, -> { where(success: true) }
9
+ scope :failed, -> { where(success: false) }
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module SpreeCmCommissioner
2
+ module Calculators
3
+ class ServiceFeeFlatAmount < Spree::Calculator
4
+ preference :flat_amount, :decimal, default: 0
5
+
6
+ def self.description
7
+ Spree.t(:service_fee_flat_amount)
8
+ end
9
+
10
+ # Per unit, not per line item — a $0.25 fee on a 2-ticket line item charges $0.50, matching
11
+ # how a booking fee is normally priced (confirmed decision, see plan.md).
12
+ def compute(line_item)
13
+ preferred_flat_amount * line_item.quantity
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module SpreeCmCommissioner
2
+ module Calculators
3
+ class ServiceFeePercent < Spree::Calculator
4
+ preference :percent, :decimal, default: 0
5
+
6
+ def self.description
7
+ Spree.t(:service_fee_percent)
8
+ end
9
+
10
+ # `discounted_amount` (price net of taxable promotions, before tax) rather than
11
+ # `pre_tax_amount`: the latter is only ever populated as a side effect of
12
+ # Spree::TaxRate.store_pre_tax_amount running for this line item, so it would make
13
+ # service-fee computation order-dependent on tax having already run. discounted_amount
14
+ # is a live computed value (amount + taxable_adjustment_total) with no such dependency.
15
+ def compute(line_item)
16
+ line_item.discounted_amount * preferred_percent / 100
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ module SpreeCmCommissioner
2
+ class CurrencyRate < SpreeCmCommissioner::Base
3
+ belongs_to :vendor, class_name: 'Spree::Vendor', optional: false
4
+
5
+ validates :from_currency, :to_currency, :rate, presence: true
6
+ validates :rate, numericality: { greater_than: 0 }
7
+ validates :from_currency, :to_currency, inclusion: { in: %w[USD KHR] }
8
+
9
+ self.whitelisted_ransackable_attributes = %w[from_currency to_currency status]
10
+
11
+ def self.by_pair(from, to)
12
+ where(from_currency: from.upcase, to_currency: to.upcase, status: 'active')
13
+ end
14
+ end
15
+ end
@@ -9,12 +9,14 @@ module SpreeCmCommissioner
9
9
  has_many :notification_taxons, dependent: :destroy, class_name: 'SpreeCmCommissioner::NotificationTaxon'
10
10
  has_many :taxons, through: :notification_taxons, class_name: 'Spree::Taxon'
11
11
 
12
- belongs_to :vendor, class_name: 'Spree::Vendor'
12
+ belongs_to :vendor, class_name: 'Spree::Vendor', optional: true
13
13
 
14
14
  validates :title, presence: true
15
15
 
16
16
  validates :notification_type, presence: true
17
17
 
18
+ validate :taxons_must_belong_to_vendor
19
+
18
20
  self.whitelisted_ransackable_attributes = %w[title excerpt notification_type]
19
21
  self.whitelisted_ransackable_associations = %w[notification_taxons taxons]
20
22
 
@@ -33,5 +35,17 @@ module SpreeCmCommissioner
33
35
  def self.scheduled_items
34
36
  where(['sent_at IS NULL AND started_at <= ? AND active', Time.current])
35
37
  end
38
+
39
+ private
40
+
41
+ # Prevents picking a taxon that isn't one of the selected vendor's own taxons
42
+ def taxons_must_belong_to_vendor
43
+ return if vendor_id.blank? || taxon_ids.blank?
44
+
45
+ invalid_taxon_ids = taxon_ids - vendor.taxon_ids
46
+ return if invalid_taxon_ids.empty?
47
+
48
+ errors.add(:taxon_ids, :must_belong_to_vendor, vendor_name: vendor.name)
49
+ end
36
50
  end
37
51
  end
@@ -8,6 +8,9 @@ module SpreeCmCommissioner
8
8
  multi_tenant :tenant, class_name: 'SpreeCmCommissioner::Tenant'
9
9
 
10
10
  attribute :segment, :integer, default: 0
11
+ attribute :mini_app, :integer, default: 0
12
+
13
+ self.whitelisted_ransackable_attributes = %w[title active]
11
14
 
12
15
  has_many :homepage_section_relatables, -> { active }, inverse_of: :homepage_section, dependent: :destroy
13
16
 
@@ -20,6 +23,28 @@ module SpreeCmCommissioner
20
23
 
21
24
  scope :filter_by_segment, -> (segment) { where('segment & ? > 0', BIT_SEGMENT[segment.to_sym]) }
22
25
 
26
+ # Sections tagged for `mini_app`; an unknown value means the untagged ones.
27
+ scope :filter_by_mini_app, lambda { |mini_app|
28
+ bit = BIT_MINI_APP[mini_app.to_s.to_sym]
29
+
30
+ bit ? where('cm_homepage_sections.mini_app & ? > 0', bit) : where(mini_app: 0)
31
+ }
32
+
33
+ # Which sections a homepage feed shows. Using Wing as the example:
34
+ #
35
+ # BookMe+ -> every section of this segment; ticking a Mini App never hides one here
36
+ # Wing -> sections of this segment with Wing ticked; if none, the BookMe+ ones
37
+ scope :for_feed, lambda { |segment, mini_app = nil|
38
+ name = mini_app.to_s.to_sym
39
+ everything = filter_by_segment(segment)
40
+ next everything unless BIT_MINI_APP.key?(name)
41
+
42
+ curated = filter_by_mini_app(name)
43
+ curated = curated.filter_by_segment(segment) unless MINI_APP_HOME_SEGMENT[name] == segment.to_sym
44
+
45
+ curated.exists? ? curated : everything
46
+ }
47
+
23
48
  # Create maintaining task to purge homepage section related caches
24
49
  after_save { SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(maintainable: self) }
25
50
  end
@@ -14,10 +14,25 @@ module SpreeCmCommissioner
14
14
  validates :incremental_sync_interval_seconds, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 3600 }
15
15
  validates :full_sync_interval_hours, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 168 }
16
16
 
17
+ # Listed from the folder rather than `descendants`, which misses subclasses Zeitwerk hasn't
18
+ # loaded yet in dev and test.
19
+ def self.types
20
+ Dir[SpreeCmCommissioner::Engine.root.join('app/models/spree_cm_commissioner/integrations/*.rb')]
21
+ .map { |path| "SpreeCmCommissioner::Integrations::#{File.basename(path, '.rb').camelize}" }
22
+ .sort
23
+ end
24
+
17
25
  def sync_manager
18
26
  raise NotImplementedError, 'Subclasses must implement the sync_manager method'
19
27
  end
20
28
 
29
+ # Whether the partner, not BookMe+, sets this variant's per-night stock, prices and closings.
30
+ # When true, stock resets and dashboard edits leave its inventory items alone, and new nights
31
+ # start closed until the partner opens them (InventoryItem). Off by default.
32
+ def inventory_external_wins?(_variant)
33
+ false
34
+ end
35
+
21
36
  def restock_external_inventory!(_order, _line_items)
22
37
  raise NotImplementedError, 'Subclasses must implement the restock_external_inventory! method'
23
38
  end
@@ -25,5 +40,36 @@ module SpreeCmCommissioner
25
40
  def unstock_external_inventory!(_order, _line_items)
26
41
  raise NotImplementedError, 'Subclasses must implement the unstock_external_inventory! method'
27
42
  end
43
+
44
+ def sync_item_availability!(_options = {})
45
+ raise NotImplementedError, 'Subclasses must implement the sync_item_availability! method'
46
+ end
47
+
48
+ def hold_inventory!(_order, _line_items)
49
+ raise NotImplementedError, 'Subclasses must implement the hold_inventory! method'
50
+ end
51
+
52
+ # Integrations that manage external inventory reservations should override this to return true
53
+ def supports_external_inventory_holding?
54
+ false
55
+ end
56
+
57
+ # Integrations that support automatically releasing external inventory
58
+ # after reservation expiration or order cancellation should override this to return true
59
+ def supports_auto_release_inventory?
60
+ false
61
+ end
62
+
63
+ def release_inventory!(_order, _line_items)
64
+ raise NotImplementedError, 'Subclasses must implement the release_inventory! method'
65
+ end
66
+
67
+ def external_inventory_items(_order)
68
+ raise NotImplementedError, 'Subclasses must implement the external_inventory_items method'
69
+ end
70
+
71
+ def external_inventory_item_status(_order, _options)
72
+ raise NotImplementedError, 'Subclasses must implement the external_inventory_item_status method'
73
+ end
28
74
  end
29
75
  end
@@ -0,0 +1,64 @@
1
+ class SpreeCmCommissioner::Integrations::BookMeBusV1 < SpreeCmCommissioner::Integration
2
+ store_private_metadata :client_id, :string
3
+ store_private_metadata :client_secret, :string
4
+ store_private_metadata :base_url, :string # e.g. 'https://bookmebus.com'
5
+ store_private_metadata :username, :string # Contact email (optional)
6
+ store_private_metadata :access_token, :string
7
+ store_private_metadata :token_expires_at, :datetime
8
+ store_private_metadata :operator_id, :string
9
+ # override
10
+ def sync_manager
11
+ SpreeCmCommissioner::Integrations::BookMeBusV1::SyncManager.new(integration: self)
12
+ end
13
+
14
+ # override
15
+ def unstock_external_inventory!(order, line_items)
16
+ result = SpreeCmCommissioner::Integrations::BookMeBusV1::Inventory::UnstockInventory.new.call(
17
+ integration: self,
18
+ order: order,
19
+ line_items: line_items
20
+ )
21
+
22
+ raise SpreeCmCommissioner::Integrations::SyncError, result.error unless result.success?
23
+ end
24
+
25
+ # override
26
+ # BookMeBus has no working cancellation API (its public reservation-cancellation endpoint is
27
+ # disabled in production), so there is nothing to actually restock on their side. Raise instead
28
+ # of silently succeeding - matching VireakBuntham/Larryta - so order cancellation surfaces this
29
+ # loudly rather than letting local inventory quietly diverge from what BookMeBus still shows as
30
+ # booked (a silent no-op here would let BookMe+ resell a seat BookMeBus still considers taken).
31
+ def restock_external_inventory!(_order, _line_items)
32
+ raise SpreeCmCommissioner::Integrations::SyncError, 'Booking cannot be cancelled once confirmed'
33
+ end
34
+
35
+ # override - Fetch all external inventory items
36
+ def external_inventory_items(order)
37
+ SpreeCmCommissioner::Integrations::BookMeBusV1::Inventory::ExternalInventoryItems.new.call(
38
+ integration: self,
39
+ order: order
40
+ )
41
+ end
42
+
43
+ # override - Fetch detailed status for a specific external inventory item
44
+ def external_inventory_item_status(order, options)
45
+ SpreeCmCommissioner::Integrations::BookMeBusV1::Inventory::ExternalInventoryItemStatus.new.call(
46
+ integration: self,
47
+ order: order,
48
+ options: options
49
+ )
50
+ end
51
+
52
+ def sync_item_availability!(options = {})
53
+ SpreeCmCommissioner::Integrations::BookMeBusV1::Polling::SyncItemAvailability.new(
54
+ integration: self
55
+ ).call(
56
+ trip_id: options[:trip_id],
57
+ on_date: options[:on_date]
58
+ )
59
+ end
60
+
61
+ def client
62
+ @client ||= SpreeCmCommissioner::Integrations::BookMeBusV1::ExternalClient::Client.new(integration: self)
63
+ end
64
+ end
@@ -0,0 +1,81 @@
1
+ class SpreeCmCommissioner::Integrations::Larryta < SpreeCmCommissioner::Integration
2
+ store_private_metadata :base_url, :string
3
+ store_private_metadata :username, :string
4
+ store_private_metadata :api_token, :string
5
+ store_private_metadata :api_uid, :string
6
+ store_private_metadata :sync_days, :integer, default: 7
7
+
8
+ def sync_manager
9
+ SpreeCmCommissioner::Integrations::Larryta::SyncManager.new(integration: self)
10
+ end
11
+
12
+ def hold_inventory!(order, line_items)
13
+ result = SpreeCmCommissioner::Integrations::Larryta::Inventory::HoldSeats.new.call(
14
+ integration: self,
15
+ order: order,
16
+ line_items: line_items
17
+ )
18
+
19
+ raise SpreeCmCommissioner::Integrations::SyncError, result.error unless result.success?
20
+ end
21
+
22
+ def unstock_external_inventory!(order, line_items)
23
+ result = SpreeCmCommissioner::Integrations::Larryta::Inventory::UnstockInventory.new.call(
24
+ integration: self,
25
+ order: order,
26
+ line_items: line_items
27
+ )
28
+
29
+ raise SpreeCmCommissioner::Integrations::SyncError, result.error unless result.success?
30
+ end
31
+
32
+ def supports_external_inventory_holding?
33
+ true
34
+ end
35
+
36
+ def restock_external_inventory!(_order, _line_items)
37
+ raise SpreeCmCommissioner::Integrations::SyncError, 'Booking cannot be cancelled once confirmed'
38
+ end
39
+
40
+ def supports_auto_release_inventory?
41
+ true
42
+ end
43
+
44
+ def release_inventory!(order, line_items)
45
+ SpreeCmCommissioner::Integrations::Larryta::Inventory::ReleaseSeats.new.call(
46
+ integration: self,
47
+ order: order,
48
+ line_items: line_items
49
+ )
50
+ end
51
+
52
+ def sync_item_availability!(options = {})
53
+ SpreeCmCommissioner::Integrations::Larryta::Polling::SyncItemAvailability.new(
54
+ integration: self
55
+ ).call(
56
+ trip_id: options[:trip_id],
57
+ on_date: options[:on_date]
58
+ )
59
+ end
60
+
61
+ # override - Fetch all external inventory items for an order
62
+ def external_inventory_items(order)
63
+ SpreeCmCommissioner::Integrations::Larryta::Inventory::ExternalInventoryItems.new.call(
64
+ integration: self,
65
+ order: order
66
+ )
67
+ end
68
+
69
+ # override - Fetch detailed status for a specific external inventory item
70
+ def external_inventory_item_status(order, options)
71
+ SpreeCmCommissioner::Integrations::Larryta::Inventory::ExternalInventoryItemStatus.new.call(
72
+ integration: self,
73
+ order: order,
74
+ options: options
75
+ )
76
+ end
77
+
78
+ def client
79
+ @client ||= SpreeCmCommissioner::Integrations::Larryta::ExternalClient::Client.new(integration: self)
80
+ end
81
+ end
@@ -5,10 +5,7 @@ class SpreeCmCommissioner::Integrations::StadiumXV1 < SpreeCmCommissioner::Integ
5
5
 
6
6
  # override
7
7
  def sync_manager
8
- SpreeCmCommissioner::Integrations::StadiumXV1::SyncManager.new(
9
- integration: self,
10
- client: client
11
- )
8
+ SpreeCmCommissioner::Integrations::StadiumXV1::SyncManager.new(integration: self)
12
9
  end
13
10
 
14
11
  # override
@@ -18,7 +15,6 @@ class SpreeCmCommissioner::Integrations::StadiumXV1 < SpreeCmCommissioner::Integ
18
15
  order: order,
19
16
  line_items: line_items
20
17
  )
21
-
22
18
  raise SpreeCmCommissioner::Integrations::SyncError, result.error unless result.success?
23
19
  end
24
20
 
@@ -27,11 +23,9 @@ class SpreeCmCommissioner::Integrations::StadiumXV1 < SpreeCmCommissioner::Integ
27
23
  raise SpreeCmCommissioner::Integrations::SyncError, 'Ticket cannot be cancelled'
28
24
  end
29
25
 
26
+ def sync_item_availability!(_options = {}); end
27
+
30
28
  def client
31
- SpreeCmCommissioner::Integrations::StadiumXV1::ExternalClient::Client.new(
32
- public_key: public_key,
33
- private_key: private_key,
34
- base_url: base_url
35
- )
29
+ @client ||= SpreeCmCommissioner::Integrations::StadiumXV1::ExternalClient::Client.new(integration: self)
36
30
  end
37
31
  end
@@ -0,0 +1,86 @@
1
+ # A hotel connected to switch.cm, which calls our Booking.com-compatible channel-manager API
2
+ # (Spree::Api::SwitchCm) instead of us calling it: it pushes availability, prices and
3
+ # restrictions, and pulls reservations. So there's nothing to pull, hold or unstock externally;
4
+ # polling only runs our own checks (SyncManager).
5
+ #
6
+ # Every switch.cm row shares one Doorkeeper app; the hotels switch.cm may manage are the rows
7
+ # pointing at it. `paused` is the setup state: switch.cm can read the hotel (room list,
8
+ # reservation summary) while it keeps selling on its own counts. `active` with conflict strategy
9
+ # `external_wins` hands the rooms' availability, prices and restrictions over to switch.cm.
10
+ class SpreeCmCommissioner::Integrations::SwitchCm < SpreeCmCommissioner::Integration
11
+ READABLE_STATUSES = %i[active paused].freeze
12
+
13
+ belongs_to :oauth_application, class_name: 'Spree::OauthApplication', optional: false
14
+
15
+ # Reservations completed before this aren't in the reservation feeds: switch.cm imports them
16
+ # through the reservation summary while setting the hotel up.
17
+ store_public_metadata :connected_at, :datetime
18
+
19
+ before_create :set_connected_at
20
+ after_commit :sync_unpushed_nights, on: %i[create update]
21
+
22
+ # Polling runs the hotel's periodic checks (SyncManager), and the incremental one alerts on
23
+ # every run while reservations sit unpulled, so the column's 10-second default would flood
24
+ # Telegram. Full runs daily upkeep.
25
+ attribute :incremental_sync_interval_seconds, :integer, default: -> { SyncStrategies::IncrementalSyncStrategy::UNSEEN_AFTER.to_i }
26
+
27
+ scope :readable, -> { where(status: READABLE_STATUSES) }
28
+
29
+ # override
30
+ def sync_manager
31
+ SyncManager.new(integration: self)
32
+ end
33
+
34
+ # Only for rooms switch.cm can see (RoomCatalog): a hotel's other products, and rooms that need
35
+ # confirmation, keep BookMe+ counts since switch.cm never pushes them.
36
+ # override
37
+ def inventory_external_wins?(variant)
38
+ owns_inventory? && variant.accommodation? && !variant.need_confirmation?
39
+ end
40
+
41
+ def owns_inventory?
42
+ active? && external_wins?
43
+ end
44
+
45
+ # switch.cm pulls reservations, so a BookMe+ sale needs nothing from it at checkout, and the
46
+ # hotel's own cancellation rules decide whether a booking can be cancelled.
47
+ # override
48
+ def unstock_external_inventory!(_order, _line_items); end
49
+
50
+ # override
51
+ def restock_external_inventory!(_order, _line_items); end
52
+
53
+ # override
54
+ def sync_item_availability!(_options = {}); end
55
+
56
+ # override
57
+ def external_inventory_items(_order)
58
+ []
59
+ end
60
+
61
+ # override
62
+ def external_inventory_item_status(_order, _options)
63
+ {}
64
+ end
65
+
66
+ # While switch.cm owns the rooms, nights it hasn't pushed stay closed, so the admin page shows
67
+ # this until the first full push lands.
68
+ def push_progress
69
+ PushedNights.new(self).progress
70
+ end
71
+
72
+ private
73
+
74
+ def set_connected_at
75
+ self.connected_at ||= Time.current.iso8601
76
+ end
77
+
78
+ # Taking over closes the rooms' unpushed nights; letting go (paused, disconnected, or another
79
+ # conflict strategy) reopens them, while pushed nights keep switch.cm's last numbers.
80
+ def sync_unpushed_nights
81
+ owned_before = status_before_last_save.to_s == 'active' && conflict_strategy_before_last_save.to_s == 'external_wins'
82
+ return if owned_before == owns_inventory?
83
+
84
+ PushedNights.new(self).reconcile_unpushed!
85
+ end
86
+ end
@@ -0,0 +1,82 @@
1
+ class SpreeCmCommissioner::Integrations::VireakBuntham < SpreeCmCommissioner::Integration
2
+ # VET API requires a `type` parameter to scope responses to a vehicle category.
3
+ # Each VireakBuntham integration instance represents one category.
4
+ ROUTE_TYPES = { bus: 1, ferry: 2, air_bus: 3 }.freeze
5
+
6
+ store_private_metadata :api_url, :string
7
+ store_private_metadata :api_token, :string
8
+ store_private_metadata :callback_username, :string
9
+ store_private_metadata :callback_pwd, :string
10
+ store_private_metadata :sync_days, :integer, default: 7
11
+ store_private_metadata :route_type, :integer, default: 1
12
+
13
+ validate :route_type_must_be_valid
14
+
15
+ def route_type_name
16
+ ROUTE_TYPES.key(route_type.to_i)
17
+ end
18
+
19
+ # VET `type` query param used by /destination/from, /schedule/list, /schedule/listByDate
20
+ def vet_type_param
21
+ route_type.to_i
22
+ end
23
+
24
+ def sync_manager
25
+ SpreeCmCommissioner::Integrations::VireakBuntham::SyncManager.new(integration: self)
26
+ end
27
+
28
+ # Confirm and finalize booking on VET: POST /booking/confirm + /booking/complete in atomic transaction.
29
+ def unstock_external_inventory!(order, line_items)
30
+ result = SpreeCmCommissioner::Integrations::VireakBuntham::Inventory::UnstockInventory.new.call(
31
+ integration: self,
32
+ order: order,
33
+ line_items: line_items
34
+ )
35
+
36
+ raise SpreeCmCommissioner::Integrations::SyncError, result.error unless result.success?
37
+ end
38
+
39
+ def restock_external_inventory!(_order, _line_items)
40
+ raise SpreeCmCommissioner::Integrations::SyncError, 'Booking cannot be cancelled once confirmed'
41
+ end
42
+
43
+ # Sync trip prices and reserved seats from VET via GET /schedule/list + /seat/unavailable APIs.
44
+ def sync_item_availability!(options = {})
45
+ SpreeCmCommissioner::Integrations::VireakBuntham::Polling::SyncItemAvailability.new(
46
+ integration: self
47
+ ).call(
48
+ trip_id: options[:trip_id],
49
+ on_date: options[:on_date]
50
+ )
51
+ end
52
+
53
+ # Fetch available seats and prices from VET for order's trips.
54
+ def external_inventory_items(order)
55
+ SpreeCmCommissioner::Integrations::VireakBuntham::Inventory::ExternalInventoryItems.new.call(
56
+ integration: self,
57
+ order: order
58
+ )
59
+ end
60
+
61
+ # Check seat availability and pricing for specific trip/date/nationality from VET.
62
+ def external_inventory_item_status(order, options)
63
+ SpreeCmCommissioner::Integrations::VireakBuntham::Inventory::ExternalInventoryItemStatus.new.call(
64
+ integration: self,
65
+ order: order,
66
+ options: options
67
+ )
68
+ end
69
+
70
+ # Lazy-loaded HTTP client instance for all VET API calls.
71
+ def client
72
+ @client ||= SpreeCmCommissioner::Integrations::VireakBuntham::ExternalClient::Client.new(integration: self)
73
+ end
74
+
75
+ private
76
+
77
+ def route_type_must_be_valid
78
+ return if ROUTE_TYPES.values.include?(route_type.to_i)
79
+
80
+ errors.add(:route_type, "must be one of: #{ROUTE_TYPES.values.join(', ')}")
81
+ end
82
+ end