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,79 @@
1
+ module Spree
2
+ module Admin
3
+ module RescheduleRequestsHelper
4
+ STATE_BADGE_CLASSES = {
5
+ 'pending_approval' => 'badge-warning',
6
+ 'pending_payment' => 'badge-info',
7
+ 'processing' => 'badge-primary',
8
+ 'completed' => 'badge-success',
9
+ 'rejected' => 'badge-danger',
10
+ 'processing_failed' => 'badge-danger',
11
+ 'cancelled' => 'badge-secondary',
12
+ 'expired' => 'badge-dark'
13
+ }.freeze
14
+
15
+ # Drives Spree's always-visible quick-search box. Covers both identities:
16
+ # the account that booked (`user`, which the detail page labels Customer)
17
+ # and the contact details captured on the order, which are often
18
+ # different people and, for guest checkout, the only ones present.
19
+ SEARCH_PREDICATE = "#{%i[
20
+ number
21
+ order_number
22
+ order_email
23
+ order_phone_number
24
+ order_intel_phone_number
25
+ user_email
26
+ user_phone_number
27
+ ].join('_or_')}_cont".freeze
28
+
29
+ def reschedule_request_search_predicate
30
+ SEARCH_PREDICATE
31
+ end
32
+
33
+ def reschedule_request_state_badge(state)
34
+ klass = STATE_BADGE_CLASSES.fetch(state.to_s, 'badge-light')
35
+ content_tag :span, state.to_s.humanize, class: "badge #{klass}"
36
+ end
37
+
38
+ # `approved` is a pseudo-status: `with_status` resolves it through
39
+ # `approved_at`, not `state`.
40
+ def reschedule_request_status_options
41
+ states = SpreeCmCommissioner::RescheduleRequest.state_machines[:state].states.map { |s| s.name.to_s }
42
+ (states + ['approved']).map { |status| [status.humanize, status] }
43
+ end
44
+
45
+ def reschedule_request_fee_label(request)
46
+ return "#{request.fee_percentage.to_d.round(2)}%" if request.fee_percentage.to_d.positive?
47
+
48
+ request.display_fee_amount
49
+ end
50
+
51
+ def reschedule_snapshot_guests(snapshot)
52
+ return [] if snapshot.blank?
53
+
54
+ ids = SpreeCmCommissioner::RescheduleRequests::GuestSelection.snapshot_guest_ids(snapshot)
55
+ return [] if ids.blank?
56
+
57
+ SpreeCmCommissioner::Guest.where(id: ids).order(:id)
58
+ end
59
+
60
+ def reschedule_guest_label(guest)
61
+ guest.display_name_with_bib.presence || "Guest ##{guest.id}"
62
+ end
63
+
64
+ def reschedule_compensation_link(request)
65
+ record = request.reschedule_compensation
66
+ return content_tag(:span, Spree.t(:none), class: 'text-muted') if record.blank?
67
+
68
+ case record
69
+ when Spree::Promotion
70
+ link_to record.name.presence || record.id, spree.edit_admin_promotion_path(record)
71
+ when Spree::StoreCredit
72
+ record.user_id ? link_to(record.display_amount, spree.admin_user_store_credit_path(record.user_id, record)) : record.display_amount
73
+ else
74
+ "#{record.class.name.demodulize.titleize} #{record.id}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,23 @@
1
+ module SpreeCmCommissioner
2
+ module Admin
3
+ # Shared by the service fee and payment fee rule lists: the Scope column links to what the
4
+ # rule is scoped to (the rule itself is opened with the row's edit action).
5
+ module FeeRulesHelper
6
+ def fee_rule_scope_link(rule)
7
+ path = fee_rule_scope_path(rule)
8
+ path ? link_to(rule.scope_label, path) : rule.scope_label
9
+ end
10
+
11
+ private
12
+
13
+ def fee_rule_scope_path(rule)
14
+ case rule.ruleable
15
+ when Spree::Product then spree.edit_admin_product_path(rule.ruleable)
16
+ when Spree::Taxon then spree.edit_admin_taxonomy_taxon_path(rule.ruleable.taxonomy_id, rule.ruleable.id)
17
+ when Spree::Vendor then spree.edit_admin_vendor_path(rule.ruleable)
18
+ when nil then rule.tenant ? spree.edit_admin_tenant_path(rule.tenant) : nil
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -136,6 +136,7 @@ module SpreeCmCommissioner
136
136
  option_type_ids: context.params[:option_type_ids],
