spree_cm_commissioner 2.13.4 → 2.13.7

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 (404) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +5 -0
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/spree/admin/base_controller_decorator.rb +14 -0
  5. data/app/controllers/spree/admin/customer_notifications_controller.rb +16 -1
  6. data/app/controllers/spree/admin/inventory_items_controller.rb +11 -0
  7. data/app/controllers/spree/admin/payment_fee_rules_controller.rb +76 -0
  8. data/app/controllers/spree/admin/prepaid_transactions_controller.rb +69 -0
  9. data/app/controllers/spree/admin/private_sales_controller.rb +6 -0
  10. data/app/controllers/spree/admin/product_commissions_controller.rb +2 -1
  11. data/app/controllers/spree/admin/reschedule_policies_controller.rb +148 -0
  12. data/app/controllers/spree/admin/reschedule_requests_controller.rb +82 -0
  13. data/app/controllers/spree/admin/service_fee_rules_controller.rb +1 -0
  14. data/app/controllers/spree/admin/store_credit_admin_controller.rb +122 -0
  15. data/app/controllers/spree/admin/ticket_transfer_rules_controller.rb +7 -0
  16. data/app/controllers/spree/api/v2/platform/store_credits_controller_decorator.rb +35 -0
  17. data/app/controllers/spree/api/v2/storefront/account/prepaid_balances_controller.rb +52 -0
  18. data/app/controllers/spree/api/v2/storefront/account/prepaid_transactions_controller.rb +56 -0
  19. data/app/controllers/spree/api/v2/storefront/cart_payment_fees_controller.rb +48 -0
  20. data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +37 -0
  21. data/app/controllers/spree/api/v2/storefront/prepaid_config_controller.rb +22 -0
  22. data/app/controllers/spree/api/v2/storefront/reschedule_requests_controller.rb +156 -0
  23. data/app/controllers/spree/api/v2/storefront/seat_layouts_controller.rb +3 -1
  24. data/app/controllers/spree/api/v2/storefront/transit/item_availabilities_controller.rb +66 -0
  25. data/app/controllers/spree/api/v2/tenant/account/prepaid_balances_controller.rb +51 -0
  26. data/app/controllers/spree/api/v2/tenant/account/prepaid_transactions_controller.rb +53 -0
  27. data/app/controllers/spree/api/v2/tenant/base_controller.rb +20 -0
  28. data/app/controllers/spree/api/v2/tenant/cart_payment_fees_controller.rb +43 -0
  29. data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +42 -0
  30. data/app/controllers/spree/api/v2/tenant/prepaid_config_controller.rb +29 -0
  31. data/app/controllers/spree_cm_commissioner/admin/store_credits_controller_decorator.rb +217 -0
  32. data/app/errors/spree_cm_commissioner/reschedule_policies/invalid_policyable_error.rb +8 -0
  33. data/app/errors/spree_cm_commissioner/reschedule_policies/not_reschedulable_error.rb +11 -0
  34. data/app/errors/spree_cm_commissioner/reschedule_requests/cancel_not_allowed_error.rb +6 -0
  35. data/app/errors/spree_cm_commissioner/reschedule_requests/complete_cash_payment_error.rb +6 -0
  36. data/app/errors/spree_cm_commissioner/reschedule_requests/eligibility_denied_error.rb +7 -0
  37. data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +6 -2
  38. data/app/factory/spree_cm_commissioner/reschedule_telegram_message_factory.rb +141 -0
  39. data/app/finders/spree_cm_commissioner/inventory_items/recently_changed_finder.rb +1 -1
  40. data/app/helpers/spree/admin/base_helper_decorator.rb +12 -0
  41. data/app/helpers/spree/admin/reschedule_policies_helper.rb +35 -0
  42. data/app/helpers/spree/admin/reschedule_requests_helper.rb +79 -0
  43. data/app/helpers/spree_cm_commissioner/admin/fee_rules_helper.rb +23 -0
  44. data/app/interactors/spree_cm_commissioner/create_ticket.rb +1 -0
  45. data/app/jobs/spree_cm_commissioner/audit_event_job.rb +29 -0
  46. data/app/jobs/spree_cm_commissioner/prepaid_reconciliation_cron_job.rb +9 -0
  47. data/app/jobs/spree_cm_commissioner/prepaid_reverse_top_up_job.rb +16 -0
  48. data/app/jobs/spree_cm_commissioner/prepaid_setup_types_job.rb +24 -0
  49. data/app/jobs/spree_cm_commissioner/prepaid_top_up_credit_job.rb +31 -0
  50. data/app/jobs/spree_cm_commissioner/reschedule_requests/approval_reminder_job.rb +17 -0
  51. data/app/jobs/spree_cm_commissioner/reschedule_requests/execute_job.rb +20 -0
  52. data/app/jobs/spree_cm_commissioner/reschedule_requests/expire_overdue_requests_job.rb +12 -0
  53. data/app/jobs/spree_cm_commissioner/reschedule_requests/expire_request_job.rb +15 -0
  54. data/app/jobs/spree_cm_commissioner/reschedule_requests/payment_reminder_job.rb +17 -0
  55. data/app/models/concerns/spree_cm_commissioner/audit_wrapper.rb +139 -0
  56. data/app/models/concerns/spree_cm_commissioner/integrations/inventory_sync_cachable.rb +9 -0
  57. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +2 -1
  58. data/app/models/concerns/spree_cm_commissioner/order_prepaid_payment.rb +15 -0
  59. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +58 -8
  60. data/app/models/concerns/spree_cm_commissioner/percent_fee_calculable.rb +36 -0
  61. data/app/models/concerns/spree_cm_commissioner/product_type.rb +6 -2
  62. data/app/models/concerns/spree_cm_commissioner/reschedule_request_state_machine.rb +332 -0
  63. data/app/models/concerns/spree_cm_commissioner/scoped_fee_rule.rb +129 -0
  64. data/app/models/concerns/spree_cm_commissioner/service_type.rb +1 -1
  65. data/app/models/concerns/spree_cm_commissioner/store_preference.rb +1 -0
  66. data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +6 -0
  67. data/app/models/concerns/spree_cm_commissioner/variant_metadata.rb +24 -0
  68. data/app/models/concerns/spree_cm_commissioner/vehicle_kind.rb +1 -1
  69. data/app/models/spree/core/controller_helpers/currency_decorator.rb +70 -0
  70. data/app/models/spree_cm_commissioner/adjustment_decorator.rb +1 -0
  71. data/app/models/spree_cm_commissioner/audit_event.rb +11 -0
  72. data/app/models/spree_cm_commissioner/calculators/payment_fee_flat_amount.rb +21 -0
  73. data/app/models/spree_cm_commissioner/calculators/payment_fee_percent.rb +17 -0
  74. data/app/models/spree_cm_commissioner/calculators/service_fee_flat_amount.rb +4 -0
  75. data/app/models/spree_cm_commissioner/calculators/service_fee_percent.rb +5 -2
  76. data/app/models/spree_cm_commissioner/configuration.rb +17 -9
  77. data/app/models/spree_cm_commissioner/currency_rate.rb +15 -0
  78. data/app/models/spree_cm_commissioner/current.rb +8 -0
  79. data/app/models/spree_cm_commissioner/customer_notification.rb +4 -1
  80. data/app/models/spree_cm_commissioner/exports/import_order_guests_csv.rb +153 -0
  81. data/app/models/spree_cm_commissioner/guest.rb +21 -0
  82. data/app/models/spree_cm_commissioner/integration.rb +31 -0
  83. data/app/models/spree_cm_commissioner/integrations/book_me_bus_v1.rb +64 -0
  84. data/app/models/spree_cm_commissioner/integrations/larryta.rb +81 -0
  85. data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +4 -10
  86. data/app/models/spree_cm_commissioner/integrations/vireak_buntham.rb +82 -0
  87. data/app/models/spree_cm_commissioner/inventory_item.rb +22 -5
  88. data/app/models/spree_cm_commissioner/line_item_decorator.rb +6 -2
  89. data/app/models/spree_cm_commissioner/notification.rb +8 -0
  90. data/app/models/spree_cm_commissioner/order_decorator.rb +31 -3
  91. data/app/models/spree_cm_commissioner/order_updater_decorator.rb +6 -1
  92. data/app/models/spree_cm_commissioner/payment_decorator.rb +31 -0
  93. data/app/models/spree_cm_commissioner/payment_fee_rule.rb +97 -0
  94. data/app/models/spree_cm_commissioner/payment_method_decorator.rb +5 -0
  95. data/app/models/spree_cm_commissioner/payment_method_store_credit_decorator.rb +81 -0
  96. data/app/models/spree_cm_commissioner/place.rb +2 -0
  97. data/app/models/spree_cm_commissioner/prepaid_config.rb +59 -0
  98. data/app/models/spree_cm_commissioner/prepaid_transaction.rb +102 -0
  99. data/app/models/spree_cm_commissioner/product_decorator.rb +6 -0
  100. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +4 -2
  101. data/app/models/spree_cm_commissioner/reschedule_policy.rb +94 -0
  102. data/app/models/spree_cm_commissioner/reschedule_policy_assignment.rb +19 -0
  103. data/app/models/spree_cm_commissioner/reschedule_request.rb +227 -0
  104. data/app/models/spree_cm_commissioner/reschedule_requests/guest_selection.rb +80 -0
  105. data/app/models/spree_cm_commissioner/reschedule_snapshot.rb +65 -0
  106. data/app/models/spree_cm_commissioner/route.rb +1 -0
  107. data/app/models/spree_cm_commissioner/seat_layout.rb +8 -0
  108. data/app/models/spree_cm_commissioner/service_calendar.rb +4 -0
  109. data/app/models/spree_cm_commissioner/service_fee_rule.rb +17 -114
  110. data/app/models/spree_cm_commissioner/stock/availability_checker.rb +1 -1
  111. data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +1 -1
  112. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +4 -0
  113. data/app/models/spree_cm_commissioner/store_credit_category_decorator.rb +23 -0
  114. data/app/models/spree_cm_commissioner/store_credit_decorator.rb +142 -0
  115. data/app/models/spree_cm_commissioner/store_credit_event_decorator.rb +94 -0
  116. data/app/models/spree_cm_commissioner/store_credit_type_decorator.rb +43 -0
  117. data/app/models/spree_cm_commissioner/store_decorator.rb +8 -0
  118. data/app/models/spree_cm_commissioner/tenant.rb +37 -0
  119. data/app/models/spree_cm_commissioner/trip.rb +2 -1
  120. data/app/models/spree_cm_commissioner/trip_stop.rb +2 -0
  121. data/app/models/spree_cm_commissioner/user_decorator.rb +79 -0
  122. data/app/models/spree_cm_commissioner/variant_decorator.rb +9 -1
  123. data/app/models/spree_cm_commissioner/vehicle.rb +3 -0
  124. data/app/models/spree_cm_commissioner/vehicle_type.rb +1 -0
  125. data/app/models/spree_cm_commissioner/vendor_decorator.rb +53 -0
  126. data/app/models/spree_cm_commissioner/vendor_place.rb +2 -0
  127. data/app/notifications/spree_cm_commissioner/reschedule_approved_notification.rb +7 -0
  128. data/app/notifications/spree_cm_commissioner/reschedule_cancelled_notification.rb +7 -0
  129. data/app/notifications/spree_cm_commissioner/reschedule_completed_notification.rb +7 -0
  130. data/app/notifications/spree_cm_commissioner/reschedule_expired_notification.rb +7 -0
  131. data/app/notifications/spree_cm_commissioner/reschedule_failed_notification.rb +7 -0
  132. data/app/notifications/spree_cm_commissioner/reschedule_notification_base.rb +41 -0
  133. data/app/notifications/spree_cm_commissioner/reschedule_payment_reminder_notification.rb +15 -0
  134. data/app/notifications/spree_cm_commissioner/reschedule_rejected_notification.rb +7 -0
  135. data/app/notifications/spree_cm_commissioner/reschedule_requested_notification.rb +7 -0
  136. data/app/overrides/spree/admin/products/_form/offline.html.erb.deface +9 -0
  137. data/app/overrides/spree/admin/shared/_main_menu/sidebar_reschedule.html.erb.deface +9 -0
  138. data/app/overrides/spree/admin/shared/_main_menu/sidebar_store_credit.html.erb.deface +8 -0
  139. data/app/overrides/spree/admin/shared/_order_tabs/reschedule_requests.html.erb.deface +10 -0
  140. data/app/overrides/spree/admin/shared/_product_tabs/stock_managements.html.erb.deface +1 -1
  141. data/app/overrides/spree/admin/shared/sub_menu/_configuration/menu_items.html.erb.deface +5 -0
  142. data/app/presenters/spree_cm_commissioner/prepaid_balance_presenter.rb +16 -0
  143. data/app/queries/spree_cm_commissioner/store_credit_overview_query.rb +75 -0
  144. data/app/queries/spree_cm_commissioner/user_store_credit_summary_query.rb +53 -0
  145. data/app/serializables/spree_cm_commissioner/item_availability_sync.rb +10 -0
  146. data/app/serializers/spree/v2/storefront/prepaid_balance_serializer.rb +15 -0
  147. data/app/serializers/spree/v2/storefront/prepaid_config_serializer.rb +11 -0
  148. data/app/serializers/spree/v2/storefront/prepaid_transaction_serializer.rb +17 -0
  149. data/app/serializers/spree/v2/storefront/user_serializer_decorator.rb +6 -0
  150. data/app/serializers/spree/v2/tenant/cart_serializer.rb +2 -1
  151. data/app/serializers/spree/v2/tenant/payment_method_serializer.rb +3 -0
  152. data/app/serializers/spree/v2/tenant/prepaid_balance_serializer.rb +15 -0
  153. data/app/serializers/spree/v2/tenant/prepaid_config_serializer.rb +16 -0
  154. data/app/serializers/spree/v2/tenant/prepaid_transaction_serializer.rb +16 -0
  155. data/app/serializers/spree_cm_commissioner/api/v2/platform/homepage_section_relatable_options_serializer.rb +5 -0
  156. data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +3 -0
  157. data/app/serializers/spree_cm_commissioner/v2/storefront/inventory_item_serializer.rb +1 -0
  158. data/app/serializers/spree_cm_commissioner/v2/storefront/item_availability_sync_serializer.rb +10 -0
  159. data/app/serializers/spree_cm_commissioner/v2/storefront/payment_fee_preview_serializer.rb +19 -0
  160. data/app/serializers/spree_cm_commissioner/v2/storefront/payment_method_serializer_decorator.rb +16 -0
  161. data/app/serializers/spree_cm_commissioner/v2/storefront/reschedule_request_serializer.rb +49 -0
  162. data/app/serializers/spree_cm_commissioner/v2/storefront/reschedule_snapshot_serializer.rb +24 -0
  163. data/app/serializers/spree_cm_commissioner/v2/storefront/seat_layout_serializer.rb +8 -0
  164. data/app/services/spree_cm_commissioner/audit_logger.rb +59 -0
  165. data/app/services/spree_cm_commissioner/checkout/add_store_credit_decorator.rb +17 -0
  166. data/app/services/spree_cm_commissioner/checkout/add_store_credit_payments.rb +96 -0
  167. data/app/services/spree_cm_commissioner/checkout/remove_store_credit_decorator.rb +17 -0
  168. data/app/services/spree_cm_commissioner/currency_converter/create.rb +27 -0
  169. data/app/services/spree_cm_commissioner/currency_rates/manage.rb +99 -0
  170. data/app/services/spree_cm_commissioner/customer_notifications/clone.rb +39 -0
  171. data/app/services/spree_cm_commissioner/google_maps_geocoder.rb +41 -0
  172. data/app/services/spree_cm_commissioner/imports/orders/create.rb +24 -11
  173. data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +1 -2
  174. data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +11 -0
  175. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/client.rb +230 -0
  176. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/connection.rb +147 -0
  177. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_item_status.rb +52 -0
  178. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_items.rb +91 -0
  179. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/unstock_inventory.rb +188 -0
  180. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_inventory.rb +79 -0
  181. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_item_availability.rb +354 -0
  182. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_locations.rb +103 -0
  183. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_nationality_pricing.rb +93 -0
  184. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_product.rb +70 -0
  185. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_route_stops.rb +121 -0
  186. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_routes.rb +128 -0
  187. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_seat_layout.rb +412 -0
  188. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_service_calendars.rb +59 -0
  189. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +224 -0
  190. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trips.rb +769 -0
  191. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_variant.rb +69 -0
  192. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_vehicle_types.rb +92 -0
  193. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/time_parser.rb +19 -0
  194. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/base.rb +40 -0
  195. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/inventory_item_status.rb +148 -0
  196. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/location.rb +55 -0
  197. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/reservation_cart.rb +85 -0
  198. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat.rb +128 -0
  199. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb +386 -0
  200. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/stop.rb +84 -0
  201. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/trip.rb +237 -0
  202. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/wallet_payment.rb +48 -0
  203. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_manager.rb +22 -0
  204. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/full_sync_strategy.rb +116 -0
  205. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/incremental_sync_strategy.rb +63 -0
  206. data/app/services/spree_cm_commissioner/integrations/larryta/external_client/client.rb +314 -0
  207. data/app/services/spree_cm_commissioner/integrations/larryta/external_client/connection.rb +74 -0
  208. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/external_inventory_item_status.rb +53 -0
  209. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/external_inventory_items.rb +106 -0
  210. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/hold_seats.rb +143 -0
  211. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/release_seats.rb +90 -0
  212. data/app/services/spree_cm_commissioner/integrations/larryta/inventory/unstock_inventory.rb +143 -0
  213. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_item_availability.rb +325 -0
  214. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_product.rb +72 -0
  215. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_routes.rb +142 -0
  216. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_seat_layout.rb +255 -0
  217. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_stock_item.rb +55 -0
  218. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_trip_stop.rb +171 -0
  219. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_trips.rb +397 -0
  220. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_variant.rb +63 -0
  221. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_vehicle_type.rb +79 -0
  222. data/app/services/spree_cm_commissioner/integrations/larryta/resources/base.rb +39 -0
  223. data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking.rb +113 -0
  224. data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking_session.rb +43 -0
  225. data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking_transaction.rb +236 -0
  226. data/app/services/spree_cm_commissioner/integrations/larryta/resources/direction.rb +71 -0
  227. data/app/services/spree_cm_commissioner/integrations/larryta/resources/passenger.rb +48 -0
  228. data/app/services/spree_cm_commissioner/integrations/larryta/resources/schedule.rb +150 -0
  229. data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat.rb +31 -0
  230. data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat_availability.rb +49 -0
  231. data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat_layout.rb +211 -0
  232. data/app/services/spree_cm_commissioner/integrations/larryta/sync_manager.rb +17 -0
  233. data/app/services/spree_cm_commissioner/integrations/larryta/sync_strategies/full_sync_strategy.rb +79 -0
  234. data/app/services/spree_cm_commissioner/integrations/larryta/sync_strategies/incremental_sync_strategy.rb +58 -0
  235. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +14 -15
  236. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +7 -4
  237. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +6 -3
  238. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +0 -3
  239. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +1 -3
  240. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +1 -3
  241. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +1 -2
  242. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/external_client/client.rb +170 -0
  243. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/external_client/connection.rb +59 -0
  244. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/external_inventory_item_status.rb +48 -0
  245. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/external_inventory_items.rb +92 -0
  246. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/unstock_inventory.rb +230 -0
  247. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_item_availability.rb +270 -0
  248. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_locations.rb +96 -0
  249. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_nationality_pricing.rb +76 -0
  250. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_product.rb +69 -0
  251. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_routes.rb +110 -0
  252. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_seat_layout.rb +207 -0
  253. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_stock_item.rb +43 -0
  254. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_trip_stop.rb +192 -0
  255. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_trips.rb +377 -0
  256. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_variant.rb +56 -0
  257. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_vehicle_type.rb +51 -0
  258. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/base.rb +32 -0
  259. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/booking.rb +48 -0
  260. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/booking_transaction.rb +46 -0
  261. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/cell.rb +53 -0
  262. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/confirm_booking_request.rb +43 -0
  263. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/location.rb +37 -0
  264. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/schedule.rb +138 -0
  265. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/seat.rb +48 -0
  266. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/seat_layout.rb +79 -0
  267. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_manager.rb +17 -0
  268. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_strategies/full_sync_strategy.rb +87 -0
  269. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_strategies/incremental_sync_strategy.rb +53 -0
  270. data/app/services/spree_cm_commissioner/inventory_holds/release.rb +13 -0
  271. data/app/services/spree_cm_commissioner/inventory_items/generate_permanent_item.rb +48 -0
  272. data/app/services/spree_cm_commissioner/order_holds/hold.rb +17 -0
  273. data/app/services/spree_cm_commissioner/order_holds/release.rb +31 -0
  274. data/app/services/spree_cm_commissioner/payment_fees/apply.rb +30 -0
  275. data/app/services/spree_cm_commissioner/payment_fees/clear.rb +15 -0
  276. data/app/services/spree_cm_commissioner/payment_fees/preview.rb +70 -0
  277. data/app/services/spree_cm_commissioner/payments/find_or_create_decorator.rb +18 -0
  278. data/app/services/spree_cm_commissioner/place_resolver.rb +95 -0
  279. data/app/services/spree_cm_commissioner/prepaid/admin_credit.rb +44 -0
  280. data/app/services/spree_cm_commissioner/prepaid/check_top_up_limits.rb +47 -0
  281. data/app/services/spree_cm_commissioner/prepaid/freeze.rb +58 -0
  282. data/app/services/spree_cm_commissioner/prepaid/mutation.rb +78 -0
  283. data/app/services/spree_cm_commissioner/prepaid/reverse_order_top_up.rb +23 -0
  284. data/app/services/spree_cm_commissioner/prepaid/reverse_top_up.rb +53 -0
  285. data/app/services/spree_cm_commissioner/prepaid/top_up.rb +68 -0
  286. data/app/services/spree_cm_commissioner/prepaid/unfreeze.rb +47 -0
  287. data/app/services/spree_cm_commissioner/prepaid_metrics.rb +37 -0
  288. data/app/services/spree_cm_commissioner/prepaid_reconciliation_service.rb +71 -0
  289. data/app/services/spree_cm_commissioner/reschedule_policies/assign.rb +52 -0
  290. data/app/services/spree_cm_commissioner/reschedule_policies/create.rb +71 -0
  291. data/app/services/spree_cm_commissioner/reschedule_policies/exclude.rb +40 -0
  292. data/app/services/spree_cm_commissioner/reschedule_policies/resolve.rb +109 -0
  293. data/app/services/spree_cm_commissioner/reschedule_policies/unassign.rb +23 -0
  294. data/app/services/spree_cm_commissioner/reschedule_policies/update.rb +73 -0
  295. data/app/services/spree_cm_commissioner/reschedule_requests/authorize_initiator.rb +79 -0
  296. data/app/services/spree_cm_commissioner/reschedule_requests/build_request.rb +317 -0
  297. data/app/services/spree_cm_commissioner/reschedule_requests/calculate_fee.rb +71 -0
  298. data/app/services/spree_cm_commissioner/reschedule_requests/cancel.rb +55 -0
  299. data/app/services/spree_cm_commissioner/reschedule_requests/check_eligibility.rb +243 -0
  300. data/app/services/spree_cm_commissioner/reschedule_requests/collect_payment.rb +204 -0
  301. data/app/services/spree_cm_commissioner/reschedule_requests/execute.rb +252 -0
  302. data/app/services/spree_cm_commissioner/reschedule_requests/expire_overdue_requests.rb +28 -0
  303. data/app/services/spree_cm_commissioner/reschedule_requests/expire_request.rb +24 -0
  304. data/app/services/spree_cm_commissioner/reschedule_requests/guests/update_seats.rb +210 -0
  305. data/app/services/spree_cm_commissioner/reschedule_requests/initiate.rb +255 -0
  306. data/app/services/spree_cm_commissioner/reschedule_requests/issue_compensation.rb +67 -0
  307. data/app/services/spree_cm_commissioner/reschedule_requests/issue_coupon.rb +90 -0
  308. data/app/services/spree_cm_commissioner/reschedule_requests/issue_store_credit.rb +16 -0
  309. data/app/services/spree_cm_commissioner/reschedule_requests/notifications/send_approval_reminder.rb +43 -0
  310. data/app/services/spree_cm_commissioner/reschedule_requests/notifications/send_payment_reminder.rb +46 -0
  311. data/app/services/spree_cm_commissioner/reschedule_requests/notifications/send_status_update.rb +42 -0
  312. data/app/services/spree_cm_commissioner/reschedule_requests/seats/hold.rb +137 -0
  313. data/app/services/spree_cm_commissioner/reschedule_requests/split_line_item.rb +86 -0
  314. data/app/services/spree_cm_commissioner/reschedule_requests/telegram/notify_operations.rb +37 -0
  315. data/app/services/spree_cm_commissioner/reschedule_requests/telegram/notify_vendor.rb +51 -0
  316. data/app/services/spree_cm_commissioner/reschedule_requests/vendors/approve.rb +61 -0
  317. data/app/services/spree_cm_commissioner/reschedule_requests/vendors/reject.rb +58 -0
  318. data/app/services/spree_cm_commissioner/topup_draft_orders/create.rb +234 -0
  319. data/app/services/spree_cm_commissioner/trips/service_calendars/create_or_update.rb +25 -14
  320. data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +43 -6
  321. data/app/views/spree/admin/classifications/edit.html.erb +17 -2
  322. data/app/views/spree/admin/classifications/new.html.erb +18 -2
  323. data/app/views/spree/admin/customer_notifications/index.html.erb +45 -3
  324. data/app/views/spree/admin/integrations/_book_me_bus_v1_fields.html.erb +47 -0
  325. data/app/views/spree/admin/integrations/_form.html.erb +12 -0
  326. data/app/views/spree/admin/integrations/_larryta_fields.html.erb +58 -0
  327. data/app/views/spree/admin/integrations/_vireak_buntham_fields.html.erb +75 -0
  328. data/app/views/spree/admin/integrations/index.html.erb +1 -1
  329. data/app/views/spree/admin/payment_fee_rules/_form.html.erb +135 -0
  330. data/app/views/spree/admin/payment_fee_rules/edit.html.erb +11 -0
  331. data/app/views/spree/admin/payment_fee_rules/index.html.erb +100 -0
  332. data/app/views/spree/admin/payment_fee_rules/new.html.erb +11 -0
  333. data/app/views/spree/admin/prepaid_transactions/index.html.erb +102 -0
  334. data/app/views/spree/admin/prepaid_transactions/show.html.erb +146 -0
  335. data/app/views/spree/admin/private_sales/_form.html.erb +24 -1
  336. data/app/views/spree/admin/private_sales/_stock.html.erb +6 -1
  337. data/app/views/spree/admin/product_commissions/index.html.erb +7 -2
  338. data/app/views/spree/admin/reschedule_policies/_form.html.erb +65 -0
  339. data/app/views/spree/admin/reschedule_policies/edit.html.erb +11 -0
  340. data/app/views/spree/admin/reschedule_policies/index.html.erb +84 -0
  341. data/app/views/spree/admin/reschedule_policies/new.html.erb +11 -0
  342. data/app/views/spree/admin/reschedule_requests/_cancel_modal.html.erb +34 -0
  343. data/app/views/spree/admin/reschedule_requests/_search_form.html.erb +57 -0
  344. data/app/views/spree/admin/reschedule_requests/_tabs.html.erb +14 -0
  345. data/app/views/spree/admin/reschedule_requests/index.html.erb +64 -0
  346. data/app/views/spree/admin/reschedule_requests/show.html.erb +212 -0
  347. data/app/views/spree/admin/reschedule_requests/state_changes.html.erb +42 -0
  348. data/app/views/spree/admin/service_fee_rules/_form.html.erb +15 -9
  349. data/app/views/spree/admin/service_fee_rules/index.html.erb +1 -1
  350. data/app/views/spree/admin/shared/_products.html.erb +3 -0
  351. data/app/views/spree/admin/shared/_reschedule_policies_tabs.html.erb +22 -0
  352. data/app/views/spree/admin/shared/_ticket_transfer_details.html.erb +3 -0
  353. data/app/views/spree/admin/shared/sub_menu/_reschedule.html.erb +4 -0
  354. data/app/views/spree/admin/stock_managements/_header.html.erb +26 -0
  355. data/app/views/spree/admin/stock_managements/_variant_stock_items.html.erb +1 -1
  356. data/app/views/spree/admin/stock_managements/calendar.html.erb +285 -171
  357. data/app/views/spree/admin/stock_managements/index.html.erb +1 -18
  358. data/app/views/spree/admin/store_credit_admin/_tabs.html.erb +31 -0
  359. data/app/views/spree/admin/store_credit_admin/_tenant_selector.html.erb +17 -0
  360. data/app/views/spree/admin/store_credit_admin/show.html.erb +200 -0
  361. data/app/views/spree/admin/store_credits/index.html.erb +224 -0
  362. data/app/views/spree/admin/tenants/_form.html.erb +33 -0
  363. data/app/views/spree/admin/ticket_transfer_rules/_form.html.erb +28 -5
  364. data/config/initializers/spree_dependencies.rb +3 -0
  365. data/config/initializers/spree_permitted_attributes.rb +1 -0
  366. data/config/locales/en.yml +363 -1
  367. data/config/locales/km.yml +282 -1
  368. data/config/routes.rb +67 -2
  369. data/db/migrate/20260327143200_create_cm_audit_events.rb +21 -0
  370. data/db/migrate/20260410045815_create_cm_currency_rates.rb +23 -0
  371. data/db/migrate/20260411102139_add_metadata_to_cm_inventory_items.rb +6 -0
  372. data/db/migrate/20260506120000_create_cm_reschedule_policies.rb +38 -0
  373. data/db/migrate/20260506120001_create_cm_reschedule_requests.rb +67 -0
  374. data/db/migrate/20260506120002_add_reschedule_count_to_cm_guests.rb +5 -0
  375. data/db/migrate/20260521140000_create_cm_reschedule_snapshots.rb +38 -0
  376. data/db/migrate/20260522160000_create_cm_reschedule_policy_assignments.rb +17 -0
  377. data/db/migrate/20260722000001_create_cm_prepaid_transactions.rb +31 -0
  378. data/db/migrate/20260722000002_add_constraints_to_store_credits.rb +46 -0
  379. data/db/migrate/20260722000003_add_expires_at_to_store_credits.rb +6 -0
  380. data/db/migrate/20260722000004_add_issuer_tenant_id_to_spree_store_credits.rb +7 -0
  381. data/db/migrate/20260722000005_add_immutability_trigger_to_cm_prepaid_transactions.rb +36 -0
  382. data/db/migrate/20260918045558_add_prepaid_currency_columns_to_cm_tenants.rb +6 -0
  383. data/db/migrate/20260923090000_add_public_private_metadata_to_cm_prepaid_transactions.rb +36 -0
  384. data/db/migrate/20260928000001_add_closed_to_cm_inventory_items.rb +5 -0
  385. data/db/migrate/20261001100000_create_cm_payment_fee_rules.rb +17 -0
  386. data/db/migrate/20261001100001_add_payment_fee_total_to_spree_orders.rb +5 -0
  387. data/db/migrate/20261006073317_add_offline_to_spree_products.rb +6 -0
  388. data/lib/spree_cm_commissioner/cached_inventory_item.rb +7 -1
  389. data/lib/spree_cm_commissioner/payment_fees/payment_fee_preview.rb +24 -0
  390. data/lib/spree_cm_commissioner/reschedule_policy_rules.rb +75 -0
  391. data/lib/spree_cm_commissioner/serializable_transaction.rb +38 -0
  392. data/lib/spree_cm_commissioner/store_credit_currency_summary.rb +11 -0
  393. data/lib/spree_cm_commissioner/store_credit_summary.rb +11 -0
  394. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +11 -0
  395. data/lib/spree_cm_commissioner/test_helper/factories/prepaid_transaction_factory.rb +12 -0
  396. data/lib/spree_cm_commissioner/test_helper/factories/reschedule_policy_assignment_factory.rb +7 -0
  397. data/lib/spree_cm_commissioner/test_helper/factories/reschedule_policy_factory.rb +59 -0
  398. data/lib/spree_cm_commissioner/test_helper/factories/reschedule_request_factory.rb +51 -0
  399. data/lib/spree_cm_commissioner/test_helper/factories/reschedule_snapshot_factory.rb +27 -0
  400. data/lib/spree_cm_commissioner/version.rb +1 -1
  401. data/lib/spree_cm_commissioner.rb +5 -0
  402. data/lib/tasks/prepaid_reconciliation_cron_scheduler.rake +7 -0
  403. data/lib/tasks/prepaid_setup_types.rake +7 -0
  404. metadata +295 -2
