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
@@ -20,6 +20,14 @@ module SpreeCmCommissioner
20
20
  ticket_transfer_completed_notification
21
21
  ticket_transfer_rejected_notification
22
22
  ticket_transfer_expired_notification
23
+ reschedule_requested_notification
24
+ reschedule_approved_notification
25
+ reschedule_completed_notification
26
+ reschedule_rejected_notification
27
+ reschedule_cancelled_notification
28
+ reschedule_expired_notification
29
+ reschedule_failed_notification
30
+ reschedule_payment_reminder_notification
23
31
  ]
24
32
  )
25
33
  }
@@ -11,6 +11,7 @@ module SpreeCmCommissioner
11
11
  base.include SpreeCmCommissioner::OrderScopes
12
12
  base.include SpreeCmCommissioner::TicketTransferable
13
13
  base.include SpreeCmCommissioner::OrderQrCodeable
14
+ base.include SpreeCmCommissioner::OrderPrepaidPayment
14
15
 
15
16
  base.before_create :link_by_phone_number
16
17
  base.before_create :associate_customer
@@ -21,7 +22,7 @@ module SpreeCmCommissioner
21
22
  base.store_private_metadata :preload_main_trip_ids, :array, default: []
22
23
  base.store_public_metadata :order_options, :array, default: []
23
24
 
24
- base.money_methods :service_fee_total
25
+ base.money_methods :service_fee_total, :payment_fee_total
25
26
 
26
27
  base.validates :promo_total, base::MONEY_VALIDATION
27
28
  base.validates :phone_number, presence: true, if: :require_phone_number
@@ -40,6 +41,9 @@ module SpreeCmCommissioner
40
41
  base.has_many :vendors, through: :products, class_name: 'Spree::Vendor'
41
42
  base.has_many :taxons, through: :products, class_name: 'Spree::Taxon'
42
43
  base.has_many :guests, through: :line_items, class_name: 'SpreeCmCommissioner::Guest'
44
+ base.has_many :reschedule_requests,
45
+ class_name: 'SpreeCmCommissioner::RescheduleRequest',
46
+ foreign_key: :order_id
43
47
 
44
48
  base.has_many :saved_guests,
45
49
  -> { reorder(nil).distinct },
@@ -51,6 +55,7 @@ module SpreeCmCommissioner
51
55
  base.has_many :reserved_blocks, through: :guests, class_name: 'SpreeCmCommissioner::ReservedBlock'
52
56
  base.has_many :guest_card_classes, class_name: 'SpreeCmCommissioner::GuestCardClass', through: :variants
53
57
  base.has_many :product_completion_steps, class_name: 'SpreeCmCommissioner::ProductCompletionStep', through: :line_items
58
+ base.has_many :audit_events, class_name: 'SpreeCmCommissioner::AuditEvent', as: :auditable
54
59
 
55
60
  base.delegate :customer, to: :user, allow_nil: true
56
61
 
@@ -166,6 +171,17 @@ module SpreeCmCommissioner
166
171
  SpreeCmCommissioner::ServiceFeeRule.adjust(self, line_items)
167
172
  end
168
173
 
174
+ # A payment fee is only correct for the total it was computed on, so one left over from an
175
+ # earlier payment attempt is dropped whenever checkout steps re-run. Skipped once a gateway
176
+ # payment exists: its amount already includes the fee, and clearing it would make the order
177
+ # total smaller than what that payment charges.
178
+ def clear_stale_payment_fee!
179
+ return unless adjustments.payment_fee.exists?
180
+ return if payments.valid.any? { |payment| !payment.store_credit? }
181
+
182
+ SpreeCmCommissioner::PaymentFees::Clear.call(order: self)
183
+ end
184
+
169
185
  # overrided
170
186
  def payment_required?
171
187
  return false if need_confirmation?
@@ -221,12 +237,18 @@ module SpreeCmCommissioner
221
237
  def create_default_payment_if_eligble