137
137
  allow_anonymous_booking: context.params[:allow_anonymous_booking],
138
138
  allow_self_check_in: context.params[:allow_self_check_in],
139
+ offline: context.params[:offline] || false,
139
140
  purchasable_on: context.params[:purchasable_on] || 'both'
140
141
  }
141
142
  end
@@ -0,0 +1,29 @@
1
+ module SpreeCmCommissioner
2
+ class AuditEventJob < ApplicationUniqueJob
3
+ def perform(options = {})
4
+ SpreeCmCommissioner::AuditEvent.create!(
5
+ event_type: options[:event_type],
6
+ success: options.fetch(:success, true),
7
+ auditable: options[:auditable],
8
+ user: options[:user],
9
+ correlation_id: options[:correlation_id],
10
+ attempt: options.fetch(:attempt, 0),
11
+ metadata: options.fetch(:metadata, {}),
12
+ retention_until: options[:retention_until]
13
+ )
14
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
15
+ # Log validation errors but don't retry - these are data issues, not transient failures
16
+ CmAppLogger.log(
17
+ label: 'AuditEventJob validation failed - non-retryable',
18
+ data: {
19
+ event_type: options[:event_type],
20
+ error_class: e.class.name,
21
+ error_message: e.message,
22
+ validation_errors: e.record&.errors&.full_messages
23
+ }
24
+ )
25
+ # Return nil to indicate the job completed (but audit wasn't saved)
26
+ nil
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ class PrepaidReconciliationCronJob < ApplicationJob
3
+ queue_as :default
4
+
5
+ def perform
6
+ SpreeCmCommissioner::PrepaidReconciliationService.run_daily
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ module SpreeCmCommissioner
2
+ # Claws back a top-up's credit when its funding order is canceled -- nothing else in the
3
+ # codebase does this today, so without this job a canceled top-up order silently leaves the
4
+ # customer holding wallet balance nothing actually paid for.
5
+ # ApplicationUniqueJob (deduped by order_id): same reasoning as PrepaidTopUpCreditJob -- the
6
+ # ledger's idempotency_key already makes a duplicate run a safe no-op, this just skips wasting
7
+ # a SERIALIZABLE transaction attempt on it.
8
+ class PrepaidReverseTopUpJob < ApplicationUniqueJob
9
+ def perform(order_id:)
10
+ order = Spree::Order.find_by(id: order_id)
11
+ return if order.nil? || !order.canceled? || !order.prepaid_top_up_order?
12
+
13
+ SpreeCmCommissioner::Prepaid::ReverseOrderTopUp.call(order: order, reason: 'order canceled')
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ module SpreeCmCommissioner
2
+ # One-time setup for the prepaid wallet's store-credit category, plus every
3
+ # Spree::StoreCreditType in STORE_CREDIT_TYPES (our own types and core's generic
4
+ # "Expiring"/"Non-expiring" -- harmless to seed those here too, find_or_create_by! is a no-op
5
+ # if core's own associate_credit_type already created them). Enqueue once per environment
6
+ # before enabling the feature (SpreeCmCommissioner::PrepaidSetupTypesJob.perform_later) --
7
+ # idempotent, so re-running it (retry, or by accident) is a no-op.
8
+ # ApplicationUniqueJob (not ApplicationJob) since it takes no arguments -- two concurrent runs
9
+ # could otherwise both miss on find_or_create_by!'s SELECT and race on the CREATE.
10
+ class PrepaidSetupTypesJob < ApplicationUniqueJob
11
+ def perform
12
+ category = Spree::StoreCreditCategory.wallet!
13
+
14
+ StoreCreditTypeDecorator::STORE_CREDIT_TYPES.each do |type_name|
15
+ Spree::StoreCreditType.find_or_create_by!(name: type_name)
16
+ end
17
+
18
+ CmAppLogger.log(
19
+ label: 'PrepaidSetupTypesJob completed',
20
+ data: { category: category.name, types: StoreCreditTypeDecorator::STORE_CREDIT_TYPES }
21
+ )
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ module SpreeCmCommissioner
2
+ # Credits the wallet for a prepaid_top_up order once the order completes.
3
+ # Deliberately does not wait for payment_fulfilled? -- pre-authorized payment methods
4
+ # (e.g. PayWay v2 with pre-auth) already hold the customer's funds at authorization
5
+ # time, and capture is done manually later -- see docs/concepts/store-credit-wallet-system.md
6
+ # Section 4.3.
7
+ # ApplicationUniqueJob (deduped by order_id): the ledger's idempotency_key already makes a
8
+ # duplicate run a safe no-op, this just skips wasting a SERIALIZABLE transaction attempt on it.
9
+ class PrepaidTopUpCreditJob < ApplicationUniqueJob
10
+ def perform(order_id:)
11
+ order = Spree::Order.find_by(id: order_id)
12
+ return if order.nil? || !order.complete? || !order.prepaid_top_up_order?
13
+
14
+ result = SpreeCmCommissioner::Prepaid::TopUp.call(
15
+ user: order.user,
16
+ amount: order.total,
17
+ currency: order.currency,
18
+ idempotency_key: "topup:order:#{order.number}",
19
+ issuer_tenant_id: order.tenant_id,
20
+ order: order
21
+ )
22
+
23
+ return if result.success?
24
+
25
+ CmAppLogger.error(
26
+ label: 'PrepaidTopUpCreditJob#perform not credited',
27
+ data: { order: order.number, error: result.error.to_s }
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ # Per-request vendor decision nudge. Recipient: the VENDOR, over Telegram.
2
+ #
3
+ # Enqueued from the model's `after_commit :schedule_approval_reminder` with
4
+ # `wait_until:` set a fraction into the decision window. Thin wrapper - the state
5
+ # re-check and the idempotency stamp live in `SendApprovalReminder`.
6
+ module SpreeCmCommissioner
7
+ module RescheduleRequests
8
+ class ApprovalReminderJob < ApplicationJob
9
+ def perform(options = {})
10
+ request = SpreeCmCommissioner::RescheduleRequest.find_by(id: options[:reschedule_request_id])
11
+ return if request.nil?
12
+
13
+ SpreeCmCommissioner::RescheduleRequests::Notifications::SendApprovalReminder.call(reschedule_request: request)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # Runs Execute once the balance-due payment is captured. Enqueued from the
2
+ # payment-completed hook so execution happens AFTER the payment commits — a
3
+ # failed Execute then lands in processing_failed without rolling back the money.
4
+ module SpreeCmCommissioner
5
+ module RescheduleRequests
6
+ class ExecuteJob < ApplicationJob
7
+ def perform(options = {})
8
+ request = SpreeCmCommissioner::RescheduleRequest.find_by(id: options[:reschedule_request_id])
9
+ return if request.nil?
10
+ return unless request.pending_payment? # idempotent: only the awaiting-payment request executes
11
+
12
+ # Authoritative paid check: the enqueuing hook fires mid-transaction,
13
+ # before payment_state settles. Nothing moves unpaid.
14
+ return unless request.order.payment_fulfilled?
15
+
16
+ SpreeCmCommissioner::RescheduleRequests::Execute.call(reschedule_request: request)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ # Cron safety net (every 15 minutes). Catches RescheduleRequests the
2
+ # per-request `ExpireRequestJob` missed (worker restart, queue loss).
3
+ # Thin wrapper — all iteration logic lives in `RescheduleRequests::ExpireOverdueRequests`.
4
+ module SpreeCmCommissioner
5
+ module RescheduleRequests
6
+ class ExpireOverdueRequestsJob < ApplicationJob
7
+ def perform
8
+ SpreeCmCommissioner::RescheduleRequests::ExpireOverdueRequests.call
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # Per-request expiry. Enqueued from the model's `after_commit
2
+ # :schedule_expiration` with `wait_until: expires_at`. Thin wrapper that
3
+ # finds the request and hands off to `RescheduleRequests::ExpireRequest`.
4
+ module SpreeCmCommissioner
5
+ module RescheduleRequests
6
+ class ExpireRequestJob < ApplicationJob
7
+ def perform(options = {})
8
+ request = SpreeCmCommissioner::RescheduleRequest.find_by(id: options[:reschedule_request_id])
9
+ return if request.nil?
10
+
11
+ SpreeCmCommissioner::RescheduleRequests::ExpireRequest.call(reschedule_request: request)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # Per-request payment nudge. Recipient: the CUSTOMER, over FCM plus the in-app list.
2
+ #
3
+ # Enqueued from the model's `after_commit :schedule_payment_reminder` with
4
+ # `wait_until:` set a fraction into the payment window. Thin wrapper - the state
5
+ # re-check and the idempotency stamp live in `SendPaymentReminder`.
6
+ module SpreeCmCommissioner
7
+ module RescheduleRequests
8
+ class PaymentReminderJob < ApplicationJob
9
+ def perform(options = {})
10
+ request = SpreeCmCommissioner::RescheduleRequest.find_by(id: options[:reschedule_request_id])
11
+ return if request.nil?
12
+
13
+ SpreeCmCommissioner::RescheduleRequests::Notifications::SendPaymentReminder.call(reschedule_request: request)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,139 @@
1
+ module SpreeCmCommissioner
2
+ # AuditWrapper is a reusable mixin for classes that make external API calls.
3
+ #
4
+ # It wraps an API call in a three-event lifecycle:
5
+ # 1. <event_base>.request – logged before the call
6
+ # 2. <event_base>.response – logged after a successful call (includes timing)
7
+ # 3. <event_base>.failed – logged when the block raises, then re-raises
8
+ #
9
+ # Metadata follows this canonical shape:
10
+ # {
11
+ # request: { method:, endpoint:, body:, params: },
12
+ # response: { status:, body: },
13
+ # timing: { duration_ms: },
14
+ # error: nil | { message: }
15
+ # }
16
+ #
17
+ # Usage:
18
+ # include SpreeCmCommissioner::AuditWrapper
19
+ #
20
+ # with_audit(
21
+ # event_base: 'bookmebus.order.hold',
22
+ # request: { method: 'POST', endpoint: '/api/v2/trip_reservations', body: body },
23
+ # auditable: order,
24
+ # user: current_user
25
+ # ) do
26
+ # post("#{BASE_PATH}/trip_reservations", body)
27
+ # end
28
+
29
+ module AuditWrapper
30
+ extend ActiveSupport::Concern
31
+
32
+ def with_audit(event_base:, request: {}, auditable: nil, user: nil, # rubocop:disable Metrics/ParameterLists
33
+ attempt: 0, retention_days: nil
34
+ )
35
+ correlation_id = SecureRandom.uuid
36
+
37
+ shared = {
38
+ auditable: auditable,
39
+ user: user,
40
+ correlation_id: correlation_id,
41
+ attempt: attempt,
42
+ retention_days: retention_days
43
+ }
44
+
45
+ # 1. Log the outbound request
46
+ AuditLogger.log!(
47
+ **shared,
48
+ event_type: "#{event_base}.request",
49
+ success: true,
50
+ metadata: build_request_metadata(request)
51
+ )
52
+
53
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
+ result = yield
55
+ duration = elapsed_ms(started_at)
56
+
57
+ # 2. Log the successful response
58
+ AuditLogger.log!(
59
+ **shared,
60
+ event_type: "#{event_base}.response",
61
+ success: true,
62
+ metadata: build_response_metadata(request, result, duration)
63
+ )
64
+
65
+ result
66
+ rescue StandardError => e
67
+ duration = elapsed_ms(started_at || Process.clock_gettime(Process::CLOCK_MONOTONIC))
68
+
69
+ # 3. Log the failure event
70
+ AuditLogger.log!(
71
+ **shared,
72
+ event_type: "#{event_base}.failed",
73
+ success: false,
74
+ metadata: build_error_metadata(request, e, duration)
75
+ )
76
+
77
+ raise
78
+ end
79
+
80
+ private
81
+
82
+ def build_request_metadata(request)
83
+ {
84
+ request: normalize_request(request),
85
+ response: nil,
86
+ timing: nil,
87
+ error: nil
88
+ }
89
+ end
90
+
91
+ def build_response_metadata(request, result, duration_ms)
92
+ status, body = extract_response(result)
93
+
94
+ {
95
+ request: normalize_request(request),
96
+ response: { status: status, body: body },
97
+ timing: { duration_ms: duration_ms },
98
+ error: nil
99
+ }
100
+ end
101
+
102
+ def build_error_metadata(request, error, duration_ms)
103
+ {
104
+ request: normalize_request(request),
105
+ response: nil,
106
+ timing: { duration_ms: duration_ms },
107
+ error: { message: error.message }
108
+ }
109
+ end
110
+
111
+ def normalize_request(request)
112
+ {
113
+ method: request[:method],
114
+ endpoint: request[:endpoint],
115
+ body: request[:body],
116
+ params: request[:params]
117
+ }
118
+ end
119
+
120
+ # Try to extract status + body from common response objects.
121
+ # Falls back to inspecting the raw result.
122
+ def extract_response(result)
123
+ if result.respond_to?(:status) && result.respond_to?(:body)
124
+ [result.status, result.body]
125
+ elsif result.is_a?(Array)
126
+ # Handle arrays of resource objects
127
+ [nil, result.map(&:to_h)]
128
+ elsif result.respond_to?(:to_h)
129
+ [nil, result.to_h]
130
+ else
131
+ [nil, result.inspect]
132
+ end
133
+ end
134
+
135
+ def elapsed_ms(started_at)
136
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ module Integrations
3
+ module InventorySyncCachable
4
+ def sync_cache_ttl
5
+ ENV.fetch('SYNC_CACHE_TTL', '300').to_i.seconds
6
+ end
7
+ end
8
+ end
9
+ end
@@ -45,7 +45,8 @@ module SpreeCmCommissioner
45
45
  'ticket-type' => 'string',
46
46
  'seat-type' => 'string',
47
47
  'intercity-taxi' => 'string',
48
- 'vote-package' => 'integer'
48
+ 'vote-package' => 'integer',
49
+ 'top-up-amount' => 'integer'
49
50
  }.freeze
