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,66 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ module Transit
6
+ class ItemAvailabilitiesController < ::Spree::Api::V2::ResourceController
7
+ include SpreeCmCommissioner::Integrations::InventorySyncCachable
8
+
9
+ def create
10
+ sync_result = sync_item_availability!
11
+
12
+ unless sync_result.success?
13
+ error_message = sync_result.to_h.dig(:error, :message) || 'Sync failed'
14
+ return render_error_payload(error_message, 422)
15
+ end
16
+
17
+ item_availability = SpreeCmCommissioner::ItemAvailabilitySync.new(
18
+ id: trip.id,
19
+ last_sync_at: sync_result.last_synced_at
20
+ )
21
+ render_serialized_payload { serialize_resource(item_availability) }
22
+ rescue ActiveRecord::RecordNotFound => e
23
+ render_error_payload(e.message, 404)
24
+ end
25
+
26
+ private
27
+
28
+ def trip
29
+ @trip ||= SpreeCmCommissioner::Trip.find(params[:trip_id])
30
+ end
31
+
32
+ def integration
33
+ @integration ||= trip.vendor&.integration
34
+ end
35
+
36
+ def sync_item_availability!
37
+ # Return success result if vendor has no integration
38
+ return SpreeCmCommissioner::Integrations::Base::SyncResult.new if integration.nil?
39
+
40
+ # Check cache first for successful results only
41
+ cached_result = Rails.cache.read(sync_cache_key)
42
+ return cached_result if cached_result
43
+
44
+ # Perform sync
45
+ result = integration.sync_item_availability!(trip_id: trip.id, on_date: params[:on_date])
46
+
47
+ # Only cache successful results to avoid sticky failures
48
+ Rails.cache.write(sync_cache_key, result, expires_in: sync_cache_ttl) if result.success?
49
+
50
+ result
51
+ end
52
+
53
+ def sync_cache_key
54
+ # Include trip.updated_at to auto-bust cache when trip is updated by background syncs
55
+ "item_availability_syncs:#{trip.id}:#{params[:on_date]}:#{trip.updated_at.to_i}"
56
+ end
57
+
58
+ def resource_serializer
59
+ SpreeCmCommissioner::V2::Storefront::ItemAvailabilitySyncSerializer
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -26,6 +26,17 @@ module Spree
26
26
  render_order(result)
27
27
  end
28
28
 
29
+ # Like `advance`, but stops at `payment` (see SpreeCmCommissioner::Checkout::AdvanceToPayment).
30
+ def advance_to_payment
31
+ return render_serialized_payload { serialized_current_order } if spree_current_order.complete?
32
+
33
+ spree_authorize! :update, spree_current_order, order_token
34
+
35
+ result = SpreeCmCommissioner::Checkout::AdvanceToPayment.call(order: spree_current_order)
36
+
37
+ render_order(result)
38
+ end
39
+
29
40
  def complete
30
41
  return render_serialized_payload { serialized_current_order } if spree_current_order.confirm? || spree_current_order.complete?
31
42
 
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ class VendorPhotosController < BaseController
6
+ def index
7
+ collection = scope.page(params[:page]).per(params[:per_page])
8
+
9
+ render_serialized_payload do
10
+ serialize_collection(collection)
11
+ end
12
+ end
13
+
14
+ def scope
15
+ current_vendor.photos.includes(attachment_attachment: :blob)
16
+ end
17
+
18
+ def collection_serializer
19
+ Spree::V2::Tenant::AssetSerializer
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -10,7 +10,7 @@ module SpreeCmCommissioner
10
10
  close_adjustments cart channel set_channel
11
11
  accept_all reject_all alert_request_to_vendor
12
12
  notifications fire_notification queue_webhooks_requests update_order_status
13
- update_internal_note
13
+ update_internal_note line_items
14
14
  ]
15
15
 