222
238
  return unless subscription?
223
239
 
224
- if covered_by_store_credit
240
+ # `covered_by_store_credit` checks the total across the user's store-default-currency
241
+ # credit, so re-scope to this order's own store/currency here -- a user with credit in
242
+ # more than one currency (e.g. USD and KHR) must have the source credit match the order's
243
+ # currency, or Spree::StoreCredit#authorize rejects it as a currency_mismatch.
244
+ store_credit = covered_by_store_credit && user.store_credits.for_store(store).where(currency: currency).order_by_priority.first
245
+
246
+ if store_credit
225
247
  payment_method = Spree::PaymentMethod::StoreCredit.available_on_back_end.first_or_create! do |method|
226
248
  method.name ||= 'StoreCredit'
227
249
  method.stores = [Spree::Store.default] if method.stores.empty?
228
250
  end
229
- source_id = user.store_credit_ids.last
251
+ source_id = store_credit.id
230
252
  source_type = 'Spree::StoreCredit'
231
253
  else
232
254
  payment_method = Spree::PaymentMethod::Check.available_on_back_end.first_or_create! do |method|
@@ -247,6 +269,12 @@ module SpreeCmCommissioner
247
269
  subscription.present?
248
270
  end
249
271
 
272
+ # A top-up order carries only the virtual `prepaid_top_up` product -- never mixed with
273
+ # real products. See docs/concepts/store-credit-wallet-system.md Section 4.1.
274
+ def prepaid_top_up_order?
275
+ line_items.present? && products.all?(&:prepaid_top_up?)
276
+ end
277
+
250
278
  # Returns true when the order was created under a tenant context.
251
279
  def purchased_from_tenant?
252
280
  !tenant_id.nil?
@@ -12,10 +12,14 @@ module SpreeCmCommissioner
12
12
  def update_adjustment_total
13
13
  super
14
14
  order.service_fee_total = line_items.sum(:service_fee_total)
15
+
16
+ # Order-level adjustments (see PaymentFees::Apply) are already in adjustment_total via
17
+ # core's `adjustments.eligible.sum(:amount)`; this is the display-only breakdown.
18
+ order.payment_fee_total = adjustments.payment_fee.eligible.sum(:amount)
15
19
  end
16
20
 
17
21
  # Duplicates Spree::OrderUpdater#persist_totals' update_columns call (spree_core 4.5.3)
18
- # rather than calling super + a second update_columns for service_fee_total, to keep this
22
+ # rather than calling super + a second update_columns for the fee totals, to keep this
19
23
  # to a single UPDATE per order -- with read replicas/connection pooling in front of this,
20
24
  # a second round-trip per persist_totals call isn't free. Must be kept in sync if core ever
21
25
  # changes that column list.
@@ -33,6 +37,7 @@ module SpreeCmCommissioner
33
37
  promo_total: order.promo_total,
34
38
  total: order.total,
35
39
  service_fee_total: order.service_fee_total,
40
+ payment_fee_total: order.payment_fee_total,
36
41
  updated_at: Time.current
37
42
  )
38
43
  end
@@ -33,6 +33,22 @@ module SpreeCmCommissioner
33
33
  payment_method.is_a?(Spree::PaymentMethod::Check)
34
34
  end
35
35
 
36
+ # Reschedule linkage. CollectPayment stamps the owning request's id into
37
+ # private_metadata so the completed-payment hook, the expiry guard, and the
38
+ # stale-payment sweep can all find "the payment for this reschedule" without
39
+ # re-deriving the metadata key in each place.
40
+ def reschedule_request_id
41
+ private_metadata.to_h['reschedule_request_id']
42
+ end
43
+
44
+ def reschedule_request_id=(value)
45
+ self.private_metadata = private_metadata.to_h.merge('reschedule_request_id' => value)
46
+ end
47
+
48
+ def for_reschedule_request?(request)
49
+ reschedule_request_id.to_s == request.id.to_s
50
+ end
51
+
36
52
  # @override Spree::Payment#after_completed