50
51
 
51
52
  included do
@@ -0,0 +1,15 @@
1
+ module SpreeCmCommissioner
2
+ module OrderPrepaidPayment
3
+ extend ActiveSupport::Concern
4
+
5
+ # Applies the buyer's eligible prepaid/coin balance (FEFO + issuer-scoped, Section 5.2 of
6
+ # docs/concepts/store-credit-wallet-system.md) to this order's outstanding balance. Goes
7
+ # through the injectable Spree::Dependencies point rather than referencing
8
+ # SpreeCmCommissioner::Checkout::AddStoreCreditPayments directly, so callers stay agnostic
9
+ # of the concrete service class.
10
+ def apply_prepaid_payment!(amount: nil)
11
+ result = Spree::Dependencies.checkout_add_store_credit_service.constantize.call(order: self, amount: amount)
12
+ result.success?
13
+ end
14
+ end
15
+ end
@@ -18,6 +18,10 @@ module SpreeCmCommissioner
18
18
  # line item changes between steps.
19
19
  state_machine.before_transition to: :payment, do: :create_service_fee_charge!
20
20
  state_machine.before_transition from: :address, do: :create_service_fee_charge!
21
+ state_machine.before_transition to: :payment, do: :clear_stale_payment_fee!
22
+ state_machine.before_transition from: :address, do: :clear_stale_payment_fee!
23
+
24
+ state_machine.after_transition to: :payment, do: :apply_prepaid_payment_on_checkout, if: :user, unless: :skip_auto_apply_prepaid_payment?
21
25
 