@@ -0,0 +1,43 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ # See Spree::Api::V2::Storefront::CartPaymentFeesController.
6
+ class CartPaymentFeesController < BaseController
7
+ include Spree::Api::V2::Storefront::OrderConcern
8
+
9
+ before_action :ensure_order
10
+
11
+ # override
12
+ def index
13
+ render_serialized_payload do
14
+ collection_serializer.new(collection).serializable_hash
15
+ end
16
+ end
17
+
18
+ # override
19
+ def collection
20
+ @collection ||= requested_payment_methods.map do |payment_method|
21
+ SpreeCmCommissioner::PaymentFees::Preview.call(order: spree_current_order, payment_method: payment_method)
22
+ .value[:payment_fee_preview]
23
+ end
24
+ end
25
+
26
+ # override
27
+ def collection_serializer
28
+ SpreeCmCommissioner::V2::Storefront::PaymentFeePreviewSerializer
29
+ end
30
+
31
+ private
32
+
33
+ # Ids the order can't actually pay with are dropped rather than rejected, so a stale client
34
+ # list (e.g. a method disabled since the catalog was cached) still gets the rest.
35
+ def requested_payment_methods
36
+ ids = Array(params[:payment_method_ids]).map(&:to_s)
37
+ spree_current_order.available_payment_methods.select { |pm| ids.include?(pm.id.to_s) }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -7,6 +7,10 @@ module Spree
7
7
 