37
53
  # Runs inside the transition to :completed, so completing the transfer stays
38
54
  # atomic with the payment. Payment completion is the single trigger for
@@ -40,6 +56,7 @@ module SpreeCmCommissioner
40
56
  def after_completed
41
57
  super
42
58
  order.complete_ticket_transfer_if_ready
59
+ execute_reschedule_if_completable
43
60
  order.run_order_complete_alerts
44
61
  end
45
62
 
@@ -56,6 +73,8 @@ module SpreeCmCommissioner
56
73
  private
57
74
 
58
75
  def lock_holds_for_payment
76
+ return if reschedule_request_id.present?
77
+
59
78
  order.lock_order_holds_for_payment
60
79
  end
61
80
 
@@ -67,6 +86,18 @@ module SpreeCmCommissioner
67
86
  .set(wait: delay)
68
87
  .perform_later(order_id: order.id)
69
88
  end
89
+
90
+ def execute_reschedule_if_completable
91
+ return unless completed?
92
+
93
+ request_id = reschedule_request_id
94
+ return if request_id.blank?
95
+
96
+ request = SpreeCmCommissioner::RescheduleRequest.find_by(id: request_id, state: 'pending_payment')
97
+ return if request.nil?
98
+
99
+ SpreeCmCommissioner::RescheduleRequests::ExecuteJob.perform_later(reschedule_request_id: request.id)
100
+ end
70
101
  end
71
102
  end
72
103
 