22
26
  # Fails fast, before any of the other before_transition/around_transition work below runs
23
27
  # (external-system unstock, Redis unstock, notifications, ...), when the inventory hold has
@@ -29,6 +33,7 @@ module SpreeCmCommissioner
29
33
  # purely an optimization so a doomed completion attempt doesn't first burn a call to the
30
34
  # external system and a Redis round trip.
31
35
  state_machine.before_transition to: :complete, do: :validate_inventory_hold_before_complete
36
+
32
37
  state_machine.before_transition to: :complete, do: :request, if: :need_confirmation?
33
38
  state_machine.before_transition to: :complete, do: :finalize_guests_and_bibs
34
39
  state_machine.before_transition to: :complete, do: :generate_completion_steps!, if: :product_completion_steps_exist?
@@ -51,9 +56,11 @@ module SpreeCmCommissioner
51
56
  state_machine.after_transition to: :canceled, do: :precalculate_conversion
52
57
  state_machine.after_transition to: :canceled, do: :restock_inventory!
53
58
  state_machine.after_transition to: :canceled, do: :de_allocate_resources
59
+ state_machine.after_transition to: :canceled, do: :enqueue_prepaid_top_up_reversal_job
54
60
 
55
61
  state_machine.after_transition to: :complete, do: :increment_route_fulfilled_order_count
