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,30 @@
1
+ module SpreeCmCommissioner
2
+ module PaymentFees
3
+ # Replaces the order's payment fee with what PaymentFees::Preview computes for
4
+ # `payment_method`, storing those exact amounts (same pattern as PricingModels::Activate):
5
+ # never recomputed through PaymentFeeRule#compute_amount, and closed so a later
6
+ # Adjustment#update! can't change them. Caller is expected to hold the transaction.
7
+ class Apply
8
+ prepend ::Spree::ServiceModule::Base
9
+
10
+ def call(order:, payment_method:)
11
+ order.adjustments.payment_fee.delete_all
12
+
13
+ preview = Preview.call(order: order, payment_method: payment_method).value[:payment_fee_preview]
14
+ preview.adjustments.each do |adjustment|
15
+ order.adjustments.create!(
16
+ amount: adjustment.amount,
17
+ source: adjustment.rule,
18
+ order: order,
19
+ label: adjustment.rule.label,
20
+ mandatory: true,
21
+ state: 'closed'
22
+ )
23
+ end
24
+
25
+ order.update_with_updater!
26
+ success(order: order, payment_fee_preview: preview)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ module SpreeCmCommissioner
2
+ module PaymentFees
3
+ class Clear
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(order:)
7
+ return success(order: order) unless order.adjustments.payment_fee.exists?
8
+
9
+ order.adjustments.payment_fee.delete_all
10
+ order.update_with_updater!
11
+ success(order: order)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,70 @@
1
+ module SpreeCmCommissioner
2
+ module PaymentFees
3
+ # Computes, without persisting, the payment fee `payment_method` would add to `order`.
4
+ #
5
+ # The base is the gateway amount only (order total minus store credit, minus any payment fee
6
+ # already applied from an earlier attempt), since that's what the provider bills on. Line items
7
+ # resolve their own rule (product -> event -> vendor -> tenant/global); when they resolve to
8
+ # different rules, the gateway amount is split between rules by each group's share of line-item
9
+ # totals, and the leftover rounding cent goes to the last group so the bases always add up.
10
+ class Preview
11
+ prepend ::Spree::ServiceModule::Base
12
+
13
+ def call(order:, payment_method:)
14
+ gateway_amount = gateway_amount_for(order)
15
+ adjustments = build_adjustments(order, payment_method, gateway_amount)
16
+ amount = adjustments.sum(BigDecimal(0), &:amount)
17
+
18
+ success(
19
+ payment_fee_preview: PaymentFeePreview.new(
20
+ payment_method_id: payment_method.id,
21
+ currency: order.currency,
22
+ amount: amount,
23
+ order_total_after_store_credit: gateway_amount + amount,
24
+ fee_description: adjustments.one? ? adjustments.first.rule.fee_description : nil,
25
+ adjustments: adjustments
26
+ )
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def gateway_amount_for(order)
33
+ [order.order_total_after_store_credit - order.payment_fee_total, 0].max
34
+ end
35
+
36
+ def build_adjustments(order, payment_method, gateway_amount)
37
+ return [] unless gateway_amount.positive?
38
+
39
+ groups = order.line_items.group_by do |line_item|
40
+ PaymentFeeRule.resolve_for(line_item: line_item, payment_method: payment_method, currency: order.currency)
41
+ end
42
+ groups.delete(nil)
43
+ return [] if groups.empty?
44
+
45
+ bases = split(gateway_amount, groups, order)
46
+ groups.keys.zip(bases).filter_map do |rule, base|
47
+ amount = round(rule.calculator&.compute(base) || 0, order)
48
+ PaymentFeePreview::Adjustment.new(rule: rule, amount: amount) if amount.positive?
49
+ end
50
+ end
51
+
52
+ # Items with no rule still pay their share of the gateway amount, just without a fee, so
53
+ # weights are taken over every line item, not only the ruled ones.
54
+ def split(gateway_amount, groups, order)
55
+ all_items_total = order.line_items.sum(&:total)
56
+ return [gateway_amount] + Array.new(groups.size - 1, 0) unless all_items_total.positive?
57
+
58
+ bases = groups.values.map { |items| round(gateway_amount * items.sum(&:total) / all_items_total, order) }
59
+ return bases if groups.size < order.line_items.size
60
+
61
+ bases[-1] += gateway_amount - bases.sum
62
+ bases
63
+ end
64
+
65
+ def round(amount, order)
66
+ amount.to_d.round(::Money::Currency.new(order.currency).exponent)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,18 @@
1
+ module SpreeCmCommissioner
2
+ module Payments
3
+ module FindOrCreateDecorator
4
+ # override: applies the selected method's payment fee before the payment is built, so
5
+ # `amount: order.order_total_after_store_credit` already includes it. Runs inside
6
+ # FindOrCreate's transaction, so a failed payment save rolls the fee back with it.
7
+ def find_or_create_payment(order:, params:, payment_method:)
8
+ SpreeCmCommissioner::PaymentFees::Apply.call(order: order, payment_method: payment_method)
9
+
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ unless Vpago::Payments::FindOrCreate.included_modules.include?(SpreeCmCommissioner::Payments::FindOrCreateDecorator)
17
+ Vpago::Payments::FindOrCreate.prepend(SpreeCmCommissioner::Payments::FindOrCreateDecorator)
18
+ end
@@ -0,0 +1,95 @@
1
+ module SpreeCmCommissioner
2
+ # PlaceResolver finds or builds a SpreeCmCommissioner::Place keyed on a stable
3
+ # upstream identifier instead of the fragile name lookup the polling services
4
+ # used to do (`Place.find_by(name:)`).
5
+ #
6
+ # Lookup order:
7
+ # 1. IntegrationMapping for (integration, external_id) on Place — authoritative
8
+ # 2. Legacy backfill: Place.find_by(name:) — adopts pre-mapping rows on first re-sync
9
+ # 3. Build a new Place
10
+ #
11
+ # The resolver persists the Place and its IntegrationMapping. Callers then
12
+ # wire the Place to a VendorPlace (which has its own mapping).
13
+ #
14
+ # `Place#reference` is left to the Google Places geocoder — it's the
15
+ # canonical Google place_id used by admin tooling and vendor place selection.
16
+ class PlaceResolver
17
+ def initialize(integration:, sync_result: nil, geocoder: nil)
18
+ @integration = integration
19
+ @sync_result = sync_result || SpreeCmCommissioner::Integrations::Base::SyncResult.new
20
+ @geocoder = geocoder || SpreeCmCommissioner::GoogleMapsGeocoder.new
21
+ end
22
+
23
+ # @param external_id [String] stable upstream id, e.g. "location-1", "stop-3", "branch-65"
24
+ # @param name [String] display name
25
+ # @param code [String, nil] short code
26
+ # @param lat [Numeric, nil] upstream latitude when available (preferred over geocoding)
27
+ # @param lon [Numeric, nil] upstream longitude when available
28
+ # @param geocode_context [String, nil] free-form context for geocoder fallback (e.g. country name)
29
+ # @return [SpreeCmCommissioner::Place]
30
+ def resolve(external_id:, name:, code: nil, lat: nil, lon: nil, geocode_context: nil) # rubocop:disable Metrics/ParameterLists
31
+ mapping = SpreeCmCommissioner::Place.find_or_initialize_integration_mapping(
32
+ integration_id: @integration.id,
33
+ external_id: external_id
34
+ )
35
+
36
+ place = resolve_place(mapping, name: name)
37
+ ensure_coordinates!(place, lat: lat, lon: lon, geocode_context: geocode_context, geocode_query: name)
38
+
39
+ @sync_result.track(:place, place) do |tracker|
40
+ attrs = { name: name }
41
+ attrs[:code] = code if code.present?
42
+ place.assign_attributes(attrs)
43
+ tracker.save_if_changed!(place)
44
+ end
45
+
46
+ finalize_mapping!(mapping, place: place, name: name)
47
+ place
48
+ end
49
+
50
+ private
51
+
52
+ # Pick the Place this mapping should point at: existing internal, legacy adopted, or new.
53
+ # Adoption-by-name only adopts a Place that does NOT already have an
54
+ # IntegrationMapping for this integration — otherwise two upstream ids with
55
+ # the same display name would both adopt the same Place row.
56
+ def resolve_place(mapping, name:)
57
+ return mapping.internal if mapping.internal&.persisted?
58
+
59
+ legacy = SpreeCmCommissioner::Place.find_by(name: name)
60
+ if legacy && !already_mapped?(legacy)
61
+ mapping.internal = legacy
62
+ legacy
63
+ else
64
+ mapping.internal
65
+ end
66
+ end
67
+
68
+ def already_mapped?(place)
69
+ SpreeCmCommissioner::IntegrationMapping.exists?(
70
+ integration_id: @integration.id,
71
+ internal_type: 'SpreeCmCommissioner::Place',
72
+ internal_id: place.id
73
+ )
74
+ end
75
+
76
+ def ensure_coordinates!(place, lat:, lon:, geocode_context:, geocode_query:) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
77
+ place.lat ||= lat if lat.present?
78
+ place.lon ||= lon if lon.present?
79
+
80
+ return if place.lat.present? && place.lon.present? && place.reference.present?
81
+ # Only geocode for brand-new Places. Persisted rows keep whatever coords they already have.
82
+ return unless place.new_record?
83
+
84
+ location_data = @geocoder.fetch_location_data(geocode_query, geocode_context) || {}
85
+ place.lat = location_data[:lat] if place.lat.blank?
86
+ place.lon = location_data[:lon] if place.lon.blank?
87
+ place.reference = location_data[:reference] if place.reference.blank?
88
+ end
89
+
90
+ def finalize_mapping!(mapping, place:, name:)
91
+ mapping.internal = place if mapping.internal != place
92
+ mapping.mark_as_active!(external_payload: { name: name })
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,44 @@
1
+ module SpreeCmCommissioner
2
+ module Prepaid
3
+ # Issues a goodwill/support credit to a user's prepaid balance. Distinct from TopUp only
4
+ # in who initiates it (an admin, not a payment) and the ledger ends up storing `reason`.
5
+ class AdminCredit
6
+ prepend ::Spree::ServiceModule::Base
7
+ extend SpreeCmCommissioner::ServiceModuleThrowable
8
+ include SpreeCmCommissioner::Prepaid::Mutation
9
+
10
+ # rubocop:disable Metrics/ParameterLists
11
+ def call(user:, amount:, currency:, idempotency_key:, reason:, issuer_tenant_id: nil, admin: nil, private_metadata: {})
12
+ return failure(nil, 'amount must be positive') unless amount.to_d.positive?
13
+ return failure(nil, 'unsupported currency') unless Spree::Store.default.supported_currency_codes.include?(currency)
14
+ return failure(nil, 'reason is required') if reason.blank?
15
+ return failure(nil, 'admin is required') if admin.nil?
16
+
17
+ store_credit = run_serializable do
18
+ credit = find_or_create_prepaid_credit!(
19
+ user: user, currency: currency, issuer_tenant_id: issuer_tenant_id,
20
+ created_by: admin, initial_amount: amount.to_d
21
+ )
22
+ check_not_topup_blocked!(credit)
23
+
24
+ credit.update!(amount: credit.amount + amount.to_d) unless credit.previously_new_record?
25
+ write_ledger!(
26
+ store_credit: credit,
27
+ transaction_type: 'admin_credit',
28
+ amount: amount.to_d,
29
+ idempotency_key: idempotency_key,
30
+ tenant_id: issuer_tenant_id,
31
+ admin: admin,
32
+ private_metadata: private_metadata.merge(reason: reason)
33
+ )
34
+ credit
35
+ end
36
+
37
+ success(store_credit)
38
+ rescue SpreeCmCommissioner::Prepaid::DuplicateTransaction, SpreeCmCommissioner::Prepaid::PrepaidFrozen => e
39
+ failure(nil, e.message)
40
+ end
41
+ # rubocop:enable Metrics/ParameterLists
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,47 @@
1
+ module SpreeCmCommissioner
2
+ module Prepaid
3
+ # Enforces top-up velocity/daily limits (hourly count, daily count, daily amount) --
4
+ # extracted from TopUp so each limit is independently unit-testable instead of only
5
+ # reachable through the full money-in transaction.
6
+ # docs/concepts/store-credit-wallet-system.md Section 8 -- platform-wide for now (the doc's
7
+ # per-tenant PrepaidConfigResolver was never built). A limit of 0 means "no limit", same
8
+ # convention the doc uses. Only user-scoped checks; the doc's IP-based "5 distinct users/hour
9
+ # from one IP" check isn't implemented here -- this has no request context to check it from.
10
+ class CheckTopUpLimits
11
+ prepend ::Spree::ServiceModule::Base
12
+ extend SpreeCmCommissioner::ServiceModuleThrowable
13
+
14
+ def call(user:, amount:, currency:)
15
+ config = SpreeCmCommissioner::Configuration.new
16
+ completed_topups = SpreeCmCommissioner::PrepaidTransaction.completed.where(user: user, transaction_type: 'top_up')
17
+
18
+ # Count-based checks (velocity/fraud signal, not money) deliberately stay currency-agnostic --
19
+ # 5 top-ups split across USD and KHR is still 5 top-ups.
20
+ hourly_limit = config.prepaid_max_topups_per_hour.to_i
21
+ if hourly_limit.positive? && completed_topups.where(created_at: 1.hour.ago..).count >= hourly_limit
22
+ return failure(nil, "too many top-ups in the last hour (max #{hourly_limit})")
23
+ end
24
+
25
+ todays_topups = completed_topups.where(created_at: Time.current.beginning_of_day..)
26
+
27
+ daily_count_limit = config.prepaid_max_topup_count_daily.to_i
28
+ if daily_count_limit.positive? && todays_topups.count >= daily_count_limit
29
+ return failure(nil, "daily top-up count limit reached (max #{daily_count_limit})")
30
+ end
31
+
32
+ # Amount-based check must be scoped to this currency and the limit scaled into it (same
33
+ # rate PrepaidConfig#build_currency_limits shows the client) -- KHR amounts run ~4100x
34
+ # USD's, so summing across currencies or comparing against the raw USD-denominated
35
+ # config value would reject (or wrongly allow) almost every non-preferred-currency top-up.
36
+ rate = config.prepaid_currency_rates[currency].to_f
37
+ daily_amount_limit = config.prepaid_daily_topup_limit.to_d * rate
38
+ todays_amount = todays_topups.where(currency: currency).sum(:amount)
39
+ if daily_amount_limit.positive? && todays_amount + amount > daily_amount_limit
40
+ return failure(nil, "daily top-up amount limit exceeded (max #{daily_amount_limit})")
41
+ end
42
+
43
+ success(nil)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,58 @@
1
+ module SpreeCmCommissioner
2
+ module Prepaid
3
+ # Freezes a store credit so PrepaidService money-in operations (and, at 'full' level,
4
+ # Spree's own spend path via StoreCreditDecorator#spend_blocked?) are blocked.
5
+ class Freeze
6
+ prepend ::Spree::ServiceModule::Base
7
+ extend SpreeCmCommissioner::ServiceModuleThrowable
8
+ include Mutation
9
+
10
+ LEVELS = %w[full topup_only].freeze
11
+
12
+ def call(store_credit:, admin:, reason:, level: 'full', until_at: nil)
13
+ return failure(nil, 'reason is required') if reason.blank?
14
+ return failure(nil, 'invalid freeze level') unless LEVELS.include?(level)
15
+
16
+ # Locked and transactional so a freeze can't interleave with a concurrent money-in
17
+ # operation (AdminCredit/TopUp lock the same row via Mutation#find_or_create_prepaid_credit!)
18
+ # -- otherwise a top-up could land in the gap between reading and writing the freeze columns.
19
+ store_credit = run_serializable do
20
+ credit = Spree::StoreCredit.lock.find(store_credit.id)
21
+
22
+ # A re-freeze (credit was already frozen, e.g. reason/level changed before ever being
23
+ # lifted) would otherwise overwrite frozen_at/freeze_reason/freeze_level with no trace
24
+ # of what the prior freeze was -- capture it here, same as Unfreeze does when a freeze
25
+ # is lifted.
26
+ write_ledger!(
27
+ store_credit: credit,
28
+ transaction_type: 'freeze',
29
+ amount: 0,
30
+ idempotency_key: "freeze:#{credit.id}:#{Time.current.to_i / 5}",
31
+ admin: admin,
32
+ private_metadata: {
33
+ reason: reason,
34
+ level: level,
35
+ until_at: until_at,
36
+ prior_frozen_by_id: credit.frozen_by_id,
37
+ prior_freeze_reason: credit.freeze_reason,
38
+ prior_freeze_level: credit.freeze_level
39
+ }
40
+ )
41
+
42
+ credit.update!(
43
+ frozen_at: Time.current,
44
+ frozen_by: admin,
45
+ freeze_reason: reason,
46
+ freeze_level: level,
47
+ frozen_until: until_at
48
+ )
49
+ credit
50
+ end
51
+
52
+ success(store_credit)
53
+ rescue SpreeCmCommissioner::Prepaid::DuplicateTransaction => e
54
+ failure(nil, e.message)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,78 @@
1
+ module SpreeCmCommissioner
2
+ module Prepaid
3
+ # Raised when a caller-supplied idempotency_key has already been used for a completed
4
+ # ledger row. Callers (jobs, other services) should rescue this and treat it as a no-op.
5
+ DuplicateTransaction = Class.new(StandardError)
6
+
7
+ # Raised when a money-in operation is attempted against frozen store credit.
8
+ PrepaidFrozen = Class.new(StandardError)
9
+
10
+ # Shared plumbing for the money-in services (TopUp, AdminCredit, ReverseTopUp): opening
11
+ # a serializable transaction, finding/creating the per-(user, currency, issuer_tenant_id)
12
+ # prepaid credit under a row lock, and writing the ledger row that mirrors the mutation.
13
+ # Not a service object itself -- included into the services that are.
14
+ module Mutation
15
+ def run_serializable(&block)
16
+ SpreeCmCommissioner::SerializableTransaction.run(&block)
17
+ end
18
+
19
+ # Spree::StoreCredit requires `amount > 0`, so a brand-new credit is created with
20
+ # `initial_amount` already applied rather than created at 0 and then topped up --
21
+ # callers must only add `amount` on top for a credit that already existed.
22
+ def find_or_create_prepaid_credit!(user:, currency:, issuer_tenant_id:, created_by:, initial_amount:)
23
+ credit_type = Spree::StoreCreditType.prepaid!
24
+
25
+ credit = Spree::StoreCredit.lock.find_by(
26
+ user_id: user.id,
27
+ currency: currency,
28
+ issuer_tenant_id: issuer_tenant_id,
29
+ type_id: credit_type.id
30
+ )
31
+ return credit if credit
32
+
33
+ Spree::StoreCredit.create!(
34
+ user: user,
35
+ currency: currency,
36
+ issuer_tenant_id: issuer_tenant_id,
37
+ credit_type: credit_type,
38
+ category: Spree::StoreCreditCategory.wallet!,
39
+ created_by: created_by,
40
+ amount: initial_amount,
41
+ store: Spree::Store.default
42
+ )
43
+ end
44
+
45
+ # rubocop:disable Metrics/ParameterLists
46
+ # admin is a real column (who performed an admin-initiated action), separate from
47
+ # private_metadata (freeform detail) -- see PrepaidTransaction#admin/store_private_metadata
48
+ # declarations for why. Named private_metadata, not the ambiguous bare `metadata`, since
49
+ # PrepaidTransaction also has a separate public_metadata column this never writes to.
50
+ def write_ledger!(store_credit:, transaction_type:, amount:, idempotency_key:, tenant_id: nil, order: nil, admin: nil, private_metadata: {})
51
+ SpreeCmCommissioner::PrepaidTransaction.create!(
52
+ user: store_credit.user,
53
+ store_credit: store_credit,
54
+ transaction_type: transaction_type,
55
+ amount: amount,
56
+ balance_after: store_credit.amount,
57
+ currency: store_credit.currency,
58
+ idempotency_key: idempotency_key,
59
+ tenant_id: tenant_id,
60
+ order: order,
61
+ admin: admin,
62
+ private_metadata: private_metadata
63
+ )
64
+ rescue ActiveRecord::RecordNotUnique
65
+ raise DuplicateTransaction, "idempotency_key already used: #{idempotency_key}"
66
+ end
67
+ # rubocop:enable Metrics/ParameterLists
68
+
69
+ def check_not_topup_blocked!(store_credit)
70
+ raise PrepaidFrozen, 'store credit is frozen for top-up' if store_credit.topup_blocked?
71
+ end
72
+
73
+ def check_not_spend_blocked!(store_credit)
74
+ raise PrepaidFrozen, 'store credit is frozen for spend' if store_credit.spend_blocked?
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,23 @@
1
+ module SpreeCmCommissioner
2
+ module Prepaid
3
+ # Reverses a top-up order's credit in one call -- looks up the order's top_up ledger row
4
+ # itself and delegates to ReverseTopUp, so callers (PrepaidReverseTopUpJob, etc.) only need
5
+ # to pass the order instead of first finding the transaction and its store_credit themselves.
6
+ class ReverseOrderTopUp
7
+ prepend ::Spree::ServiceModule::Base
8
+
9
+ def call(order:, reason: nil, private_metadata: {})
10
+ txn = SpreeCmCommissioner::PrepaidTransaction.find_by(order: order, transaction_type: 'top_up')
11
+ return failure(nil, 'no top-up transaction found for order') if txn.nil?
12
+
13
+ SpreeCmCommissioner::Prepaid::ReverseTopUp.call(
14
+ store_credit: txn.store_credit,
15
+ amount: order.total,
16
+ idempotency_key: "cancel:order:#{order.number}",
17
+ reason: reason,
18
+ private_metadata: private_metadata
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ module SpreeCmCommissioner
2
+ module Prepaid
3
+ # Claws back a prepaid credit, capped at the unspent amount -- amount_used can never be
4
+ # reduced, so a partially-spent top-up can only be reversed for what remains available.
5
+ # The ledger records the amount actually reversed (which may be less than requested) so
6
+ # reconciliation reconstructs the balance exactly.
7
+ class ReverseTopUp
8
+ prepend ::Spree::ServiceModule::Base
9
+ extend SpreeCmCommissioner::ServiceModuleThrowable
10
+ include SpreeCmCommissioner::Prepaid::Mutation
11
+
12
+ NothingToReverse = Class.new(StandardError)
13
+
14
+ # rubocop:disable Metrics/ParameterLists,Lint/MissingCopEnableDirective
15
+ # admin is optional, not required (unlike AdminCredit) -- this service has a second,
16
+ # system-initiated caller (ReverseOrderTopUp, via PrepaidReverseTopUpJob, reversing a
17
+ # top-up order's credit on cancellation) with no admin in context at all.
18
+ def call(store_credit:, amount:, idempotency_key:, admin: nil, reason: nil, private_metadata: {})
19
+ return failure(nil, 'amount must be positive') unless amount.to_d.positive?
20
+
21
+ store_credit = run_serializable do
22
+ # Deliberately no check_not_topup_blocked!/check_not_spend_blocked! here: freezing a
23
+ # wallet blocks the *account holder* from topping up or spending -- it must never block
24
+ # this administrative/system clawback, or freezing a wallet during a fraud review would
25
+ # actually protect the fraudulent credit from being reversed.
26
+ credit = Spree::StoreCredit.lock.find(store_credit.id)
27
+
28
+ available = credit.amount - credit.amount_used - credit.amount_authorized
29
+ reversed = [amount.to_d, available].min
30
+ raise NothingToReverse if reversed <= 0
31
+
32
+ credit.update!(amount: credit.amount - reversed)
33
+ write_ledger!(
34
+ store_credit: credit,
35
+ transaction_type: 'reverse_top_up',
36
+ amount: -reversed,
37
+ idempotency_key: idempotency_key,
38
+ tenant_id: credit.issuer_tenant_id,
39
+ admin: admin,
40
+ private_metadata: private_metadata.merge(reason: reason, requested_amount: amount.to_d, reversed_amount: reversed)
41
+ )
42
+ credit
43
+ end
44
+
45
+ success(store_credit)
46
+ rescue SpreeCmCommissioner::Prepaid::DuplicateTransaction => e
47
+ failure(nil, e.message)
48
+ rescue NothingToReverse
49
+ failure(nil, 'nothing available to reverse')
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,68 @@
1
+ module SpreeCmCommissioner
2
+ module Prepaid
3
+ # Credits a user's prepaid store credit balance (real-money top-up). One writer path
4
+ # shared with AdminCredit/ReverseTopUp via Prepaid::Mutation -- see
5
+ # docs/concepts/store-credit-wallet-system.md Section 4.
6
+ class TopUp
7
+ prepend ::Spree::ServiceModule::Base
8
+ extend SpreeCmCommissioner::ServiceModuleThrowable
9
+ include SpreeCmCommissioner::Prepaid::Mutation
10
+
11
+ LimitExceeded = Class.new(StandardError)
12
+
13
+ # rubocop:disable Metrics/ParameterLists
14
+ def call(user:, amount:, currency:, idempotency_key:, issuer_tenant_id: nil, order: nil, private_metadata: {})
15
+ return failure(nil, 'amount must be positive') unless amount.to_d.positive?
16
+ return failure(nil, 'unsupported currency') unless Spree::Store.default.supported_currency_codes.include?(currency)
17
+
18
+ store_credit = run_serializable do
19
+ SpreeCmCommissioner::Prepaid::CheckTopUpLimits.call!(user: user, amount: amount.to_d, currency: currency)
20
+
21
+ # The paying customer authorized this via their own payment -- no admin/system
22
+ # account involved, so they're the natural `created_by` (Spree only requires it be
23
+ # *a* Spree::User; nothing gates it to an admin role).
24
+ credit = find_or_create_prepaid_credit!(
25
+ user: user, currency: currency, issuer_tenant_id: issuer_tenant_id,
26
+ created_by: user, initial_amount: amount.to_d
27
+ )
28
+ check_not_topup_blocked!(credit)
29
+ check_max_balance!(credit, amount.to_d)
30
+
31
+ credit.update!(amount: credit.amount + amount.to_d) unless credit.previously_new_record?
32
+ write_ledger!(
33
+ store_credit: credit,
34
+ transaction_type: 'top_up',
35
+ amount: amount.to_d,
36
+ idempotency_key: idempotency_key,
37
+ tenant_id: issuer_tenant_id,
38
+ order: order,
39
+ private_metadata: private_metadata
40
+ )
41
+ credit
42
+ end
43
+
44
+ success(store_credit)
45
+ rescue SpreeCmCommissioner::Prepaid::DuplicateTransaction, SpreeCmCommissioner::Prepaid::PrepaidFrozen,
46
+ SpreeCmCommissioner::ServiceFailure, LimitExceeded => e
47
+ failure(nil, e.message)
48
+ end
49
+ # rubocop:enable Metrics/ParameterLists
50
+
51
+ private
52
+
53
+ # max_balance is denominated in Spree::Store.default#default_currency -- scale it into
54
+ # credit.currency's own magnitude (same PrepaidConfig#build_currency_limits formula) before
55
+ # comparing, or a KHR wallet's ~4100x-larger raw numbers would trip the USD-sized cap
56
+ # almost immediately.
57
+ def check_max_balance!(credit, amount)
58
+ config = SpreeCmCommissioner::Configuration.new
59
+ rate = config.prepaid_currency_rates[credit.currency].to_f
60
+ max_balance = config.prepaid_max_balance.to_d * rate
61
+ return unless max_balance.positive?
62
+ return unless credit.amount + amount > max_balance
63
+
64
+ raise LimitExceeded, "store credit balance cap exceeded (max #{max_balance})"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,47 @@
1
+ module SpreeCmCommissioner
2
+ module Prepaid
3
+ class Unfreeze
4
+ prepend ::Spree::ServiceModule::Base
5
+ extend SpreeCmCommissioner::ServiceModuleThrowable
6
+ include Mutation
7
+
8
+ def call(store_credit:, admin: nil)
9
+ # Same locking rationale as Freeze#call -- avoid interleaving with a concurrent money-in
10
+ # operation that locks the same row.
11
+ store_credit = run_serializable do
12
+ credit = Spree::StoreCredit.lock.find(store_credit.id)
13
+
14
+ # Freeze columns are about to be cleared -- capture them here so who lifted the freeze,
15
+ # and what was being lifted, isn't lost. Idempotency key is keyed to this specific
16
+ # freeze (frozen_at), so a double-submitted unfreeze can't write two ledger rows, while
17
+ # a later freeze/unfreeze cycle on the same credit still gets its own row.
18
+ if credit.frozen_at.present?
19
+ write_ledger!(
20
+ store_credit: credit,
21
+ transaction_type: 'unfreeze',
22
+ amount: 0,
23
+ idempotency_key: "unfreeze:#{credit.id}:#{credit.frozen_at.to_i}",
24
+ admin: admin,
25
+ private_metadata: {
26
+ prior_frozen_by_id: credit.frozen_by_id,
27
+ prior_freeze_reason: credit.freeze_reason,
28
+ prior_freeze_level: credit.freeze_level
29
+ }
30
+ )
31
+ end
32
+
33
+ credit.update!(
34
+ frozen_at: nil,
35
+ frozen_by: nil,
36
+ freeze_reason: nil,
37
+ freeze_level: nil,
38
+ frozen_until: nil
39
+ )
40
+ credit
41
+ end
42
+
43
+ success(store_credit)
44
+ end
45
+ end
46
+ end
47
+ end