@@ -0,0 +1,97 @@
1
+ module SpreeCmCommissioner
2
+ class PaymentFeeRule < SpreeCmCommissioner::Base
3
+ include SpreeCmCommissioner::ScopedFeeRule
4
+ include Spree::AdjustmentSource
5
+ include Spree::CalculatedAdjustments
6
+
7
+ # Same reason as ServiceFeeRule: without class_name Rails looks for
8
+ # SpreeCmCommissioner::Adjustment.
9
+ has_many :adjustments, as: :source, class_name: 'Spree::Adjustment'
10
+
11
+ # optional: false spelled out because Spree turns off belongs_to_required_by_default; without
12
+ # it a blank admin form reaches the NOT NULL column and raises instead of showing a form error.
13
+ belongs_to :payment_method, class_name: 'Spree::PaymentMethod', optional: false
14
+
15
+ # One rule per (payment method, scope, currency), active or not: `active` is an on/off switch,
16
+ # not a way to keep old versions around. A duplicate is pointed at the existing rule instead
17
+ # (see #duplicate_rule and the admin form).
18
+ validates :currency,
19
+ uniqueness: {
20
+ scope: %i[payment_method_id ruleable_type ruleable_id tenant_id],
21
+ case_sensitive: false,
22
+ message: lambda { |object, _data|
23
+ I18n.t('payment_fee.rules.rule_already_exists_for_scope',
24
+ payment_method: object.payment_method&.name,
25
+ scope_type: object.ruleable_type_label,
26
+ currency: object.currency
27
+ )
28
+ }
29
+ }
30
+
31
+ scope :for_payment_method, -> (payment_method) { where(payment_method_id: payment_method) }
32
+
33
+ def duplicate_rule
34
+ self.class.where.not(id: id).find_by(
35
+ payment_method_id: payment_method_id, ruleable_type: ruleable_type, ruleable_id: ruleable_id,
36
+ tenant_id: tenant_id, currency: currency
37
+ )
38
+ end
39
+
40
+ # Caches on the payment method whether any active rule exists for it, at any scope, so the
41
+ # catalog can serialize fee_configured without a query. Coarser than "could reach this vendor"
42
+ # (a shared store method is flagged for every vendor once one vendor has a fee), but `false`
43
+ # still guarantees no fee, which is all the client relies on.
44
+ after_commit :sync_payment_method_fee_configured!
45
+
46
+ def self.resolve_for(line_item:, payment_method:, currency:)
47
+ resolve_scoped(line_item: line_item, currency: currency, relation: for_payment_method(payment_method))
48
+ end
49
+
50
+ def self.available_calculator_types
51
+ [SpreeCmCommissioner::Calculators::PaymentFeePercent, SpreeCmCommissioner::Calculators::PaymentFeeFlatAmount]
52
+ end
53
+
54
+ # Required by Spree::AdjustmentSource, but PaymentFees::Apply never goes through it: it stores
55
+ # the amounts PaymentFees::Preview computed, as closed adjustments, so this is only reached if
56
+ # something reopens one. Recomputing here would need the whole order's gateway split, so keep
57
+ # whatever was stored.
58
+ def compute_amount(adjustable)
59
+ adjustments.find_by(adjustable: adjustable)&.amount || 0
60
+ end
61
+
62
+ def fee_description
63
+ return Spree.t(:service_fee_rule_not_configured) if calculator.nil?
64
+ return Spree.t(:payment_fee_mode_none) if no_fee?
65
+
66
+ calculator.fee_description(currency: currency)
67
+ end
68
+
69
+ # An explicit zero rule: how a narrower scope opts out of a broader rule's fee.
70
+ def no_fee?
71
+ case calculator
72
+ when Calculators::PaymentFeePercent
73
+ calculator.preferred_percent.to_d.zero? && calculator.preferred_flat_amount.to_d.zero?
74
+ when Calculators::PaymentFeeFlatAmount
75
+ calculator.preferred_flat_amount.to_d.zero?
76
+ else
77
+ true
78
+ end
79
+ end
80
+
81
+ def label
82
+ Spree.t(:payment_fee)
83
+ end
84
+
85
+ private
86
+
87
+ # Also re-syncs the previous method when a rule moves to another one.
88
+ def sync_payment_method_fee_configured!
89
+ ids = [payment_method_id, payment_method_id_previously_was].compact.uniq
90
+
91
+ Spree::PaymentMethod.where(id: ids).find_each do |payment_method|
92
+ payment_method.preload_fee_configured = self.class.active_rules.exists?(payment_method_id: payment_method.id)
93
+ payment_method.save! if payment_method.changed?
94
+ end
95
+ end
96
+ end
97
+ end
@@ -7,6 +7,11 @@ module SpreeCmCommissioner
7
7
 
8
8
  base.whitelisted_ransackable_attributes = %w[name]
9
9
 
10
+ base.include SpreeCmCommissioner::StoreMetadata
11
+ # Kept in sync by PaymentFeeRule#sync_payment_method_fee_configured!, so the cached payment
12
+ # methods catalog can say whether to ask for fee previews without a rules query per request.
13
+ base.store_private_metadata :preload_fee_configured, :boolean, default: false
14
+
10
15
  # Expose a dedicated class helper so services can call Spree::PaymentMethod.ticket_transfer
11
16
  # instead of duplicating lookup/creation logic.
12
17
  base.define_singleton_method(:ticket_transfer) do