56
62
  state_machine.after_transition to: :complete, do: :allocate_resources
63
+ state_machine.after_transition to: :complete, do: :enqueue_prepaid_top_up_credit_job
57
64
  state_machine.after_transition to: :complete, do: :run_order_complete_alerts
58
65
 
59
66
  scope :accepted, -> { where(request_state: 'accepted') }
@@ -100,6 +107,26 @@ module SpreeCmCommissioner
100
107
  end
101
108
  end
102
109
 
110
+ # Best-effort, like apply_prepaid_payment!'s other callers: a user with no (or no
111
+ # currency-matching) store credit just doesn't get a payment here, and the app can still
112
+ # remove/reapply it afterwards through the existing checkout store-credit endpoints.
113
+ def apply_prepaid_payment_on_checkout
114
+ apply_prepaid_payment!
115
+ end
116
+
117
+ # subscription?: SubscriptionsOrderCreator already builds its own store-credit payment via
118
+ # create_default_payment_if_eligble, then drives the order through Spree::Checkout::Advance,
119
+ # which passes through :payment. Running this callback too would invalidate that payment
120
+ # mid-flow and rebuild a second one, racing the balance/capture logic already in progress.
121
+ #
122
+ # prepaid_top_up_order?: buying wallet balance with wallet balance is circular --
123
+ # Checkout::AddStoreCreditPayments already refuses these (see topup_paid_with_store_credit?),
124
+ # so this is belt-and-suspenders to keep the callback a visible no-op instead of a call that
125
+ # always fails deep in the service.
126
+ def skip_auto_apply_prepaid_payment?
127
+ subscription? || prepaid_top_up_order?
128
+ end
129
+
103
130
  # override