8
8
  before_action -> { ensure_anonymous_booking_allowed!(draft_order_products) }, only: :create
9
9
 
10
+ # A top-up credits a wallet, which must belong to a real user -- unlike #create,
11
+ # this action has no guest-checkout path.
12
+ before_action :require_spree_current_user, only: [:topup]
13
+
10
14
  # Creates a generic draft order from a locally-assembled cart in a single request.
11
15
  # Endpoint: POST /api/v2/tenant/draft_orders
12
16
  # Params:
@@ -33,6 +37,44 @@ module Spree
33
37
  end
34
38
  end
35
39
 
40
+ # Builds a prepaid top-up order from just an amount and a payment method -- the client
41
+ # never fetches/knows the prepaid_top_up product or variant. Tenant-scoped: the resulting
42
+ # order.tenant_id makes PrepaidTopUpCreditJob produce tenant-issued (not platform-issued)
43
+ # credit.
44
+ # Endpoint: POST /api/v2/tenant/draft_orders/topup
45
+ # Params:
46
+ # - amount: the custom top-up amount (required)
47
+ # - payment_method_id: id of the payment method to attach (required)
48
+ # - currency: optional -- must be inside this tenant's own supported_currencies_list if
49
+ # given; omitted, resolves to Tenant#preferred_currency_or_default (see
50
+ # BaseController#resolve_requested_currency). This deliberately doesn't read
51
+ # current_currency here even though it's tenant-aware now too -- resolve_requested_currency
52
+ # hard-rejects an out-of-list currency (422), where current_currency would silently
53
+ # fall back instead, which would make this endpoint's own validation cosmetic.
54
+ def topup
55
+ currency, error = resolve_requested_currency
56
+ return render_error_payload(error) if error
57
+
58
+ result = SpreeCmCommissioner::TopupDraftOrders::Create.call(
59
+ amount: params[:amount],
60
+ payment_method_id: params[:payment_method_id],
61
+ user: spree_current_user,
62
+ store: current_store,
63
+ currency: currency,
64
+ tenant: @tenant,
65
+ options: {
66
+ device_id: params[:device_id],
67
+ last_ip_address: request.remote_ip
68
+ }
69
+ )
70
+
71
+ if result.success?
72
+ render_serialized_payload(201) { serialize_resource(serializable_order(result.value[:order])) }
73
+ else
74
+ render_error_payload(result.error)
75
+ end
76
+ end
77
+
36
78
  private