16
16
  base.before_action :initialize_notification_methods, only: %i[
@@ -82,6 +82,14 @@ module SpreeCmCommissioner
82
82
 
83
83
  def notifications; end
84
84
 
85
+ # Rendered lazily into a turbo_frame from the orders index (see line_items.html.erb),
86
+ # since eager-loading every order's line items on the index would be wasteful.
87
+ def line_items
88
+ @line_items = @order.line_items.includes(:event, variant: :product)
89
+
90
+ render layout: false
91
+ end
92
+
85
93
  def update_internal_note
86
94
  internal_note = params.dig(:order, :internal_note).to_s.strip.presence
87
95
  @order.update_columns(internal_note: internal_note, updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
@@ -5,6 +5,9 @@ module SpreeCmCommissioner
5
5
  module CheckoutControllerDecorator
6
6
  def self.prepended(base)
7
7
  base.include SpreeCmCommissioner::OrderConcern
8
+ base.include SpreeCmCommissioner::TurnstileProtectable
9
+
10
+ base.before_action -> { verify_turnstile! }, only: %i[create_payment]
8
11
  # Sets Current.load_test_bypass from the X-Request-Verify secret so
9
12
  # InventoryHolds::ValidateLimits can skip anti-abuse checks for trusted load tests.
10
13
  base.include SpreeCmCommissioner::LoadTestBypassable
@@ -20,6 +23,18 @@ module SpreeCmCommissioner
20
23
 
21
24
  super
22
25
  end
26
+
27
+ # Like `advance`, but stops at `payment` (see SpreeCmCommissioner::Checkout::AdvanceToPayment) so the
28
+ # client can attach a payment before confirming.
29
+ def advance_to_payment
30
+ return render_serialized_payload { serialized_current_order } if spree_current_order.complete?
31
+
32
+ spree_authorize! :update, spree_current_order, order_token
33
+
34
+ result = SpreeCmCommissioner::Checkout::AdvanceToPayment.call(order: spree_current_order)
35
+
36
+ render_order(result)
37
+ end
23
38
  end
24
39
  end
25
40
  end
@@ -200,10 +200,14 @@ module SpreeCmCommissioner
200
200
  grouped.map { |nationality_group, guests| customer_type_summary(nationality_group, guests) }.join(', ')
201
201
  end
202
202
 
203
- # e.g. "Local (Adult x1, Child x1)". age_group is NOT NULL, so it is always present.
203
+ # e.g. "Local (Adult x1, Child x1)". `cm_saved_guests.age_group` is declared `null: false` in
204
+ # the migration, but a dev/staging DB whose column predates that migration (added via
205
+ # `add_column ... if_not_exists: true`, which silently skips the constraint if the column
206
+ # already exists) can still carry real NULL rows - group those under "Unknown" rather than
207
+ # crashing the whole order-completion Telegram notification on nil.humanize.
204
208
  def customer_type_summary(nationality_group, guests)
205
209
  age_breakdown = guests
206
- .group_by { |sg| sg.age_group.humanize }
210
+ .group_by { |sg| sg.age_group&.humanize || 'Unknown' }
207
211
  .map { |age_group, age_guests| "#{age_group} x#{age_guests.count}" }
208
212
  .join(', ')
209
213
 
@@ -0,0 +1,57 @@
1
+ # Builds the audience description shown in the "Send to All" confirmation modal.
2
+ # This help to clarify who will receive the notification based on the notification's taxons and vendor.
3
+ # 1. No vendor and no taxons: sent to all BookMe+ users, excluding tenant users
4
+ # 2. Has taxons: sent to users who bought a ticket for those specific events
5
+ # 3. Has vendor, vendor has no tenant: sent to users who bought a ticket for any of the vendor's events
6
+ # 4. Has vendor, vendor has a tenant: sent to all users in that tenant
7
+
8
+ module Spree
9
+ module Admin
10
+ module CustomerNotificationsHelper
11
+ def send_all_description(notification)
12
+ if notification.taxons.any?
13
+ send_all_taxons_description(notification)
14
+ elsif notification.vendor.present?
15
+ send_all_vendor_description(notification.vendor)
16
+ else
17
+ content_tag(:p, 'This notification will be sent to all BookMe+ users, excluding tenant users.')
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def send_all_taxons_description(notification)
24
+ content_tag(:div) do
25
+ concat('This notification will be sent to everyone who bought the ticket from these events:')
26
+ concat(
27
+ content_tag(:ul) do
28
+ safe_join(notification.taxons.map { |taxon| content_tag(:li, taxon_link(taxon)) })
29
+ end
30
+ )
31
+ end
32
+ end
33
+
34
+ def send_all_vendor_description(vendor)
35
+ content_tag(:p) do
36
+ if vendor.tenant.present?
37
+ safe_join(['This notification will be sent to all users in Tenant ', tenant_link(vendor.tenant), '.'])
38
+ else
39
+ safe_join(['This notification will be sent to everyone who bought a ticket for any event from vendor ', vendor_link(vendor), '.'])
40
+ end
41
+ end
42
+ end
43
+
44
+ def taxon_link(taxon)
45
+ link_to(taxon.name, edit_admin_taxonomy_taxon_path(taxon.taxonomy_id, taxon.id))
46
+ end
47
+
48
+ def vendor_link(vendor)
49
+ link_to(vendor.name, edit_admin_vendor_path(vendor.slug))
50
+ end
51
+
52
+ def tenant_link(tenant)
53
+ link_to(tenant.name, edit_admin_tenant_path(tenant.slug))
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,35 +1,57 @@
1
1
  module SpreeCmCommissioner
2
2
  module Admin
3
3
  module HomepageSegmentHelper
4
+ # Bootstrap has only eight badge colors, so later segments share one; the label tells them apart.
5
+ SEGMENT_BADGE_COLORS = {
6
+ general: 'primary',
7
+ ticket: 'secondary',
8
+ tour: 'info',
9
+ accommodation: 'warning',
10
+ things_to_do: 'success',
11
+ football: 'dark',
12
+ show: 'danger',
13
+ bus: 'light',
14
+ intercity_taxi: 'warning',
15
+ rails: 'secondary',
16
+ ferry: 'info'
17
+ }.freeze
18
+
4
19
  def badge_class_for_segment(segment_name)
5
- case segment_name.to_sym
6
- when :general
7
- 'badge badge-primary text-uppercase'
8
- when :ticket
9
- 'badge badge-secondary text-uppercase'
10
- when :tour
11
- 'badge badge-info text-uppercase'
12
- when :accommodation
13
- 'badge badge-warning text-uppercase'
14
- when :things_to_do
15
- 'badge badge-success text-uppercase'
16
- when :football
17
- 'badge badge-dark text-uppercase'
18
- when :show
19
- 'badge badge-danger text-uppercase'
20
- else
21
- 'badge'
22
- end
20
+ color = SEGMENT_BADGE_COLORS[segment_name.to_sym]
21
+ color ? "badge badge-#{color} text-uppercase" : 'badge'
22
+ end
23
+
24
+ def badge_class_for_mini_app(_mini_app_name)
25
+ 'badge badge-info text-uppercase'
26
+ end
27
+
28
+ def mini_app_label(mini_app_name)
29
+ mini_app_name.to_s.titleize
30
+ end
31
+
32
+ # `none` is any value outside BIT_MINI_APP, which filter_by_mini_app reads as untagged.
33
+ def mini_app_filter_options
34
+ [['No mini app', 'none']] +
35
+ SpreeCmCommissioner::HomepageSectionBitwise::BIT_MINI_APP.keys.map { |name| [mini_app_label(name), name] }
23
36
  end
24
37
 
25
38
  def calculate_segment_value(params)
26
- segment_params = params.slice(*SpreeCmCommissioner::HomepageSectionBitwise::BIT_SEGMENT.keys)
39
+ calculate_bitwise_value(params, SpreeCmCommissioner::HomepageSectionBitwise::BIT_SEGMENT)
40
+ end
41
+
42
+ def calculate_mini_app_value(params)
43
+ calculate_bitwise_value(params, SpreeCmCommissioner::HomepageSectionBitwise::BIT_MINI_APP)
44
+ end
45
+
46
+ private
27
47
 
28
- return nil unless segment_params.values.any?
48
+ # Sums the checked boxes into one integer. Keyed by name, not position, so a checkbox the
49
+ # form omits cannot shift the rest onto the wrong bits. Returns 0, never nil: `value & bit`
50
+ # raises on NULL.
51
+ def calculate_bitwise_value(params, bit_map)
52
+ return 0 if params.blank?
29
53
 
30
- segment_params.values.each_with_index.sum do |value, index|
31
- value.to_i * (2**index)
32
- end
54
+ bit_map.sum { |name, bit_value| ActiveModel::Type::Boolean.new.cast(params[name]) ? bit_value : 0 }
33
55
  end
34
56
  end
35
57
  end
@@ -54,21 +54,30 @@ module SpreeCmCommissioner
54
54
  .where(cm_notifications: { id: nil })
55
55
  end
56
56
 
57
+ # Resolves the base user scope for the notification: by its taxons, by its vendor's own
58
+ # taxons (when the vendor has no tenant), or by tenant otherwise
57
59
  def end_users
58
- users_scope = Spree.user_class
59
- .where(tenant_id: notification_tenant_id)
60
-
61
60
  if customer_notification.notification_taxons.exists?
62
61
  taxon_ids = customer_notification.notification_taxons.pluck(:taxon_id)
63
- users_scope = SpreeCmCommissioner::UsersByEventFetcherQuery
64
- .new(taxon_ids)
65
- .call
66
- .end_users_push_notificable
67
- .where(tenant_id: notification_tenant_id)
62
+ users_by_taxon_ids(taxon_ids)
63
+ elsif notification_tenant_id.nil? && customer_notification.vendor.present?
64
+ taxon_ids = customer_notification.vendor.taxons.ids
65
+ users_by_taxon_ids(taxon_ids)
68
66
  else
69
- users_scope = users_scope.end_users_push_notificable
67
+ Spree.user_class
68
+ .where(tenant_id: notification_tenant_id)
69
+ .end_users_push_notificable
70
70
  end
71
+ end
71
72
 
73
+ # Fetches push-notificable users who purchased under the given taxon(s), scoped to the
74
+ # notification's tenant when it has one
75
+ def users_by_taxon_ids(taxon_ids)
76
+ users_scope = SpreeCmCommissioner::UsersByEventFetcherQuery
77
+ .new(event_id: taxon_ids)
78
+ .call
79
+ .end_users_push_notificable
80
+ users_scope = users_scope.where(tenant_id: notification_tenant_id) if notification_tenant_id.present?
72
81
  users_scope
73
82
  end
74
83
 
@@ -0,0 +1,17 @@
1
+ # Resolves a user from a cross-app handoff JWT (see CrossAppHandoffTokens::Decode) instead of a
2
+ # username/password pair. Routed here by UserAuthenticator when the grant_type=password request
3
+ # carries a `handoff_token` param — used by the operator app to sign a user in after the customer
4
+ # app minted the token on their behalf (purpose: operator_sign_in).
5
+ module SpreeCmCommissioner
6
+ class UserCrossAppHandoffAuthenticator < BaseInteractor
7
+ delegate :handoff_token, to: :context
8
+
9
+ def call
10
+ result = SpreeCmCommissioner::CrossAppHandoffTokens::Decode.call(token: handoff_token, purpose: 'operator_sign_in')
11
+ context.fail!(message: I18n.t('authenticator.incorrect_login')) unless result.success?
12
+
13
+ context.user = result.value[:user]
14
+ reject_unauthenticatable_user!(context.user)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ module SpreeCmCommissioner
2
+ class AuditEventJob < ApplicationUniqueJob
3
+ def perform(options = {})
4
+ SpreeCmCommissioner::AuditEvent.create!(
5
+ event_type: options[:event_type],
6
+ success: options.fetch(:success, true),
7
+ auditable: options[:auditable],
8
+ user: options[:user],
9
+ correlation_id: options[:correlation_id],
10
+ attempt: options.fetch(:attempt, 0),
11
+ metadata: options.fetch(:metadata, {}),
12
+ retention_until: options[:retention_until]
13
+ )
14
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
15
+ # Log validation errors but don't retry - these are data issues, not transient failures
16
+ CmAppLogger.log(
17
+ label: 'AuditEventJob validation failed - non-retryable',
18
+ data: {
19
+ event_type: options[:event_type],
20
+ error_class: e.class.name,
21
+ error_message: e.message,
22
+ validation_errors: e.record&.errors&.full_messages
23
+ }
24
+ )
25
+ # Return nil to indicate the job completed (but audit wasn't saved)
26
+ nil
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,139 @@
1
+ module SpreeCmCommissioner
2
+ # AuditWrapper is a reusable mixin for classes that make external API calls.
3
+ #
4
+ # It wraps an API call in a three-event lifecycle:
5
+ # 1. <event_base>.request – logged before the call
6
+ # 2. <event_base>.response – logged after a successful call (includes timing)
7
+ # 3. <event_base>.failed – logged when the block raises, then re-raises
8
+ #
9
+ # Metadata follows this canonical shape:
10
+ # {
11
+ # request: { method:, endpoint:, body:, params: },
12
+ # response: { status:, body: },
13
+ # timing: { duration_ms: },
14
+ # error: nil | { message: }
15
+ # }
16
+ #
17
+ # Usage:
18
+ # include SpreeCmCommissioner::AuditWrapper
19
+ #
20
+ # with_audit(
21
+ # event_base: 'bookmebus.order.hold',
22
+ # request: { method: 'POST', endpoint: '/api/v2/trip_reservations', body: body },
23
+ # auditable: order,
24
+ # user: current_user
25
+ # ) do
26
+ # post("#{BASE_PATH}/trip_reservations", body)
27
+ # end
28
+
29
+ module AuditWrapper
30
+ extend ActiveSupport::Concern
31
+
32
+ def with_audit(event_base:, request: {}, auditable: nil, user: nil, # rubocop:disable Metrics/ParameterLists
33
+ attempt: 0, retention_days: nil
34
+ )
35
+ correlation_id = SecureRandom.uuid
36
+
37
+ shared = {
38
+ auditable: auditable,
39
+ user: user,
40
+ correlation_id: correlation_id,
41
+ attempt: attempt,
42
+ retention_days: retention_days
43
+ }
44
+
45
+ # 1. Log the outbound request
46
+ AuditLogger.log!(
47
+ **shared,
48
+ event_type: "#{event_base}.request",
49
+ success: true,
50
+ metadata: build_request_metadata(request)
51
+ )
52
+
53
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
+ result = yield
55
+ duration = elapsed_ms(started_at)
56
+
57
+ # 2. Log the successful response
58
+ AuditLogger.log!(
59
+ **shared,
60
+ event_type: "#{event_base}.response",
61
+ success: true,
62
+ metadata: build_response_metadata(request, result, duration)
63
+ )
64
+
65
+ result
66
+ rescue StandardError => e
67
+ duration = elapsed_ms(started_at || Process.clock_gettime(Process::CLOCK_MONOTONIC))
68
+
69
+ # 3. Log the failure event
70
+ AuditLogger.log!(
71
+ **shared,
72
+ event_type: "#{event_base}.failed",
73
+ success: false,
74
+ metadata: build_error_metadata(request, e, duration)
75
+ )
76
+
77
+ raise
78
+ end
79
+
80
+ private
81
+
82
+ def build_request_metadata(request)
83
+ {
84
+ request: normalize_request(request),
85
+ response: nil,
86
+ timing: nil,
87
+ error: nil
88
+ }
89
+ end
90
+
91
+ def build_response_metadata(request, result, duration_ms)
92
+ status, body = extract_response(result)
93
+
94
+ {
95
+ request: normalize_request(request),
96
+ response: { status: status, body: body },
97
+ timing: { duration_ms: duration_ms },
98
+ error: nil
99
+ }
100
+ end
101
+
102
+ def build_error_metadata(request, error, duration_ms)
103
+ {
104
+ request: normalize_request(request),
105
+ response: nil,
106
+ timing: { duration_ms: duration_ms },
107
+ error: { message: error.message }
108
+ }
109
+ end
110
+
111
+ def normalize_request(request)
112
+ {
113
+ method: request[:method],
114
+ endpoint: request[:endpoint],
115
+ body: request[:body],
116
+ params: request[:params]
117
+ }
118
+ end
119
+
120
+ # Try to extract status + body from common response objects.
121
+ # Falls back to inspecting the raw result.
122
+ def extract_response(result)
123
+ if result.respond_to?(:status) && result.respond_to?(:body)
124
+ [result.status, result.body]
125
+ elsif result.is_a?(Array)
126
+ # Handle arrays of resource objects
127
+ [nil, result.map(&:to_h)]
128
+ elsif result.respond_to?(:to_h)
129
+ [nil, result.to_h]
130
+ else
131
+ [nil, result.inspect]
132
+ end
133
+ end
134
+
135
+ def elapsed_ms(started_at)
136
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
137
+ end
138
+ end
139
+ end
@@ -2,6 +2,8 @@ module SpreeCmCommissioner
2
2
  module HomepageSectionBitwise
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ # Append only: [SpreeCmCommissioner::HomepageBackground] maps these keys to an integer enum by
6
+ # index, so inserting one renumbers every stored background.
5
7
  BIT_SEGMENT = {
6
8
  general: 0b00001,
7
9
  ticket: 0b00010,
@@ -9,7 +11,25 @@ module SpreeCmCommissioner
9
11
  accommodation: 0b01000,
10
12
  things_to_do: 0b10000,
11
13
  football: 0b100000,
12
- show: 0b1000000
14
+ show: 0b1000000,
15
+ bus: 0b10000000,
16
+ intercity_taxi: 0b100000000,
17
+ rails: 0b1000000000,
18
+ ferry: 0b10000000000
19
+ }.freeze
20
+
21
+ # Keys match the `mini_app` query param the app sends (ApiConstant.miniApp.paramValue).
22
+ BIT_MINI_APP = {
23
+ vattanac: 0b001,
24
+ acleda: 0b010,
25
+ wing: 0b100
26
+ }.freeze
27
+
28
+ # The feed each mini app's home requests, mirroring `isWing ? BusRoute : HomeRoute` in routes.dart.
29
+ MINI_APP_HOME_SEGMENT = {
30
+ vattanac: :general,
31
+ acleda: :general,
32
+ wing: :bus
13
33
  }.freeze
14
34
 
15
35
  BIT_SEGMENT.each do |segment, bit_value|
@@ -18,6 +38,12 @@ module SpreeCmCommissioner
18
38
  end
19
39
  end
20
40
 
41
+ BIT_MINI_APP.each do |mini_app, bit_value|
42
+ define_method "#{mini_app}?" do
43
+ mini_app_enabled?(bit_value)
44
+ end
45
+ end
46
+
21
47
  def segments
22
48
  BIT_SEGMENT.filter_map do |segment_value, bit_value|
23
49
  segment_value if segment_enabled?(bit_value)
@@ -27,5 +53,15 @@ module SpreeCmCommissioner
27
53
  def segment_enabled?(bit_value)
28
54
  segment & bit_value != 0
29
55
  end
56
+
57
+ def mini_apps
58
+ BIT_MINI_APP.filter_map do |mini_app_value, bit_value|
59
+ mini_app_value if mini_app_enabled?(bit_value)
60
+ end
61
+ end
62
+
63
+ def mini_app_enabled?(bit_value)
64
+ mini_app.to_i & bit_value != 0
65
+ end
30
66
  end
31
67
  end
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ module Integrations
3
+ module InventorySyncCachable
4
+ def sync_cache_ttl
5
+ ENV.fetch('SYNC_CACHE_TTL', '300').to_i.seconds
6
+ end
7
+ end
8
+ end
9
+ end
@@ -66,11 +66,11 @@ module SpreeCmCommissioner
66
66
  end
67
67
 
68
68
  def redeemed?
69
- redeemed_at.present?
69
+ open_dated? && redeemed_at.present?
70
70
  end
71
71
 
72
72
  def expired?
73
- valid_until.present? && valid_until.to_date < Date.current
73
+ open_dated? && valid_until.present? && valid_until.to_date < Date.current
74
74
  end
75
75
 
76
76
  def can_redeem?
@@ -9,6 +9,16 @@ module SpreeCmCommissioner
9
9
 
10
10
  state_machine.before_transition to: :address, do: :hold_order_holds
11
11
 
12
+ # Same transitions core calls create_tax_charge! from (gems/core/app/models/spree/order/checkout.rb,
13
+ # before_transition to: :payment / from: :address), so this fires twice per checkout, plus
14
+ # again for orders TransitOrder::Create/DraftOrders::Create already called it on at creation.
15
+ #
16
+ # Each run is a destroy_all plus one rule lookup per line item — cheap enough that recomputing
17
+ # on every step isn't worth guarding against, and it's what keeps the fee correct if a rule or
18
+ # line item changes between steps.
19
+ state_machine.before_transition to: :payment, do: :create_service_fee_charge!
20
+ state_machine.before_transition from: :address, do: :create_service_fee_charge!
21
+
12
22
  # Fails fast, before any of the other before_transition/around_transition work below runs
13
23
  # (external-system unstock, Redis unstock, notifications, ...), when the inventory hold has
14
24
  # already expired and been released rather than converted — see
@@ -10,7 +10,11 @@ module SpreeCmCommissioner
10
10
  private
11
11
 
12
12
  def sanitize_phone_number
13
- return if phone_number.blank?
13
+ if phone_number.blank?
14
+ self.intel_phone_number = nil
15
+ self.country_code = nil
16
+ return
17
+ end
14
18
 
15
19
  parser = PhoneNumberParser.call(
16
20
  phone_number: phone_number,