104
131
  def deliver_order_confirmation_email
105
132
  return if requested?
@@ -156,18 +183,21 @@ module SpreeCmCommissioner
156
183
  yield if block_given? # Equal to block.call
157
184
 
158
185
  # After the transition is complete, the following code will execute first before proceeding to other `after_transition` callbacks.
159
- # This ensures that if `reserve_order_holds!` or `unstock_inventory_in_redis!` fails, the state will be rolled back,
186
+ # This ensures that if `reserve_order_holds!`, `unstock_inventory_in_redis!`, or
187
+ # `capture_authorized_store_credit_payments!` fails, the state will be rolled back,
160
188
  # and neither the `finalize!` method nor any notifications will be triggered.
161
- # The payment will be reversed in vPago gem, and `Spree::Checkout::Complete` will be called, which checks `order.reload.complete?`.
162
- # This is critical because if the order state is complete, the payment will be marked as paid.
163
-
189
+ # `unstock_inventory_from_external_system!` is the one exception: it calls out to a real
190
+ # external system, so a DB rollback here can't undo it, and any gateway-backed remainder
191
+ # payment already captured earlier (in process_payments!, before this transaction) can't be
192
+ # undone by this rollback either -- those still need vPago's own reversal/manual refund.
193
+ # Store credit is different: capture_authorized_store_credit_payments! is a plain DB write
194
+ # against our own tables, so it rolls back cleanly with everything else here -- no manual
195
+ # refund needed for that leg specifically.
196
+
197
+ capture_authorized_store_credit_payments!
164
198
  unstock_inventory_from_external_system!
165
199
  reserve_order_holds!
166
200
  unstock_inventory_in_redis!
167
-
168
- # We rollback only order state, and we keep payment state as it is.
169
- # We implement payment in vPago gem, and it will be reversed in the gem.
170
- # Some bank has api for refund, but some don't have the api to refund yet. So we keep the payment state as it is and refund manually.
171
201
  end
172
202
  rescue StandardError => e
