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
@@ -2,20 +2,26 @@ module Spree
2
2
  module V2
3
3
  module Storefront
4
4
  module LineItemSerializerDecorator
5
- def self.prepended(base)
5
+ def self.prepended(base) # rubocop:disable Metrics/MethodLength
6
6
  base.attributes :from_date, :to_date, :need_confirmation, :product_type, :event_status, :amount, :display_amount,
7
7
  :checkin_date, :checkout_date,
8
8
  :number, :qr_data,
9
9
  :kyc, :kyc_fields, :remaining_total_guests, :number_of_guests,
10
10
  :completion_steps, :available_social_contact_platforms, :allow_anonymous_booking, :discontinue_on,
11
11
  :high_demand, :jwt_token,
12
- :direction, :trip_id, :boarding_trip_stop_id, :drop_off_trip_stop_id
12
+ :direction, :trip_id, :boarding_trip_stop_id, :drop_off_trip_stop_id,
13
+ :service_fee_total, :display_service_fee_total
13
14
 
14
15
  base.attribute :required_self_check_in_location, &:required_self_check_in_location?
15
16
  base.attribute :allowed_self_check_in, &:allowed_self_check_in?
16
17
  base.attribute :allowed_upload_later, &:allowed_upload_later?
17
18
  base.attribute :delivery_required, &:delivery_required?
18
19
  base.attribute :sufficient_stock, &:sufficient_stock?
20
+ base.attribute :open_dated, &:open_dated?
21
+ base.attribute :redeemed, &:redeemed?
22
+ base.attribute :can_redeem, &:can_redeem?
23
+ base.attribute :expired, &:expired?
24
+ base.attribute :open_dated_route_label, &:open_dated_route_label
19
25
 
20
26
  base.attribute :pickup_map_place do |line_item|
21
27
  line_item.pickup_map_place&.to_h
@@ -13,7 +13,8 @@ module Spree
13
13
  :promo_total, :display_promo_total, :item_count, :special_instructions, :display_total,
14
14
  :pre_tax_item_amount, :display_pre_tax_item_amount, :pre_tax_total, :display_pre_tax_total,
15
15
  :shipment_state, :payment_state, :public_metadata,
16
- :phone_number, :intel_phone_number, :country_code, :request_state, :channel
16
+ :phone_number, :intel_phone_number, :country_code, :request_state, :channel,
17
+ :service_fee_total, :display_service_fee_total
17
18
 
18
19
  belongs_to :user, serializer: Spree::V2::Tenant::UserSerializer
19
20
  belongs_to :billing_address,
@@ -11,13 +11,19 @@ module Spree
11
11
  :display_amount, :number, :qr_data, :kyc, :kyc_fields, :remaining_total_guests, :number_of_guests,
12
12
  :completion_steps, :available_social_contact_platforms, :allow_anonymous_booking,
13
13
  :discontinue_on, :high_demand, :jwt_token, :pre_tax_amount, :display_pre_tax_amount, :public_metadata,
14
- :direction, :trip_id, :boarding_trip_stop_id, :drop_off_trip_stop_id, :passenger_count, :remark
14
+ :direction, :trip_id, :boarding_trip_stop_id, :drop_off_trip_stop_id, :passenger_count, :remark,
15
+ :service_fee_total, :display_service_fee_total
15
16
 
16
17
  attribute :required_self_check_in_location, &:required_self_check_in_location?
17
18
  attribute :allowed_self_check_in, &:allowed_self_check_in?
18
19
  attribute :allowed_upload_later, &:allowed_upload_later?
19
20
  attribute :delivery_required, &:delivery_required?
20
21
  attribute :sufficient_stock, &:sufficient_stock?
22
+ attribute :open_dated, &:open_dated?
23
+ attribute :redeemed, &:redeemed?
24
+ attribute :can_redeem, &:can_redeem?
25
+ attribute :expired, &:expired?
26
+ attribute :open_dated_route_label, &:open_dated_route_label
21
27
 
22
28
  attribute :pickup_map_place do |line_item|
23
29
  line_item.pickup_map_place&.to_h
@@ -6,7 +6,8 @@ module SpreeCmCommissioner
6
6
 
7
7
  attributes :first_name, :last_name, :dob, :gender, :kyc_fields, :other_occupation, :created_at, :updated_at,