@@ -0,0 +1,81 @@
1
+ module SpreeCmCommissioner
2
+ module PaymentMethodStoreCreditDecorator
3
+ # Forced false, not left as a per-store admin toggle: OrderStateMachine#
4
+ # capture_authorized_store_credit_payments! is the only thing that ever captures a store-credit
5
+ # payment, deferring it until stock is confirmed cut inside the same DB transaction as order
6
+ # completion (see docs/concepts/store-credit-wallet-system.md Section 5). If a store's own
7
+ # PaymentMethod::StoreCredit#auto_capture column were left/set true, process_payments! would
8
+ # capture (spend) the wallet balance immediately, before stock is cut -- reintroducing the
9
+ # "captured, then stock fails, needs a manual refund" case this design exists to avoid.
10
+ def auto_capture?
11
+ false
12
+ end
13
+
14
+ # Reuses the payment's own pre-generated response_code as this authorize event's
15
+ # authorization_code, instead of letting Spree::StoreCredit#authorize mint a fresh one.
16
+ # Spree::StoreCreditEvent#order (core) resolves by matching authorization_code back to
17
+ # Payment#response_code -- if authorize generates its own code, that match fails until
18
+ # Payment::Processing#handle_response updates response_code afterward, which happens too
19
+ # late for StoreCreditEventDecorator's after_create ledger mirror (PrepaidTransaction#order
20
+ # comes back nil). Reusing the code the payment already carries closes that gap
21
+ # unconditionally, including for an authorization that's later voided or never captured --
22
+ # exactly the case where a customer would most want their wallet history to still point back
23
+ # at the order.
24
+ #
25
+ # started_processing! (Payment::Processing#process_authorization) already stamps state:
26
+ # 'processing' before this method is ever reached, and store_credit.with_lock (inside
27
+ # handle_action_call, below) serializes concurrent authorize attempts on the same
28
+ # store_credit -- so at most one payment can genuinely be 'processing' against this source
29
+ # at a time.
30
+ #
31
+ # Duplicates core's Spree::PaymentMethod::StoreCredit#authorize body (rather than calling
32
+ # super) because the only way to change what's passed to Spree::StoreCredit#authorize is to
33
+ # rebuild the lambda that calls it -- core exposes no hook to inject an extra keyword arg.
34
+ def authorize(amount_in_cents, store_credit, gateway_options = {})
35
+ return super if store_credit.nil?
36
+
37
+ payment = Spree::Payment.find_by(source: store_credit, payment_method: self, state: 'processing')
38
+
39
+ action = lambda do |sc|
40
+ sc.authorize(
41
+ amount_in_cents / 100.0.to_d,
42
+ gateway_options[:currency],
43
+ action_originator: gateway_options[:originator],
44
+ action_authorization_code: payment&.response_code
45
+ )
46
+ end
47
+ handle_action_call(store_credit, action, :authorize)
48
+ end
49
+
50
+ # Core's own #cancel (called by Spree::Order#after_cancel -> Payment#cancel! for every
51
+ # *completed* payment on a canceled order -- this is the path that reverses a captured
52
+ # store-credit payment when the order itself gets canceled, not the top-up wallet reversal in
53
+ # Prepaid::ReverseOrderTopUp, which is a separate ledger entirely) calls `store_credit.credit`
54
+ # with zero options -- no gateway_options, no originator -- so the mirrored PrepaidTransaction
55
+ # this produces (StoreCreditEventDecorator#mirror_to_prepaid_ledger) always came out with a
56
+ # blank Note, with no way to tell it apart from a manual admin credit after the fact.
57
+ #
58
+ # Passing the payment itself as originator gives mirrored_reason something concrete to point
59
+ # at (the order), even though this is a genuine Spree::Payment, not a Spree::Refund -- core
60
+ # only ever reads `originator` back through polymorphic association, it never assumes a type.
61
+ #
62
+ # Duplicates core's #cancel body (rather than calling super) for the same reason #authorize
63
+ # above does: the only way to add an option to the store_credit.credit call is to rebuild the
64
+ # lambda that makes it.
65
+ def cancel(auth_code, payment = nil)
66
+ store_credit_event = Spree::StoreCreditEvent.find_by(authorization_code: auth_code, action: Spree::StoreCredit::CAPTURE_ACTION)
67
+ store_credit = store_credit_event.try(:store_credit)
68
+
69
+ return super if !store_credit_event || !store_credit
70
+
71
+ action = lambda do |sc|
72
+ sc.credit(store_credit_event.amount, auth_code, sc.currency, action_originator: payment)
73
+ end
74
+ handle_action(action, :cancel, auth_code)
75
+ end
76
+ end
77
+ end
78
+
79
+ unless Spree::PaymentMethod::StoreCredit.included_modules.include?(SpreeCmCommissioner::PaymentMethodStoreCreditDecorator)
80
+ Spree::PaymentMethod::StoreCredit.prepend SpreeCmCommissioner::PaymentMethodStoreCreditDecorator
81
+ end
@@ -1,5 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  class Place < Base
3
+ include SpreeCmCommissioner::Integrations::IntegrationMappable
4
+
3
5
  acts_as_nested_set