173
203
  CmAppLogger.log(
@@ -178,6 +208,16 @@ module SpreeCmCommissioner
178
208
  raise e
179
209
  end
180
210
 
211
+ # Store credit is authorized (not captured) at process_payments! time -- see
212
+ # Spree::PaymentMethod::StoreCredit#auto_capture, set false for this exact reason. Capturing
213
+ # runs first here, before stock is confirmed, but that's still safe: this whole method body is
214
+ # one transaction (see unstock_inventory! above), so if reserve_order_holds! or any later step
215
+ # fails, this capture rolls back along with it -- the customer is never left having spent
216
+ # store credit on an order that didn't actually complete.
217
+ def capture_authorized_store_credit_payments!
218
+ payments.valid.store_credits.where(state: :pending).find_each(&:capture!)
219
+ end
220
+
181
221
  def restock_inventory!
182
222
  ActiveRecord::Base.transaction do
183
223
  restock_inventory_from_external_system!
@@ -389,6 +429,16 @@ module SpreeCmCommissioner
389
429
  SpreeCmCommissioner::VotingCreditAllocationJob.perform_later(order_id: id)
390
430
  end
391
431
 
432
+ def enqueue_prepaid_top_up_credit_job
433
+ SpreeCmCommissioner::PrepaidTopUpCreditJob.perform_later(order_id: id)
434
+ end
435
+
436
+ # Same shape as enqueue_prepaid_top_up_credit_job above: always enqueue, the job itself
437
+ # re-checks prepaid_top_up_order? and no-ops for every other order.
438
+ def enqueue_prepaid_top_up_reversal_job
439
+ SpreeCmCommissioner::PrepaidReverseTopUpJob.perform_later(order_id: id)
440
+ end
441
+
392
442
  def de_allocate_resources
393
443
  SpreeCmCommissioner::VotingCreditDeAllocationJob.perform_later(order_id: id)
394
444
  end
@@ -0,0 +1,36 @@
1
+ module SpreeCmCommissioner
2
+ # "10% + $0.25" fee shape shared by ServiceFeePercent and PaymentFeePercent. Lives outside
3
+ # SpreeCmCommissioner::Calculators on purpose: PricingAction.available_calculator_types lists
4
+ # every constant in that namespace, and a module there would show up as a pickable calculator
5
+ # type. The flat part is named like the *FlatAmount calculators' preference so admins see one
6
+ # term; each including calculator decides what it's multiplied by (per unit for service fees,
7
+ # once per payment for payment fees).
8
+ module PercentFeeCalculable
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ preference :percent, :decimal, default: 0
13
+ preference :flat_amount, :decimal, default: 0
14
+ end
15
+
16
+ def fee_description(currency:)
17
+ percent = "#{preferred_percent.to_d.round(2).to_s('F')}%"
18
+ return percent unless preferred_flat_amount.to_d.positive?
19
+
20
+ flat = Spree::Money.new(preferred_flat_amount, currency: currency).to_s
21
+ return flat if preferred_percent.to_d.zero?
22
+
23
+ Spree.t(:fee_percent_plus_flat, percent: percent, flat: flat)
24
+ end
25
+
26
+ private
27
+
28
+ # Nothing to charge on a zero base (a free line item, or store credit covering the order),
29
+ # so the flat part isn't added on its own there either.
30
+ def percent_plus_flat(base, flat)
31
+ return 0 unless base.to_d.positive?
32
+
33
+ (base * preferred_percent / 100) + flat
34
+ end
35
+ end
36
+ end
@@ -5,9 +5,13 @@ module SpreeCmCommissioner
5
5
  module ProductType
6
6
  extend ActiveSupport::Concern
7
7
 
8
- PRODUCT_TYPES = %i[accommodation service ecommerce transit show_episode vote_package advertisement].freeze
8
+ PRODUCT_TYPES = %i[accommodation service ecommerce transit show_episode vote_package advertisement prepaid_top_up].freeze
9
9
  PERMANENT_STOCK_PRODUCT_TYPES = %w[accommodation service transit].freeze
10
- PRE_INVENTORY_DAYS = { 'transit' => 90, 'accommodation' => 365, 'service' => 30 }.freeze
10
+ PRE_INVENTORY_DAYS = {
11
+ 'transit' => ENV.fetch('PRE_INVENTORY_DAYS_TRANSIT', 180).to_i,
12
+ 'accommodation' => ENV.fetch('PRE_INVENTORY_DAYS_ACCOMMODATION', 365).to_i,
13
+ 'service' => ENV.fetch('PRE_INVENTORY_DAYS_SERVICE', 180).to_i
14
+ }.freeze
11
15
 
12
16
  included do
13
17
  enum product_type: PRODUCT_TYPES if table_exists? && column_names.include?('product_type')