8
8
  :qr_data, :social_contact_platform, :social_contact, :available_social_contact_platforms, :event_id, :seat_number,
9
- :age, :emergency_contact, :other_organization, :expectation, :upload_later, :address, :formatted_bib_number, :phone_number
9
+ :age, :emergency_contact, :other_organization, :expectation, :upload_later, :address, :formatted_bib_number, :phone_number,
10
+ :state
10
11
 
11
12
  attribute :allowed_checkout, &:allowed_checkout?
12
13
  attribute :require_kyc_field, &:require_kyc_field?
@@ -9,7 +9,8 @@ module SpreeCmCommissioner
9
9
 
10
10
  base.attributes :phone_number, :intel_phone_number, :country_code, :request_state,
11
11
  :term_accepted_at,
12
- :channel, :hold_expires_at
12
+ :channel, :hold_expires_at,
13
+ :service_fee_total, :display_service_fee_total
13
14
 
14
15
  base.attribute :has_incomplete_guest_info, &:has_incomplete_guest_info?
15
16
  base.attribute :required_payway_payout, &:required_payway_payout?
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Storefront
4
+ class CrossAppHandoffTokenSerializer < BaseSerializer
5
+ attribute :token, :expires_at
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Storefront
4
+ class ItemAvailabilitySyncSerializer < BaseSerializer
5
+ set_type :item_availability_sync
6
+ attributes :last_sync_at
7
+ end
8
+ end
9
+ end
10
+ end
@@ -148,7 +148,7 @@ module SpreeCmCommissioner
148
148
  SpreeCmCommissioner::InvalidateCacheRequestJob.perform_later(patterns: patterns)
149
149
  end
150
150
 
151
- # {:general, :ticket, :tour, :accommodation, :things_to_do, :football}
151
+ # Every key of BIT_SEGMENT (general, ticket, ..., ferry).
152
152
  def homepage_segment
153
153
  SpreeCmCommissioner::HomepageSectionBitwise::BIT_SEGMENT.keys
154
154
  end