4
6
 
5
7
  validates :reference, presence: true, if: :validate_reference?
@@ -0,0 +1,59 @@
1
+ module SpreeCmCommissioner
2
+ # Presenter-shaped value object for GET prepaid/config -- reads the same platform-wide
3
+ # Spree::Store.default currency settings that Prepaid::TopUp already enforces
4
+ # (docs/concepts/store-credit-wallet-system.md Section 8), plus the fraud/velocity limits from
5
+ # SpreeCmCommissioner::Configuration. `id` is a fixed string since this isn't a database-backed
6
+ # resource; jsonapi-serializer requires one.
7
+ #
8
+ # supported_currencies/preferred_currency/currency_limits ARE tenant-aware, though: pass
9
+ # `tenant:` (from Spree::Api::V2::Tenant::BaseController#require_tenant) to resolve
10
+ # Tenant#supported_currencies_list/#preferred_currency_or_default instead of the platform
11
+ # ceiling -- these are the exact same values POST draft_orders/topup and GET account/prepaid
12
+ # already validate/default `?currency=` against (Tenant::BaseController#
13
+ # resolve_requested_currency), just finally made discoverable by the client instead of only
14
+ # enforced after the fact on submit.
15
+ PrepaidConfig = Struct.new(
16
+ :id, :daily_topup_count, :supported_currencies, :preferred_currency, :currency_limits,
17
+ keyword_init: true
18
+ ) do
19
+ def self.resolve(tenant: nil)
20
+ config = SpreeCmCommissioner::Configuration.new
21
+ store = Spree::Store.default
22
+ preferred_currency = tenant ? tenant.preferred_currency_or_default : store.default_currency
23
+ supported_currencies = order_preferred_first(
24
+ tenant ? tenant.supported_currencies_list : store.supported_currency_codes, preferred_currency
25
+ )
26
+
27
+ new(
28
+ id: 'prepaid_config',
29
+ daily_topup_count: config.prepaid_max_topup_count_daily,
30
+ supported_currencies: supported_currencies,
31
+ preferred_currency: preferred_currency,
32
+ currency_limits: build_currency_limits(config, supported_currencies)
33
+ )
34
+ end
35
+
36
+ # Preferred currency first, so the client's currency picker (and the currency_limits hash
37
+ # key order below, which Ruby/JSON preserves) both default to it without the client having
38
+ # to know which entry is preferred and reorder it themselves.
39
+ def self.order_preferred_first(currencies, preferred_currency)
40
+ currencies.sort_by { |currency| currency == preferred_currency ? 0 : 1 }
41
+ end
42
+
43
+ # Scales the base limits into each supported currency via Configuration#prepaid_currency_rates.
44
+ # A currency missing a configured rate is omitted rather than shown unconverted.
45
+ def self.build_currency_limits(config, supported_currencies)
46
+ supported_currencies.each_with_object({}) do |currency, limits|
47
+ rate = config.prepaid_currency_rates[currency]
48
+ next unless rate
49
+
50
+ limits[currency] = {
51
+ min_topup_amount: (config.prepaid_topup_min_amount * rate).round(2),
52
+ max_topup_amount: (config.prepaid_topup_max_amount * rate).round(2),
53
+ daily_topup_amount: (config.prepaid_daily_topup_limit * rate).round(2),
54
+ max_balance: (config.prepaid_max_balance * rate).round(2)
55
+ }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,102 @@
1
+ module SpreeCmCommissioner
2
+ class PrepaidTransaction < Base
3
+ include SpreeCmCommissioner::StoreMetadata
4
+
5
+ TRANSACTION_TYPES = %w[
6
+ top_up admin_credit authorize capture void credit reverse_top_up freeze unfreeze
7
+ ].freeze
8
+
9
+ STATUSES = %w[pending completed failed reversed].freeze
10
+
11
+ # English fallback label for every TRANSACTION_TYPES value -- covers capture/reverse_top_up
12
+ # too, even though neither is customer-facing (see CUSTOMER_FACING_TYPES below), because the
13
+ # admin dashboard/per-user transactions tabs show every type with no allowlist filter; without
14
+ # an entry here those rows would display the raw, un-friendly type string instead of a label.
15
+ DISPLAY_TYPES = {
16
+ 'top_up' => 'Top Up',
17
+ 'admin_credit' => 'Admin Credit',
18
+ 'authorize' => 'Purchase',
19
+ 'capture' => 'Capture',
20
+ 'void' => 'Canceled',
21
+ 'credit' => 'Refund',
22
+ 'reverse_top_up' => 'Reversal',
23
+ 'freeze' => 'Freeze',
24
+ 'unfreeze' => 'Unfreeze'
25
+ }.freeze
26
+
27
+ # Which types ever reach the customer-facing storefront/tenant API (see
28
+ # PrepaidTransactionsController::DISPLAYED_TRANSACTION_TYPES). Excludes `capture` (always
29
+ # amount: 0, a bookkeeping-only row that never changes the balance) and `freeze`/`unfreeze`
30
+ # (also amount: 0 -- a status change, not a transaction; nothing useful to show in a money
31
+ # list, and no client currently renders them).
32
+ #
33
+ # reverse_top_up IS included, even though it's admin-initiated: Prepaid::ReverseTopUp
34
+ # actually reduces the credit's real amount, so hiding it would leave an admin_credit/top_up
35
+ # row permanently visible with no visible transaction explaining why that money is gone --
36
+ # exactly the "I see a transaction but the balance doesn't match" confusion this list exists
37
+ # to prevent. Kept as its own list rather than derived from DISPLAY_TYPES.keys, so expanding
38
+ # DISPLAY_TYPES for the admin UI's sake can't silently widen what customers are shown.
39
+ CUSTOMER_FACING_TYPES = %w[top_up admin_credit authorize void credit reverse_top_up].freeze
40
+
41
+ belongs_to :user, class_name: 'Spree::User'
42
+ belongs_to :store_credit, class_name: 'Spree::StoreCredit'
43
+ belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant', optional: true
44
+ belongs_to :order, class_name: 'Spree::Order', optional: true
45
+ # The admin who performed an admin-initiated action (admin_credit, reverse_top_up, freeze,
46
+ # unfreeze) -- a real column, not a metadata key, since "who did this" is audit-critical,
47
+ # queryable identity data (see the migration adding this column for the full reasoning).
48
+ # nil for non-admin-initiated types (top_up, authorize, capture, void, credit).
49
+ belongs_to :admin, class_name: 'Spree::User', optional: true
50
+
51
+ # Internal/audit detail that's never exposed to a customer via an API serializer -- freeze
52
+ # metadata isn't identity data (unlike admin_id above) and doesn't need to be queried on, so
53
+ # it doesn't need its own column. requested_amount/reversed_amount (reverse_top_up) are
54
+ # written directly into private_metadata without a typed accessor below -- StoreMetadata has
55
+ # no :decimal type, and these are money amounts where losing precision through a lossy type
56
+ # cast isn't acceptable.
57
+ store_private_metadata :reason, :text
58
+ store_private_metadata :level, :string
59
+ store_private_metadata :until_at, :datetime
60
+ store_private_metadata :prior_frozen_by_id, :integer
61
+ store_private_metadata :prior_freeze_reason, :text
62
+ store_private_metadata :prior_freeze_level, :string
63
+
64
+ # Spree::Money-formatted display_amount/display_balance_after, same display_* convention as
65
+ # products/orders/store credits (Spree::DisplayMoney). Negative on authorize rows (a hold) --
66
+ # Spree::Money formats the sign as-is, no special-casing needed here.
67
+ extend Spree::DisplayMoney
68
+ money_methods :amount, :balance_after
69
+
70
+ validates :transaction_type, presence: true
71
+ validates :amount, :balance_after, numericality: true
72
+ validates :currency, presence: true
73
+ validates :idempotency_key, presence: true
74
+ validates :status, inclusion: { in: STATUSES }
75
+
76
+ scope :for_tenant, -> (tenant_id) { where(tenant_id: tenant_id) }
77
+ scope :completed, -> { where(status: 'completed') }
78
+
79
+ def self.ransackable_attributes(_auth_object = nil)
80
+ %w[transaction_type status currency tenant_id user_id store_credit_id admin_id created_at]
81
+ end
82
+
83
+ # user/order let the admin wallet transactions search filter by user_email_cont/
84
+ # order_number_cont without exposing every association on the model.
85
+ self.whitelisted_ransackable_associations = %w[user order]
86
+
87
+ def readonly?
88
+ persisted? && status_was == 'completed'
89
+ end
90
+
91
+ # Client-facing label for `transaction_type` ("Top Up" / "Purchase"), localized to the
92
+ # current request's locale -- DISPLAY_TYPES only supplies the English fallback; the actual
93
+ # string comes from locale files, so a Khmer-speaking client doesn't get English baked into
94
+ # the API response. See CUSTOMER_FACING_TYPES for which types customers are ever shown.
95
+ def display_type
96
+ I18n.t(
97
+ "spree_cm_commissioner.prepaid_transaction.display_type.#{transaction_type}",
98
+ default: DISPLAY_TYPES.fetch(transaction_type, transaction_type)
99
+ )
100
+ end
101
+ end
102
+ end
@@ -37,6 +37,12 @@ module SpreeCmCommissioner
37
37
  base.has_one :default_state, through: :vendor