37
79
 
38
80
  # Re-fetch the created order with everything the enriched payload touches eager-loaded, so
@@ -0,0 +1,29 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ # Endpoint: GET /api/v2/tenant/prepaid_config
6
+ # Mirrors Spree::Api::V2::Storefront::PrepaidConfigController, resolved with this tenant
7
+ # so supported_currencies/preferred_currency reflect Tenant#supported_currencies_list/
8
+ # #preferred_currency_or_default -- the same values BaseController#
9
+ # resolve_requested_currency already validates/defaults `?currency=` against on
10
+ # GET account/prepaid_balances and POST draft_orders/topup, now discoverable up front
11
+ # instead of only enforced after the fact on submit. Not under account/ -- this is
12
+ # tenant-wide config, not per-user account data (identical response for every user).
13
+ class PrepaidConfigController < BaseController
14
+ before_action :require_spree_current_user
15
+
16
+ def show
17
+ render_serialized_payload { serialize_resource(SpreeCmCommissioner::PrepaidConfig.resolve(tenant: @tenant)) }
18
+ end
19
+
20
+ private
21
+
22
+ def resource_serializer
23
+ Spree::V2::Tenant::PrepaidConfigSerializer
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,217 @@
1
+ module SpreeCmCommissioner
2
+ module Admin
3
+ # Core's admin/users/:user_id/store_credits form (Spree::Admin::StoreCreditsController) builds
4
+ # a raw Spree::StoreCredit directly -- fine for ordinary store credit, but for the prepaid
5
+ # category (still named "Wallet" at the DB row level -- see #prepaid_category? below) it would
6
+ # silently bypass everything Prepaid::AdminCredit exists for: the ledger
7
+ # (cm_prepaid_transactions), the one-row-per-(user,currency,issuer_tenant_id) invariant
8
+ # (Mutation#find_or_create_prepaid_credit!), and the credit_type the prepaid system actually
9
+ # looks up by (core's own associate_credit_type would set "Expiring"/"Non-expiring" here, not
10
+ # our "prepaid" type -- the row wouldn't even show up in GET /prepaid). Route a prepaid-category
11
+ # submission through AdminCredit instead; everything else keeps core's original behavior.
12
+ module StoreCreditsControllerDecorator
13
+ TABS = %w[credits transactions].freeze
14
+ TRANSACTIONS_PER_PAGE = 25
15
+
16
+ # override
17
+ # Adds a per-currency balance summary (actual balance, topped up, reversed, held) for this
18
+ # user's prepaid-category credits, same shape as the admin store-credit dashboard's overview
19
+ # tab (StoreCreditOverviewQuery) -- see UserStoreCreditSummaryQuery for why it's a separate
20
+ # query rather than reusing that one directly. Also adds a second, filterable "Transactions"
21
+ # tab across this user's own prepaid ledger, same tab/search pattern as that dashboard's own
22
+ # Transactions tab -- see build_transactions_search below.
23
+ def index
24
+ super
25
+ @currency_summaries = SpreeCmCommissioner::UserStoreCreditSummaryQuery.new(user: @user).call
26
+ @current_tab = TABS.include?(params[:tab]) ? params[:tab] : 'credits'
27
+ build_transactions_search if @current_tab == 'transactions'
28
+ end
29
+
30
+ # override
31
+ def create
32
+ return super unless prepaid_category?
33
+
34
+ result = SpreeCmCommissioner::Prepaid::AdminCredit.call(
35
+ user: @user,
36
+ amount: params[:store_credit][:amount],
37
+ currency: params[:store_credit][:currency],
38
+ reason: params[:store_credit][:memo].presence || 'admin credit',
39
+ admin: try_spree_current_user,
40
+ idempotency_key: admin_credit_idempotency_key
41
+ )
42
+
43
+ if result.success?
44
+ flash[:success] = flash_message_for(result.value, :successfully_created)
45
+ redirect_to spree.admin_user_store_credits_path(@user)
46
+ else
47
+ @store_credit = @user.store_credits.new(permitted_store_credit_params)
48
+ load_categories
49
+ flash[:error] = result.error.value || result.error.to_s
50
+ render :new, status: :unprocessable_entity
51
+ end
52
+ end
53
+
54
+ # override
55
+ # A prepaid credit's amount is derived entirely from the ledger (top-ups, admin credits,
56
+ # reversals) -- letting this form edit it directly would silently desync amount from
57
+ # cm_prepaid_transactions with no ledger row to explain the change. destroy is already safe
58
+ # (prepaid_transactions has dependent: :restrict_with_exception), so only update needs a guard.
59
+ #
60
+ # For a non-prepaid credit, core's own #update leaves `action`/`action_originator` unset, so
61
+ # Spree::StoreCredit#store_event (store_credit_decorator.rb) reuses/overwrites the single
62
+ # `allocation` event instead of appending one -- the prior amount and who changed it are
63
+ # both lost, and originator ends up nil. Setting `action` here makes store_event build a new
64
+ # event row per edit instead, so amount history survives.
65
+ def update
66
+ return credit_not_editable if prepaid_category?
67
+
68
+ @store_credit.assign_attributes(permitted_store_credit_params)
69
+ @store_credit.action = SpreeCmCommissioner::StoreCreditDecorator::ADMIN_ADJUSTMENT_ACTION
70
+ @store_credit.action_originator = try_spree_current_user
71
+ @store_credit.created_by = try_spree_current_user
72
+
73
+ if @store_credit.save
74
+ flash[:success] = flash_message_for(@store_credit, :successfully_updated)
75
+ redirect_to spree.admin_user_store_credits_path(@user)
76
+ else
77
+ load_categories
78
+ flash[:error] = Spree.t('store_credit.errors.unable_to_update')
79
+ render :edit, status: :unprocessable_entity
80
+ end
81
+ end
82
+
83
+ # Reverses a single top_up/admin_credit ledger row (member route, admin/users/:user_id/
84
+ # store_credits/:id/reverse_top_up -- :id is the store_credit, params[:transaction_id]
85
+ # picks which ledger row). Keyed off the transaction id so a resubmitted form (double-click,
86
+ # retried request) can't reverse the same row twice.
87
+ def reverse_top_up
88
+ @store_credit = @user.store_credits.find(params[:id])
89
+ transaction = @store_credit.prepaid_transactions.find(params[:transaction_id])
90
+
91
+ error = not_reversible_reason(transaction)
92
+ if error
93
+ flash[:error] = error
94
+ return redirect_to spree.admin_user_store_credits_path(@user)
95
+ end
96
+
97
+ result = SpreeCmCommissioner::Prepaid::ReverseTopUp.call(
98
+ store_credit: @store_credit,
99
+ amount: transaction.amount,
100
+ idempotency_key: "admin_reverse:#{transaction.id}",
101
+ reason: params[:reason].presence || "admin reversal of #{transaction.transaction_type} ##{transaction.id}",
102
+ admin: try_spree_current_user
103
+ )
104
+
105
+ if result.success?
106
+ flash[:success] = Spree.t(:successfully_updated, resource: Spree::StoreCredit.model_name.human)
107
+ else
108
+ flash[:error] = result.error.value || result.error.to_s
109
+ end
110
+
111
+ redirect_to spree.admin_user_store_credits_path(@user)
112
+ end
113
+
114
+ # Freezes a prepaid-category store_credit (member route, admin/users/:user_id/store_credits/
115
+ # :id/freeze). 'full' blocks both spend and top-up; 'topup_only' blocks only top-up -- see
116
+ # StoreCreditDecorator#spend_blocked?/#topup_blocked?.
117
+ def freeze
118
+ @store_credit = @user.store_credits.find(params[:id])
119
+
120
+ result = SpreeCmCommissioner::Prepaid::Freeze.call(
121
+ store_credit: @store_credit,
122
+ admin: try_spree_current_user,
123
+ reason: params[:reason],
124
+ level: params[:level].presence || 'full',
125
+ until_at: params[:until_at].presence
126
+ )
127
+
128
+ if result.success?
129
+ flash[:success] = Spree.t(:successfully_updated, resource: Spree::StoreCredit.model_name.human)
130
+ else
131
+ flash[:error] = result.error.value || result.error.to_s
132
+ end
133
+
134
+ redirect_to spree.admin_user_store_credits_path(@user)
135
+ end
136
+
137
+ # Lifts a freeze on a prepaid-category store_credit (member route, admin/users/:user_id/
138
+ # store_credits/:id/unfreeze).
139
+ def unfreeze
140
+ @store_credit = @user.store_credits.find(params[:id])
141
+ SpreeCmCommissioner::Prepaid::Unfreeze.call(store_credit: @store_credit, admin: try_spree_current_user)
142
+
143
+ flash[:success] = Spree.t(:successfully_updated, resource: Spree::StoreCredit.model_name.human)
144
+ redirect_to spree.admin_user_store_credits_path(@user)
145
+ end
146
+
147
+ private
148
+
149
+ # This user's own prepaid ledger rows across every currency/tenant, filtered via Ransack
150
+ # (q[currency_eq], q[transaction_type_eq], q[created_at_gteq]/[created_at_lteq], ...) --
151
+ # same fields the admin store-credit dashboard's own Transactions tab filters on, minus
152
+ # user_email_cont (already scoped to one user here). Also carries the per-credit "recent
153
+ # activity + Reverse" behavior formerly on an inline <details> block under each credit row
154
+ # -- @reversed_transaction_ids is computed over this user's *entire* ledger, not just the
155
+ # current page, since a reversal row and the top_up/admin_credit it reverses can land on
156
+ # different pages once this is paginated.
157
+ def build_transactions_search
158
+ params[:q] = {} if params[:q].blank?
159
+ @transactions_search = SpreeCmCommissioner::PrepaidTransaction.where(user: @user).ransack(params[:q])
160
+ @transactions = @transactions_search.result
161
+ .includes(:order, :tenant, :store_credit, :admin)
162
+ .order(created_at: :desc)
163
+ .page(params[:page])
164
+ .per(TRANSACTIONS_PER_PAGE)
165
+
166
+ @reversed_transaction_ids = SpreeCmCommissioner::PrepaidTransaction
167
+ .where(user: @user, transaction_type: 'reverse_top_up')
168
+ .pluck(:idempotency_key)
169
+ .filter_map { |key| key[/\Aadmin_reverse:(\d+)\z/, 1]&.to_i }
170
+ .to_set
171
+ end
172
+
173
+ # Deterministic within a 30s window so a double-clicked/retried submit of the same form
174
+ # collides on the ledger's unique idempotency_key (see Mutation#write_ledger!) instead of
175
+ # double-crediting the user; a genuinely separate credit made later (new window) or with a
176
+ # different amount/currency/memo still gets its own row.
177
+ def admin_credit_idempotency_key
178
+ memo_digest = Digest::SHA256.hexdigest(params[:store_credit][:memo].to_s)[0, 12]
179
+ window = Time.current.to_i / 30
180
+ "admin_credit:#{@user.id}:#{params[:store_credit][:amount]}:#{params[:store_credit][:currency]}:#{memo_digest}:#{window}"
181
+ end
182
+
183
+ # See SpreeCmCommissioner::StoreCreditCategoryDecorator::WALLET_CATEGORY_NAME for why this
184
+ # is still "Wallet" at the DB level -- renaming that row's `name` is a data migration
185
+ # outside this refactor's scope; this method just gives the DB-level check a
186
+ # codebase-consistent name to call.
187
+ def prepaid_category?
188
+ category_id = params.dig(:store_credit, :category_id) || @store_credit&.category_id
189
+ wallet_name = SpreeCmCommissioner::StoreCreditCategoryDecorator::WALLET_CATEGORY_NAME
190
+ category_id.present? && Spree::StoreCreditCategory.find_by(id: category_id)&.name == wallet_name
191
+ end
192
+
193
+ def credit_not_editable
194
+ flash[:error] = I18n.t('spree_cm_commissioner.store_credit.credit_not_editable')
195
+ redirect_to spree.admin_user_store_credits_path(@user)
196
+ end
197
+
198
+ def not_reversible_reason(transaction)
199
+ unless %w[top_up admin_credit].include?(transaction.transaction_type) && transaction.status == 'completed'
200
+ return I18n.t('spree_cm_commissioner.store_credit.transaction_not_reversible')
201
+ end
202
+
203
+ # ReverseTopUp only ever adjusts the prepaid ledger -- it never touches the order/payment. For
204
+ # an order-backed top_up (a real payment the customer made), reversing here would silently
205
+ # claw back store credit without refunding the original payment, leaving the order looking
206
+ # paid while the money it credited is gone. Route that case through a real refund instead.
207
+ return I18n.t('spree_cm_commissioner.store_credit.order_backed_transaction_not_reversible') if transaction.order.present?
208
+
209
+ nil
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ unless Spree::Admin::StoreCreditsController.ancestors.include?(SpreeCmCommissioner::Admin::StoreCreditsControllerDecorator)
216
+ Spree::Admin::StoreCreditsController.prepend(SpreeCmCommissioner::Admin::StoreCreditsControllerDecorator)
217
+ end
@@ -0,0 +1,8 @@
1
+ # Raised when a policy is assigned to an unsupported policyable type, or when
2
+ # the policy's vendor does not match the policyable's vendor.
3
+ module SpreeCmCommissioner
4
+ module ReschedulePolicies
5
+ class InvalidPolicyableError < StandardError
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ # Raised when one or more line items are explicitly excluded from rescheduling.
2
+ module SpreeCmCommissioner
3
+ module ReschedulePolicies
4
+ class NotReschedulableError < StandardError
5
+ # override
6
+ def message
7
+ I18n.t('reschedule_policies.resolve.not_reschedulable')
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module SpreeCmCommissioner
2
+ module RescheduleRequests
3
+ class CancelNotAllowedError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module SpreeCmCommissioner
2
+ module RescheduleRequests
3
+ class CompleteCashPaymentError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ # Raised when one or more line items are explicitly excluded from rescheduling.
2
+ module SpreeCmCommissioner
3
+ module RescheduleRequests
4
+ class EligibilityDeniedError < StandardError
5
+ end
6
+ end
7
+ 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,141 @@
1
+ # 🔁 --- [RESCHEDULE REQUESTED] ---
2
+ #
3
+ # Request Number: <code>RS525393292</code>
4
+ # Booking: <code>R898147633</code>
5
+ #
6
+ # <b>Leg 1</b>
7
+ # <i>🗓️ Aug 24, 2026 - 08:00 → Aug 26, 2026 - 08:00</i>
8
+ # <i>💺 A1, A2 → B3, B4</i>
9
+ #
10
+ # <b>Leg 2</b>
11
+ # <i>🗓️ Aug 27, 2026 - 18:00 → Aug 29, 2026 - 18:00</i>
12
+ #
13
+ # <b>Fee:</b> $5.00
14
+ # <b>Balance due:</b> $12.00
15
+ # <i>⏳ Respond before Aug 24, 2026 - 20:00</i>
16
+
17
+ module SpreeCmCommissioner
18
+ class RescheduleTelegramMessageFactory < TelegramMessageFactory
19
+ attr_reader :reschedule_request
20
+
21
+ def initialize(title:, reschedule_request:, subtitle: nil)
22
+ @reschedule_request = reschedule_request
23
+
24
+ # `subtitle` carries `last_error` for the ops escalation - exception text, the
25
+ # likeliest place for `<` and `&`. Telegram rejects malformed HTML with a 400,
26
+ # so an unescaped one loses the very alert that needed a human.
27
+ super(title: title, subtitle: escape(subtitle))
28
+ end
29
+
30
+ # override
31
+ def body
32
+ text = []
33
+ text << "Request Number: #{inline_code(reschedule_request.number)}\nBooking: #{inline_code(reschedule_request.order&.number)}"
34
+ text << leg_blocks.join("\n\n") if leg_blocks.any?
35
+ text << money_lines.join("\n")
36
+ text.compact_blank.join("\n\n")
37
+ end
38
+
39
+ # override
40
+ def footer
41
+ return if reschedule_request.expires_at.blank?
42
+
43
+ italic("⏳ Respond before #{pretty_datetime(reschedule_request.expires_at)}")
44
+ end
45
+
46
+ private
47
+
48
+ # Snapshots are paired by `position`, so zipping the two sides gives one
49
+ # before -> after block per leg. Seats, checkout and product only get a line when
50
+ # they changed; the date is always shown, because a seat-only move still needs to
51
+ # say which departure it is about.
52
+ def leg_blocks
53
+ @leg_blocks ||= begin
54
+ before = reschedule_request.previous_request_snapshots.sort_by(&:position)
55
+ after = reschedule_request.new_request_snapshots.sort_by(&:position)
56
+ pairs = before.zip(after)
57
+
58
+ pairs.each_with_index.filter_map do |(before_snapshot, after_snapshot), index|
59
+ next if before_snapshot.nil?
60
+
61
+ lines = []
62
+ lines << bold("Leg #{index + 1}") if pairs.size > 1
63
+ lines << product_line(before_snapshot, after_snapshot)
64
+ lines << date_line(before_snapshot, after_snapshot)
65
+ lines << checkout_line(before_snapshot, after_snapshot)
66
+ lines << seat_line(before_snapshot, after_snapshot)
67
+ lines.compact.join("\n").presence
68
+ end
69
+ end
70
+ end
71
+
72
+ def date_line(before_snapshot, after_snapshot)
73
+ from = pretty_datetime(before_snapshot&.from_date)
74
+ to = pretty_datetime(after_snapshot&.from_date)
75
+ return if from.blank? && to.blank?
76
+ return italic("🗓️ #{from}") if from == to
77
+
78
+ italic("🗓️ #{from} → #{to}")
79
+ end
80
+
81
+ # Accommodation legs carry a checkout date; transit legs do not.
82
+ def checkout_line(before_snapshot, after_snapshot)
83
+ from = checkout_text(before_snapshot)
84
+ to = checkout_text(after_snapshot)
85
+ return if from.blank? && to.blank?
86
+ return if from == to
87
+
88
+ italic("🏁 until #{from} → until #{to}")
89
+ end
90
+
91
+ def checkout_text(snapshot)
92
+ return '' if snapshot.nil? || snapshot.to_date.blank?
93
+ return '' if snapshot.from_date.present? && snapshot.to_date.to_date == snapshot.from_date.to_date
94
+
95
+ pretty_date(snapshot.to_date)
96
+ end
97
+
98
+ # Blank when the seats did not move, or when the product has no seating at all.
99
+ def seat_line(before_snapshot, after_snapshot)
100
+ from = seat_text(before_snapshot)
101
+ to = seat_text(after_snapshot)
102
+ return if from.blank? && to.blank?
103
+ return if from == to
104
+
105
+ italic("💺 #{from.presence || '-'} → #{to.presence || 'to be assigned'}")
106
+ end
107
+
108
+ # Only meaningful when the policy allowed a variant change.
109
+ def product_line(before_snapshot, after_snapshot)
110
+ from = before_snapshot.variant&.product&.name.to_s
111
+ to = after_snapshot&.variant&.product&.name.to_s
112
+ return if from.blank? || from == to
113
+
114
+ bold("#{escape(from)} → #{escape(to)}")
115
+ end
116
+
117
+ def seat_text(snapshot)
118
+ escape(Array(snapshot&.block_labels).compact_blank.join(', '))
119
+ end
120
+
121
+ # Telegram's HTML parse mode only tolerates `<`, `>` and `&` as entities, and
122
+ # rejects the whole message otherwise. Vendor-supplied names ("Bed & Breakfast")
123
+ # hit this in normal use.
124
+ def escape(text)
125
+ return text if text.blank?
126
+
127
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
128
+ end
129
+
130
+ def money_lines
131
+ lines = [bold_amount('Fee', reschedule_request.display_fee_amount)]
132
+ lines << bold_amount('Balance due', reschedule_request.display_amount_due_total) if reschedule_request.amount_due_total.to_f.positive?
133
+ lines << bold_amount('Credit', reschedule_request.display_credit_amount_total) if reschedule_request.credit_amount_total.to_f.positive?
134
+ lines.compact
135
+ end
136
+
137
+ def bold_amount(label, money)
138
+ "#{bold("#{label}:")} #{money}"
139
+ end
140
+ end
141
+ end
@@ -23,7 +23,7 @@ module SpreeCmCommissioner
23
23
  def fetch_recent_items