@@ -0,0 +1,59 @@
1
+ require_dependency 'spree_cm_commissioner'
2
+
3
+ module SpreeCmCommissioner
4
+ # AuditLogger is a safe write-through service for API audit events.
5
+ #
6
+ # Usage:
7
+ # AuditLogger.log!(
8
+ # event_type: 'bookmebus.order.hold.request',
9
+ # success: true,
10
+ # metadata: { request: { method: 'POST', endpoint: '/api/v2/trip_reservations', body: {} } },
11
+ # actor: integration,
12
+ # auditable: order,
13
+ # user: current_user,
14
+ # correlation_id: 'uuid',
15
+ # attempt: 0,
16
+ # retention_days: 90
17
+ # )
18
+ #
19
+ # This service NEVER raises — any failure is swallowed and logged so it
20
+ # cannot disrupt the main application flow.
21
+ class AuditLogger
22
+ class << self
23
+ def log!( # rubocop:disable Metrics/ParameterLists
24
+ event_type:,
25
+ success: true,
26
+ metadata: {},
27
+ auditable: nil,
28
+ user: nil,
29
+ correlation_id: nil,
30
+ attempt: 0,
31
+ retention_days: nil
32
+ )
33
+ retention_until = (retention_days || 90).days.from_now.to_date
34
+
35
+ SpreeCmCommissioner::AuditEventJob.perform_later(
36
+ event_type: event_type,
37
+ success: success,
38
+ metadata: metadata,
39
+ auditable: auditable,
40
+ user: user,
41
+ correlation_id: correlation_id,
42
+ attempt: attempt,
43
+ retention_until: retention_until
44
+ )
45
+ rescue StandardError => e
46
+ CmAppLogger.error(
47
+ label: 'AuditLogger failed to persist audit event',
48
+ data: {
49
+ event_type: event_type,
50
+ error_class: e.class.name,
51
+ error_message: e.message,
52
+ backtrace: e.backtrace&.first(5)
53
+ }
54
+ )
55
+ nil
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,27 @@
1
+ module SpreeCmCommissioner
2
+ module Checkout
3
+ # Same as Spree::Checkout::Advance, but stops at `payment` instead of running on to `confirm`.
4
+ # Lets a client reach the payment step in one call instead of stepping through `next` state by
5
+ # state -- useful wherever the checkout doesn't need per-state client input in between (e.g. a
6
+ # top-up order, or a booking flow where guest info is already assigned so there's nothing for
7
+ # the client to fill in at the address/delivery states).
8
+ # Fails with the order's error message when it can't get there.
9
+ class AdvanceToPayment
10
+ prepend Spree::ServiceModule::Base
11
+
12
+ def call(order:)
13
+ Spree::Dependencies.checkout_next_service.constantize.call(order: order) until cannot_make_transition?(order)
14
+
15
+ return success(order) if order.payment?
16
+
17
+ failure(order, order.errors.full_messages.to_sentence.presence)
18
+ end
19
+
20
+ private
21
+
22
+ def cannot_make_transition?(order)
23
+ order.payment? || order.confirm? || order.complete? || order.errors.present?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ # Verifies a token minted by Encode and resolves the user it was signed for. Two-step decode:
2
+ # peek at `sub` without verifying (the signing key is per-user, so the user must be looked up
3
+ # before the signature can be checked), then verify with that user's own secure_token.
4
+ #
5
+ # Also checks the token's `ver` against the user's current cross_app_handoff_version (see
6
+ # Encode) — a mismatch means either a newer token has since been minted (this one is stale) or
7
+ # this exact token was already consumed once (single-use). The check and the bump that follows a
8
+ # match happen inside the same row lock, so two concurrent replays of the same token can't both
9
+ # pass the check before either bumps it.
10
+ module SpreeCmCommissioner
11
+ module CrossAppHandoffTokens
12
+ class Decode
13
+ prepend Spree::ServiceModule::Base
14
+
15
+ def call(token:, purpose:)
16
+ unverified_payload = JWT.decode(token, nil, false).first
17
+ user = Spree.user_class.find_by(id: unverified_payload['sub'])
18
+ return failure(nil, 'invalid_token') if user.nil?
19
+
20
+ payload = JWT.decode(token, user.secure_token, true, algorithm: Encode::ALGORITHM).first
21
+ return failure(nil, 'invalid_purpose') unless payload['purpose'] == purpose.to_s
22
+ return failure(nil, 'invalid_token') unless consume_if_current_version!(user, payload['ver'])
23
+
24
+ success(user: user, payload: payload)
25
+ rescue JWT::DecodeError
26
+ failure(nil, 'invalid_token')
27
+ end
28
+
29
+ private
30
+
31
+ def consume_if_current_version!(user, token_version)
32
+ user.with_lock do
33
+ next false unless token_version == user.cross_app_handoff_version
34
+
35
+ user.cross_app_handoff_version += 1
36
+ user.save!
37
+ true
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,47 @@
1
+ # Mints a stateless, signed handoff token used to carry an authenticated user's identity into
2
+ # another app/session without a second login — modeled on Users::QrData (signed with the user's
3
+ # own secure_token, so it self-revokes if that column is regenerated) and LoadTest::LoginToken
4
+ # (short-lived JWT verified by signature + expiry alone, no DB row to look up or clean up).
5
+ #
6
+ # `purpose` is locked into the signed payload so a token minted for one hand-off can't be replayed
7
+ # for a different one, even before it expires — `operator_sign_in` (customer app → operator app)
8
+ # and `organizer_web_session` (operator app → organizer web dashboard) exist today. See Decode for
9
+ # the counterpart.
10
+ #
11
+ # `ver` (User#cross_app_handoff_version, a plain public_metadata counter — same shape as
12
+ # Users::QrData's qr_data_version, no migration needed) is bumped here on every mint, so minting a
13
+ # new token immediately invalidates any earlier, not-yet-consumed one for this user. Decode bumps
14
+ # it again on successful consumption, so the token just used can't be replayed either.
15
+ module SpreeCmCommissioner
16
+ module CrossAppHandoffTokens
17
+ class Encode
18
+ prepend Spree::ServiceModule::Base
19
+
20
+ TTL = 1.minute
21
+ ALGORITHM = 'HS256'.freeze
22
+
23
+ PURPOSES = %w[operator_sign_in organizer_web_session].freeze
24
+
25
+ def call(user:, purpose:)
26
+ return failure(nil, "invalid purpose: #{purpose}") unless PURPOSES.include?(purpose.to_s)
27
+
28
+ bump_version!(user)
29
+
30
+ expires_at = TTL.from_now
31
+ payload = { sub: user.id, purpose: purpose.to_s, ver: user.cross_app_handoff_version, exp: expires_at.to_i }
32
+
33
+ success(token: JWT.encode(payload, user.secure_token, ALGORITHM), expires_at: expires_at)
34
+ end
35
+
36
+ private
37
+
38
+ def bump_version!(user)
39
+ user.with_lock do
40
+ user.cross_app_handoff_version ||= 0
41
+ user.cross_app_handoff_version += 1
42
+ user.save!
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ module SpreeCmCommissioner
2
+ module CurrencyConverter
3
+ # Converts amounts between two currencies using vendor-specific exchange rates
4
+ class Create
5
+ attr_reader :vendor, :from_currency, :to_currency, :converted_amount
6
+
7
+ def initialize(vendor:, from_currency: 'USD', to_currency: 'USD')
8
+ @vendor = vendor
9
+ @from_currency = from_currency.upcase
10
+ @to_currency = to_currency.upcase
11
+ @converted_amount = nil
12
+ end
13
+
14
+ # Converts the given amount from one currency to another
15
+ def call(amount)
16
+ return amount if from_currency == to_currency || amount.zero?
17
+
18
+ rate = vendor.exchange_rate(from_currency, to_currency)
19
+ raise MissingCurrencyRateError, "No conversion rate from #{from_currency} to #{to_currency} for vendor #{vendor.name}" if rate.nil?
20
+
21
+ @converted_amount = (amount * rate).round(2)
22
+ end
23
+ end
24
+
25
+ class MissingCurrencyRateError < StandardError; end
26
+ end
27
+ end
@@ -0,0 +1,99 @@
1
+ module SpreeCmCommissioner
2
+ module CurrencyRates
3
+ # Manages currency rate creation and updates for vendors.
4
+ # Handles rate deduplication using floating-point tolerance and maintains
5
+ # a history of rate changes by marking old rates as inactive.
6
+ #
7
+ # Example:
8
+ # response = Manage.new(
9
+ # vendor_id: 1,
10
+ # from_currency: 'USD',
11
+ # to_currency: 'KHR',
12
+ # rate: 4100.50
13
+ # ).call
14
+ #
15
+ # if response.success?
16
+ # puts response.currency_rate.rate # => 4100.50
17
+ # else
18
+ # puts response.error # => "Rate must be greater than 0"
19
+ # end
20
+ class Manage
21
+ Response = Struct.new(:success?, :error, :currency_rate, keyword_init: true)
22
+
23
+ attr_reader :options, :vendor_id, :from_currency, :to_currency, :rate
24
+
25
+ def initialize(options = {})
26
+ @options = options
27
+ @vendor_id = options[:vendor_id]
28
+ @from_currency = options[:from_currency]
29
+ @to_currency = options[:to_currency]
30
+ @rate = options[:rate]
31
+ end
32
+
33
+ def call
34
+ return failure_response('Vendor ID is required') if vendor_id.blank?
35
+ return failure_response('From currency is required') if from_currency.blank?
36
+ return failure_response('To currency is required') if to_currency.blank?
37
+ return failure_response('Rate is required') if rate.blank?
38
+ return failure_response('Rate must be greater than 0') if rate.to_f <= 0
39
+
40
+ existing = existing_rates.first
41
+
42
+ # Deduplication: If a rate already exists with the same value (within floating-point tolerance),
43
+ # return the existing rate without creating a new one.
44
+ # This prevents duplicate entries when the same rate is submitted multiple times.
45
+ #
46
+ # Example:
47
+ # Existing rate: 4100.0
48
+ # New rate: 4100.00001 (difference: 0.00001, which is < 0.0001 tolerance)
49
+ # Result: Returns existing rate, no new record created
50
+ #
51
+ # The tolerance of 0.0001 accounts for floating-point precision issues
52
+ # and minor rounding differences in currency calculations.
53
+ return success_response(existing) if existing && (existing.rate - rate.to_f).abs < 0.0001
54
+
55
+ ActiveRecord::Base.transaction do
56
+ override_existing_rates!
57
+ new_rate = create_new_rate
58
+ success_response(new_rate)
59
+ end
60
+ rescue ActiveRecord::RecordInvalid => e
61
+ failure_response(e.message)
62
+ end
63
+
64
+ private
65
+
66
+ def override_existing_rates!
67
+ existing_rates.each do |rate|
68
+ rate.update!(status: 'inactive', override_at: Time.current)
69
+ end
70
+ end
71
+
72
+ def existing_rates
73
+ vendor.currency_rates.by_pair(from_currency, to_currency).where(override_at: nil)
74
+ end
75
+
76
+ def vendor
77
+ @vendor ||= Spree::Vendor.find(vendor_id)
78
+ end
79
+
80
+ def create_new_rate
81
+ SpreeCmCommissioner::CurrencyRate.create!(
82
+ vendor_id: vendor_id,
83
+ from_currency: from_currency.upcase,
84
+ to_currency: to_currency.upcase,
85
+ rate: rate,
86
+ status: 'active'
87
+ )
88
+ end
89
+
90
+ def success_response(currency_rate)
91
+ Response.new(success?: true, error: nil, currency_rate: currency_rate)
92
+ end
93
+
94
+ def failure_response(message)
95
+ Response.new(success?: false, error: message, currency_rate: nil)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -90,6 +90,7 @@ module SpreeCmCommissioner
90
90
  order.update_with_updater!
