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
@@ -0,0 +1,103 @@
1
+ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
2
+ class SyncLocations
3
+ # Initialize SyncLocations service
4
+ # @param integration [SpreeCmCommissioner::Integrations::BookMeBusV1] The integration instance
5
+ # @param sync_result [SpreeCmCommissioner::Integrations::Base::SyncResult] Optional sync result tracker
6
+ def initialize(integration:, sync_result: nil)
7
+ @integration = integration
8
+ @sync_result = sync_result || SpreeCmCommissioner::Integrations::Base::SyncResult.new
9
+ end
10
+
11
+ def call
12
+ raise SpreeCmCommissioner::Integrations::SyncError, 'Integration must have a vendor to sync locations' if @integration.vendor.blank?
13
+
14
+ synced_location_mappings = sync_items!
15
+
16
+ cleanup_stale_mappings!(synced_location_mappings)
17
+ end
18
+
19
+ private
20
+
21
+ def client
22
+ @client ||= @integration.client
23
+ end
24
+
25
+ def geocoder
26
+ @geocoder ||= SpreeCmCommissioner::GoogleMapsGeocoder.new
27
+ end
28
+
29
+ # Sync all available locations from the API and create them as places
30
+ def sync_items!
31
+ external_locations = begin
32
+ @sync_result.track_api_call('get_locations') { client.get_locations! }
33
+ rescue SpreeCmCommissioner::Integrations::ExternalClientError => e
34
+ raise SpreeCmCommissioner::Integrations::SyncError, "Failed to sync locations: #{e.message}"
35
+ end
36
+
37
+ external_locations.map do |external_location|
38
+ sync_item!(external_location)
39
+ end
40
+ end
41
+
42
+ # Sync a single location and create it as a place with vendor place mapping
43
+ def sync_item!(external_location)
44
+ mapping = SpreeCmCommissioner::VendorPlace.find_or_initialize_integration_mapping(
45
+ integration_id: @integration.id,
46
+ external_id: external_location.id
47
+ )
48
+
49
+ vendor_place = mapping.internal
50
+
51
+ place = vendor_place.place ||
52
+ SpreeCmCommissioner::Place.find_by(name: external_location.name) ||
53
+ SpreeCmCommissioner::Place.new
54
+
55
+ @sync_result.track(:location, place) do |tracker|
56
+ if place.new_record? || place.lat.nil? || place.lon.nil?
57
+ location_data = geocoder.fetch_location_data(external_location.name, external_location.country_name)
58
+ place.assign_attributes(
59
+ lat: location_data[:lat],
60
+ lon: location_data[:lon],
61
+ reference: location_data[:reference]
62
+ )
63
+ end
64
+
65
+ place.assign_attributes(
66
+ name: external_location.name,
67
+ code: external_location.abbreviation.presence || external_location.slug.presence
68
+ )
69
+
70
+ tracker.save_if_changed!(place)
71
+ end
72
+
73
+ @sync_result.track(:vendor_place, vendor_place) do |tracker|
74
+ vendor_place.assign_attributes(
75
+ vendor_id: @integration.vendor.id,
76
+ place_type: :location,
77
+ place: place
78
+ )
79
+ tracker.save_if_changed!(vendor_place)
80
+ end
81
+
82
+ mapping.mark_as_active!(external_payload: external_location.to_h)
83
+ mapping
84
+ end
85
+
86
+ # Archive stale location mappings that are no longer in the external API
87
+ def cleanup_stale_mappings!(synced_location_mappings)
88
+ synced_ids = synced_location_mappings.to_set(&:external_id)
89
+ active_mappings = SpreeCmCommissioner::IntegrationMapping.where(
90
+ integration_id: @integration.id,
91
+ internal_type: 'SpreeCmCommissioner::VendorPlace',
92
+ status: :active
93
+ )
94
+
95
+ active_mappings.find_each do |mapping|
96
+ next if synced_ids.include?(mapping.external_id)
97
+
98
+ mapping.mark_as_archived!
99
+ @sync_result.increment_metric(:location, :archived)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,93 @@
1
+ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
2
+ # SyncNationalityPricing - applies BookMeBus local/non-local pricing to a Route via a
3
+ # PricingModel, so BookMe+ charges the correct price based on the ACTUAL guest's nationality at
4
+ # checkout instead of always flatly charging non_local_price (Resources::Trip#sellable_price,
5
+ # unchanged by this class) to every customer.
6
+ #
7
+ # Deliberately keeps non_local_price as the base (matching sellable_price's existing default)
8
+ # and layers a DISCOUNT adjustment on top for guests who positively match the vendor's own
9
+ # nationality - the opposite direction from the sibling VireakBuntham integration's
10
+ # SyncNationalityPricing, which stores local as the base and surcharges foreigners. That
11
+ # direction is riskier here: real storefront checkout (spree/api/v2/storefront/guests_controller.rb)
12
+ # only ever permits `nationality_id`, never `nationality_group`, so any guest whose nationality
13
+ # can't be positively matched must fall back to today's already-approved default (non_local),
14
+ # not silently receive the cheaper local price.
15
+ #
16
+ # Uses PricingRules::Nationality (not NationalityGroup, which VET uses) for the same reason -
17
+ # Nationality matches on nationality_id, which real guests actually have; NationalityGroup
18
+ # requires nationality_group, which real storefront checkout never sets.
19
+ #
20
+ # One PricingModel per vendor; one PricingRuleGroup per route (so each route gets its own
21
+ # independent FlatRate calculator) - confirmed per-route granularity with the user: if two trips
22
+ # on the same route ever have genuinely different local/non-local differentials, the most
23
+ # recently synced one wins for the whole route.
24
+ #
25
+ # Skipped when there's no real differential to encode (either price missing, or they're equal -
26
+ # both are always present per BookMeBus's own price_by_nationalities, so equality is the
27
+ # meaningful no-op signal, not blankness).
28
+ class SyncNationalityPricing
29
+ PRICING_MODEL_NAME = 'BookMeBus Nationality Pricing'.freeze
30
+
31
+ def initialize(route:, vendor:, local_price:, non_local_price:)
32
+ @route = route
33
+ @vendor = vendor
34
+ @local_price = local_price&.to_f
35
+ @non_local_price = non_local_price&.to_f
36
+ end
37
+
38
+ def call
39
+ return if @local_price.nil? || @non_local_price.nil? || @local_price == @non_local_price
40
+ return if @route.blank? || @vendor.blank?
41
+
42
+ local_taxon = @vendor.service_origin
43
+ return if local_taxon.blank?
44
+
45
+ delta = @local_price - @non_local_price
46
+
47
+ model = find_or_create_pricing_model
48
+ group = find_or_create_rule_group(model, local_taxon)
49
+ action = find_or_create_action(group, delta)
50
+
51
+ action.calculator.update!(preferred_amount: delta)
52
+ find_or_create_model_route(model)
53
+ end
54
+
55
+ private
56
+
57
+ def find_or_create_pricing_model
58
+ @vendor.pricing_models.find_or_create_by!(name: PRICING_MODEL_NAME) do |m|
59
+ m.status = :active
60
+ end
61
+ end
62
+
63
+ def find_or_create_rule_group(model, local_taxon)
64
+ group_name = "Local (#{@route.route_name})"
65
+ model.pricing_rule_groups.find_or_create_by!(name: group_name) do |g|
66
+ g.status = :active
67
+ g.private_metadata = { match_type: 'any' }
68
+ g.pricing_rules.build(
69
+ type: 'SpreeCmCommissioner::PricingRules::Nationality',
70
+ private_metadata: { nationalities: [local_taxon.id], rule_type: 'any' }
71
+ )
72
+ end
73
+ end
74
+
75
+ def find_or_create_action(group, delta)
76
+ group.pricing_action || group.create_pricing_action!(
77
+ type: 'SpreeCmCommissioner::PricingActions::CreateGuestAdjustments',
78
+ calculator: Spree::Calculator::FlatRate.new(
79
+ preferred_amount: delta,
80
+ preferred_currency: Spree::Store.default.default_currency
81
+ )
82
+ )
83
+ end
84
+
85
+ def find_or_create_model_route(model)
86
+ model.pricing_model_routes.find_or_create_by!(
87
+ route: @route,
88
+ origin_place: @route.origin_place,
89
+ destination_place: @route.destination_place
90
+ )
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,70 @@
1
+ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
2
+ # SyncProduct handles product synchronization for BookMeBus trips
3
+ #
4
+ # This service is responsible for:
5
+ # - Creating or updating Product records from external trip data
6
+ # - Managing product attributes (name, status, pricing)
7
+ # - Handling integration mappings for products
8
+ #
9
+ # Usage:
10
+ # sync_service = SyncProduct.new(integration: integration, sync_result: sync_result)
11
+ # product = sync_service.call(external_trip: external_trip, price: 10.0)
12
+ #
13
+ class SyncProduct
14
+ def initialize(integration:, sync_result:)
15
+ @integration = integration
16
+ @sync_result = sync_result
17
+ @vendor = @integration.vendor
18
+ end
19
+
20
+ def call(external_trip:, price:)
21
+ external_id = "product-#{external_trip.trip_id}"
22
+
23
+ mapping = Spree::Product.find_or_initialize_integration_mapping(
24
+ integration_id: @integration.id,
25
+ external_id: external_id
26
+ )
27
+
28
+ product = mapping.internal
29
+
30
+ @sync_result.track(:product, product) do |tracker|
31
+ product.assign_attributes(
32
+ name: build_product_name(external_trip),
33
+ status: :active,
34
+ vendor: @vendor,
35
+ product_type: :transit,
36
+ available_on: Time.current,
37
+ shipping_category: default_shipping_category,
38
+ stores: [Spree::Store.default],
39
+ option_types: [seat_type_option].compact,
40
+ price: price
41
+ )
42
+ tracker.save_if_changed!(product)
43
+ end
44
+
45
+ mapping.mark_as_active!(external_payload: { trip_id: external_trip.trip_id })
46
+ product
47
+ end
48
+
49
+ private
50
+
51
+ def build_product_name(external_trip)
52
+ origin_name = external_trip.origin_location_name
53
+ destination_name = external_trip.destination_location_name
54
+ "#{origin_name} → #{destination_name}"
55
+ end
56
+
57
+ def seat_type_option
58
+ # find_or_create_by! (not find_by) - matches SyncVariant's own lookup for the same OptionType,
59
+ # so the very first sync in a fresh environment doesn't create a Product with no option_types
60
+ # just because SyncProduct happened to run before the type existed.
61
+ @seat_type_option ||= Spree::OptionType.find_or_create_by!(name: 'seat-type') do |ot|
62
+ ot.presentation = 'Seat Type'
63
+ end
64
+ end
65
+
66
+ def default_shipping_category
67
+ @default_shipping_category ||= Spree::ShippingCategory.first_or_create!(name: 'Default')
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,121 @@
1
+ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
2
+ # SyncRouteStops synchronizes route stops from BookMeBus trips
3
+ #
4
+ # Structure follows SyncZones pattern:
5
+ # - initialize: Setup integration and sync_result
6
+ # - call: Main entry point, validates inputs, orchestrates sync
7
+ # - private methods: Implementation details
8
+ class SyncRouteStops
9
+ def initialize(integration:, sync_result:, route:)
10
+ @integration = integration
11
+ @sync_result = sync_result
12
+ @route = route
13
+ @location_mappings = {}
14
+ end
15
+
16
+ def call(external_trips:)
17
+ raise SpreeCmCommissioner::Integrations::SyncError, 'external_trips is required' if external_trips.blank?
18
+ raise SpreeCmCommissioner::Integrations::SyncError, 'route is required' if @route.blank?
19
+
20
+ preload_location_mappings!
21
+ sync_route_stops!(external_trips)
22
+ end
23
+
24
+ private
25
+
26
+ # Preload all location mappings to avoid N+1 queries
27
+ def preload_location_mappings!
28
+ @location_mappings = SpreeCmCommissioner::IntegrationMapping
29
+ .where(
30
+ integration_id: @integration.id,
31
+ internal_type: 'SpreeCmCommissioner::VendorPlace',
32
+ status: :active
33
+ )
34
+ .includes(:internal)
35
+ .index_by(&:external_id)
36
+ end
37
+
38
+ def sync_route_stops!(external_trips)
39
+ return unless @route && external_trips.any?
40
+
41
+ route_stops = build_route_stops_from(external_trips)
42
+ return if route_stops.empty?
43
+
44
+ update_route_stops(route_stops)
45
+ end
46
+
47
+ def build_route_stops_from(external_trips)
48
+ external_trips.flat_map do |trip|
49
+ next [] unless trip.stop_times.is_a?(Array)
50
+
51
+ trip.stop_times.each_with_index.filter_map do |stop_time, index|
52
+ build_route_stop_from(stop_time, index + 1)
53
+ end
54
+ end
55
+ end
56
+
57
+ def build_route_stop_from(stop_time, sequence)
58
+ stop_data = stop_time['stop']
59
+ location_data = stop_time['location']
60
+ return nil unless stop_data && location_data
61
+
62
+ external_stop = SpreeCmCommissioner::Integrations::BookMeBusV1::Resources::Stop.new(stop_data.merge('location' => location_data))
63
+ vendor_place = find_vendor_place_for(external_stop.unique_external_id)
64
+ location_vendor_place = find_vendor_place_for(location_data['id'])
65
+ return nil unless vendor_place && location_vendor_place
66
+
67
+ # Use actual sequence from stop_times order to preserve trip structure
68
+ {
69
+ vendor_place_id: vendor_place.id,
70
+ location_id: location_vendor_place.id,
71
+ type: external_stop.branch? ? :branch : :stop,
72
+ sequence: sequence
73
+ }
74
+ end
75
+
76
+ def update_route_stops(new_stops, retried: false)
77
+ @sync_result.track(:route, @route) do |tracker|
78
+ # Merge new stops with existing ones, preserving existing stops not in new data
79
+ existing = @route.route_stops.to_h.index_by { |s| s[:vendor_place_id] || s['vendor_place_id'] }
80
+ new_stops.each { |stop| existing[stop[:vendor_place_id]] = stop }
81
+
82
+ # Sort by sequence to maintain trip order
83
+ sorted_stops = existing.values.sort_by { |s| s[:sequence] || 0 }
84
+
85
+ # Re-sequence to ensure continuous sequence from 0
86
+ sorted_stops.each_with_index { |stop, idx| stop[:sequence] = idx }
87
+
88
+ # Validate route_stops before assignment
89
+ route_stop_collection = SpreeCmCommissioner::Transit::RouteStopCollection.from_hash(sorted_stops)
90
+ unless route_stop_collection.valid?
91
+ error_message = route_stop_collection.errors.join(', ')
92
+ raise SpreeCmCommissioner::Integrations::SyncError, "Invalid route stops: #{error_message}"
93
+ end
94
+
95
+ @route.route_stops = sorted_stops if @route.route_stops.to_h != sorted_stops
96
+ tracker.save_if_changed!(@route)
97
+ end
98
+
99
+ CmAppLogger.log(
100
+ label: 'SyncRouteStops synced',
101
+ data: { route_id: @route.id, stops_count: new_stops.count }
102
+ )
103
+ rescue ActiveRecord::StaleObjectError
104
+ # A concurrent sync (full + incremental overlapping) updated this route's lock_version
105
+ # between our read and write. Reload to pick up the latest route_stops and merge again,
106
+ # once, rather than losing this sync's stops or aborting the whole run outright.
107
+ raise if retried
108
+
109
+ @route.reload
110
+ update_route_stops(new_stops, retried: true)
111
+ end
112
+
113
+ def find_vendor_place_for(external_id)
114
+ return nil unless external_id
115
+
116
+ # Use preloaded mappings to avoid N+1 queries
117
+ mapping = @location_mappings[external_id.to_s]
118
+ mapping&.internal
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,128 @@
1
+ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
2
+ class SyncRoutes
3
+ # Initialize SyncRoutes service
4
+ # @param integration [SpreeCmCommissioner::Integrations::BookMeBusV1] The integration instance
5
+ # @param sync_result [SpreeCmCommissioner::Integrations::Base::SyncResult] Optional sync result tracker
6
+ def initialize(integration:, sync_result: nil)
7
+ @integration = integration
8
+ @sync_result = sync_result || SpreeCmCommissioner::Integrations::Base::SyncResult.new
9
+ end
10
+
11
+ def call
12
+ raise SpreeCmCommissioner::Integrations::SyncError, 'Integration must have a vendor to sync routes' if @integration.vendor.blank?
13
+
14
+ synced_route_mappings = sync_items!
15
+
16
+ cleanup_stale_mappings!(synced_route_mappings)
17
+ end
18
+
19
+ private
20
+
21
+ # Generate routes from location pairs and sync them
22
+ def sync_items!
23
+ location_pairs = build_location_pairs
24
+ return [] if location_pairs.empty?
25
+
26
+ location_pairs.map do |origin_mapping, destination_mapping|
27
+ sync_item!(origin_mapping, destination_mapping)
28
+ end.compact
29
+ end
30
+
31
+ # Sync a single route from location pair
32
+ def sync_item!(origin_mapping, destination_mapping)
33
+ # Extract places from the location mappings
34
+ origin_place = origin_mapping.internal&.place
35
+ destination_place = destination_mapping.internal&.place
36
+
37
+ # Skip if either place is missing (invalid location mapping)
38
+ return nil unless origin_place && destination_place
39
+
40
+ # Build unique external_id from mapping external_ids
41
+ external_id = build_route_external_id(origin_mapping, destination_mapping)
42
+
43
+ # Find or create integration mapping for this route
44
+ mapping = SpreeCmCommissioner::Route.find_or_initialize_integration_mapping(
45
+ integration_id: @integration.id,
46
+ external_id: external_id
47
+ )
48
+
49
+ # Get route from mapping (already created by find_or_initialize_integration_mapping)
50
+ route = mapping.internal
51
+ route.vendor = @integration.vendor if route.new_record?
52
+
53
+ # Update route attributes and save if changed
54
+ @sync_result.track(:route, route) do |tracker|
55
+ route.origin_place = origin_place
56
+ route.destination_place = destination_place
57
+ route.route_name = build_route_name(origin_place, destination_place)
58
+ route.short_name = build_short_name(origin_place, destination_place)
59
+ route.route_type ||= :bus
60
+ tracker.save_if_changed!(route)
61
+ end
62
+
63
+ mapping.mark_as_active!(
64
+ external_payload: {
65
+ origin_place_id: origin_mapping.external_id,
66
+ destination_place_id: destination_mapping.external_id,
67
+ origin_name: origin_mapping.external_payload&.dig('name'),
68
+ destination_name: destination_mapping.external_payload&.dig('name')
69
+ }
70
+ )
71
+
72
+ mapping
73
+ end
74
+
75
+ # Build external_id using mapping external_ids (stable across environments)
76
+ def build_route_external_id(origin_mapping, destination_mapping)
77
+ "route-#{origin_mapping.external_id}-#{destination_mapping.external_id}"
78
+ end
79
+
80
+ # Build all location pairs (A->B and B->A)
81
+ def build_location_pairs
82
+ location_mappings = SpreeCmCommissioner::IntegrationMapping
83
+ .includes(internal: :place)
84
+ .where(
85
+ integration_id: @integration.id,
86
+ internal_type: 'SpreeCmCommissioner::VendorPlace',
87
+ status: :active
88
+ )
89
+ .to_a
90
+ pairs = []
91
+ location_mappings.combination(2).each do |origin, destination|
92
+ pairs << [origin, destination]
93
+ pairs << [destination, origin]
94
+ end
95
+
96
+ pairs
97
+ end
98
+
99
+ # Archive stale route mappings that are no longer in the generated pairs
100
+ def cleanup_stale_mappings!(synced_route_mappings)
101
+ synced_ids = synced_route_mappings.to_set(&:external_id)
102
+ active_mappings = SpreeCmCommissioner::IntegrationMapping.where(
103
+ integration_id: @integration.id,
104
+ internal_type: 'SpreeCmCommissioner::Route',
105
+ status: :active
106
+ )
107
+
108
+ active_mappings.find_each do |mapping|
109
+ next if synced_ids.include?(mapping.external_id)
110
+
111
+ mapping.mark_as_archived!
112
+ @sync_result.increment_metric(:route, :archived)
113
+ end
114
+ end
115
+
116
+ def build_route_name(origin_place, destination_place)
117
+ "#{origin_place.name} - #{destination_place.name}"
118
+ end
119
+
120
+ def build_short_name(origin_place, destination_place)
121
+ "#{place_code(origin_place)}-#{place_code(destination_place)}"
122
+ end
123
+
124
+ def place_code(place)
125
+ place.code.presence || place.id.to_s
126
+ end
127
+ end
128
+ end