38
38
  base.has_one :google_wallet, class_name: 'SpreeCmCommissioner::GoogleWallet', dependent: :destroy
39
39
 
40
+ base.has_one :reschedule_policy_assignment, as: :policyable,
41
+ class_name: 'SpreeCmCommissioner::ReschedulePolicyAssignment',
42
+ dependent: :destroy
43
+ base.has_one :reschedule_policy, through: :reschedule_policy_assignment,
44
+ class_name: 'SpreeCmCommissioner::ReschedulePolicy'
45
+
40
46
  base.has_many :complete_line_items, through: :classifications, source: :line_items
41
47
  base.has_many :inventory_items, through: :variants
42
48
  base.has_many :guests, through: :line_items
@@ -49,13 +49,15 @@ module SpreeCmCommissioner
49
49
  quantity_locked_on_hold: cache_inventory(locked_hold_keys[index], item, item.quantity_locked_on_hold, locked_hold[index]),
50
50
  max_capacity: item.max_capacity,
51
51
  inventory_item_id: item.id,
52
- variant_id: item.variant_id
52
+ variant_id: item.variant_id,
53
+ closed: item.closed
53
54
  )
54
55
  end
55
56
 
56
57
  def cache_inventory(key, inventory_item, db_value, count_in_redis)
58
+ # Past dates are warmed too: InventoryItem#adjust_quantity! relies on this to seed the key before
59
+ # applying its delta, and an admin can still edit a past date's stock.
57
60
  return count_in_redis.to_i if count_in_redis.present?
58
- return db_value unless inventory_item.active?
59
61
 
60
62
  # Use atomic SET NX to prevent race condition where multiple concurrent reads
61
63
  # initialize cache with stale values. Only the first thread wins.