91
91
  order.line_items.reload
92
92
  order.create_tax_charge!
93
+ order.create_service_fee_charge!
93
94
 
94
95
  order.update_with_updater!
95
96
 
@@ -0,0 +1,41 @@
1
+ module SpreeCmCommissioner
2
+ class GoogleMapsGeocoder
3
+ class GeocodingError < StandardError; end
4
+ class ConfigurationError < StandardError; end
5
+
6
+ def initialize(api_key: nil)
7
+ @api_key = api_key || ENV.fetch('GOOGLE_MAP_KEY', nil)
8
+ end
9
+
10
+ def fetch_location_data(location_name, country_name)
11
+ raise ConfigurationError, 'Google Maps API key not configured' if @api_key.blank?
12
+
13
+ query = "#{location_name}, #{country_name}"
14
+ url = "#{GOOGLE_MAP_API_URL}/place/textsearch/json"
15
+ params = { query: query, key: @api_key }
16
+
17
+ response = Faraday.get(url, params)
18
+
19
+ begin
20
+ data = JSON.parse(response.body)
21
+ rescue JSON::ParserError => e
22
+ raise GeocodingError, "Failed to parse Google Maps API response: #{e.message}"
23
+ end
24
+
25
+ unless data['status'] == 'OK' && data['results'].present?
26
+ error_msg = "Geocoding failed for '#{query}': #{data['status']}"
27
+ error_msg += " - #{data['error_message']}" if data['error_message'].present?
28
+ raise GeocodingError, error_msg
29
+ end
30
+
31
+ result = data['results'].first
32
+ location = result['geometry']['location']
33
+
34
+ {
35
+ lat: location['lat'],
36
+ lon: location['lng'],
37
+ reference: result['reference']
38
+ }
39
+ end
40
+ end
41
+ end
@@ -1,8 +1,7 @@
1
1
  module SpreeCmCommissioner::Integrations::Base
2
2
  class SyncManager
3
- def initialize(integration:, client:)
3
+ def initialize(integration:)
4
4
  @integration = integration
5
- @client = client
6
5
  @sync_session = nil
7
6
  end
8
7
 
@@ -174,6 +174,17 @@ module SpreeCmCommissioner::Integrations::Base
174
174
  @error.nil?
175
175
  end
176
176
 
177
+ # Access metrics
178
+ # @return [Hash] The metrics hash
179
+ attr_reader :metrics
180
+
181
+ # Access API calls
182
+ # @return [Hash] The API calls hash
183
+ attr_reader :api_calls
184
+
185
+ # Timestamp of the last synced item (set by SyncItemAvailability)
186
+ attr_accessor :last_synced_at
187
+
177
188
  # Serialize to hash for JSONB persistence
178
189
  # @return [Hash] Serialized representation
179
190
  def to_h