24
24
  query = SpreeCmCommissioner::InventoryItem.active
25
25
  .joins(variant: :product)
26
- .where(spree_variants: { track_inventory: true })
26
+ .where(spree_variants: { track_inventory: true }, spree_products: { offline: false })
27
27
  .where('cm_inventory_items.updated_at > ?', @time_range)
28
28
  .select(
29
29
  'cm_inventory_items.id',
@@ -26,6 +26,18 @@ module Spree
26
26
  rescue StandardError
27
27
  nil
28
28
  end
29
+
30
+ # A small info-circle icon with a native-title hover tooltip, for labeling a metric/column
31
+ # whose meaning isn't obvious from its name alone (e.g. "Held" -> "Amount currently
32
+ # authorized ..."). Uses svg_icon/backend-info-circle.svg -- the same inline-SVG mechanism
33
+ # every other icon in this admin theme already renders through (menu tabs, buttons via
34
+ # link_to_with_icon) -- rather than a text/Unicode glyph, which can render as an invisible
35
+ # box depending on the system font and was the reason two earlier attempts here didn't show.
36
+ def hint_icon(hint)
37
+ icon = svg_icon(name: 'info-circle.svg', width: 12, height: 12)
38
+ icon_style = 'cursor: help; margin-left: 4px; display: inline-flex; vertical-align: middle;'
39
+ content_tag(:span, icon, title: hint, style: icon_style)
40
+ end
29
41
  end
30
42
  end
31
43
  end
@@ -0,0 +1,35 @@
1
+ module Spree
2
+ module Admin
3
+ module ReschedulePoliciesHelper
4
+ # Human-readable fee summary for a policy's calculator.
5
+ def reschedule_policy_fee_label(policy)
6
+ calculator = policy.calculator
7
+ return '-' if calculator.blank?
8
+
9
+ case calculator
10
+ when Spree::Calculator::FlatRate
11
+ Spree::Money.new(calculator.preferred_amount, currency: calculator.preferred_currency).to_s
12
+ when Spree::Calculator::PercentOnLineItem
13
+ "#{calculator.preferred_percent}%"
14
+ else
15
+ calculator.class.name.demodulize.titleize
16
+ end
17
+ end
18
+
19
+ # 'percent' or 'flat', used to pre-select the fee-type toggle on the form.
20
+ def reschedule_policy_fee_type(policy)
21
+ policy.calculator.is_a?(Spree::Calculator::PercentOnLineItem) ? 'percent' : 'flat'
22
+ end
23
+
24
+ # Bootstrap badge for the policy status enum.
25
+ def reschedule_policy_status_badge(status)
26
+ klass = { 'active' => 'badge-success',
27
+ 'draft' => 'badge-secondary',
28
+ 'inactive' => 'badge-warning',
29
+ 'archived' => 'badge-dark'
30
+ }.fetch(status.to_s, 'badge-light')
31
+ content_tag :span, status.to_s.titleize, class: "badge #{klass}"
32
+ end
33
+ end
34
+ end
35
+ end