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,75 @@
1
+ <!-- VireakBuntham (VET) API Credentials -->
2
+ <div class="col-12">
3
+ <hr class="my-4">
4
+
5
+ <h6 class="text-muted">
6
+ <%= I18n.t('spree.admin.external_integrations.vireak_buntham_credentials', default: 'VireakBuntham (VET) API Credentials') %>
7
+ </h6>
8
+ </div>
9
+
10
+ <div class="col-6">
11
+ <%= f.field_container :api_url do %>
12
+ <%= f.label :api_url, raw(I18n.t('spree.admin.external_integrations.api_url', default: 'API URL') + required_span_tag) %>
13
+ <%= text_field_tag 'spree_cm_commissioner_integration[private_metadata][api_url]', f.object.api_url, class: 'form-control', placeholder: 'https://api.virakbuntham.com', required: true %>
14
+ <% end %>
15
+ </div>
16
+
17
+ <div class="col-6">
18
+ <%= f.field_container :api_token do %>
19
+ <%= f.label :api_token, raw(I18n.t('spree.admin.external_integrations.api_token', default: 'API Token') + required_span_tag) %>
20
+ <%= text_field_tag 'spree_cm_commissioner_integration[private_metadata][api_token]', f.object.api_token, class: 'form-control', required: true %>
21
+ <% end %>
22
+ </div>
23
+
24
+ <div class="col-6">
25
+ <%= f.field_container :callback_username do %>
26
+ <%= f.label :callback_username, I18n.t('spree.admin.external_integrations.callback_username', default: 'Callback Username') %>
27
+ <%= text_field_tag 'spree_cm_commissioner_integration[private_metadata][callback_username]', f.object.callback_username, class: 'form-control', placeholder: 'For VET webhook Basic Auth' %>
28
+ <% end %>
29
+ </div>
30
+
31
+ <div class="col-6">
32
+ <%= f.field_container :callback_pwd do %>
33
+ <%= f.label :callback_pwd, I18n.t('spree.admin.external_integrations.callback_pwd', default: 'Callback Password') %>
34
+ <%= text_field_tag 'spree_cm_commissioner_integration[private_metadata][callback_pwd]', f.object.callback_pwd, class: 'form-control', placeholder: 'For VET webhook Basic Auth' %>
35
+ <% end %>
36
+ </div>
37
+
38
+ <!-- Sync Configuration -->
39
+ <div class="col-12">
40
+ <hr class="my-4">
41
+
42
+ <h6 class="text-muted">
43
+ <%= I18n.t('spree.admin.external_integrations.vireak_buntham_sync_config', default: 'Sync Configuration') %>
44
+ </h6>
45
+ </div>
46
+
47
+ <div class="col-6">
48
+ <%= f.field_container :route_type do %>
49
+ <%= f.label :route_type, raw(I18n.t('spree.admin.external_integrations.route_type', default: 'Route Type') + required_span_tag) %>
50
+ <%= select_tag 'spree_cm_commissioner_integration[private_metadata][route_type]',
51
+ options_for_select(
52
+ [
53
+ [I18n.t('spree.admin.external_integrations.route_type_bus', default: 'Bus'), 1],
54
+ [I18n.t('spree.admin.external_integrations.route_type_ferry', default: 'Ferry / Boat'), 2],
55
+ [I18n.t('spree.admin.external_integrations.route_type_air_bus', default: 'Air Bus'), 3]
56
+ ],
57
+ f.object.route_type || 1
58
+ ),
59
+ class: 'form-control select2',
60
+ required: true %>
61
+ <small class="form-text text-muted">
62
+ <%= I18n.t('spree.admin.external_integrations.route_type_description', default: 'Vehicle category — determines the VET type parameter (1/2/3) sent on every API call') %>
63
+ </small>
64
+ <% end %>
65
+ </div>
66
+
67
+ <div class="col-6">
68
+ <%= f.field_container :sync_days do %>
69
+ <%= f.label :sync_days, raw(I18n.t('spree.admin.external_integrations.sync_days', default: 'Sync Days') + required_span_tag) %>
70
+ <%= number_field_tag 'spree_cm_commissioner_integration[private_metadata][sync_days]', f.object.sync_days || 7, class: 'form-control', min: 1, max: 30, required: true %>
71
+ <small class="form-text text-muted">
72
+ <%= I18n.t('spree.admin.external_integrations.sync_days_description', default: 'Number of days to sync trips for (default: 7)') %>
73
+ </small>
74
+ <% end %>
75
+ </div>
@@ -31,7 +31,7 @@
31
31
  <td><%= integration.name %></td>
32
32
 
33
33
  <td>
34
- <%= integration_base_url_link(integration.base_url) %>
34
+ <%= integration_base_url_link(integration.try(:base_url)) %>
35
35
  </td>
36
36
 
37
37
  <td>
@@ -0,0 +1,135 @@
1
+ <%= render partial: 'spree/admin/shared/error_messages', locals: { target: @object } %>
2
+
3
+ <% if f.object.errors[:currency].present? && (existing = f.object.duplicate_rule) %>
4
+ <div class="alert alert-warning">
5
+ <%= link_to Spree.t(:fee_rule_edit_existing), edit_admin_payment_fee_rule_path(existing) %>
6
+ </div>
7
+ <% end %>
8
+
9
+ <div class="mb-4 card">
10
+ <div class="card-header">
11
+ <div class="d-flex justify-content-between align-items-center">
12
+ <h5 class="mb-0"><%= Spree.t(:service_fee_rule_scope) %></h5>
13
+ <div class="mb-0 form-check d-flex align-items-center">
14
+ <%= f.check_box :active, class: 'form-check-input mt-0' %>
15
+ <%= f.label :active, Spree.t(:service_fee_rule_active), class: 'form-check-label ms-1' %>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ <div class="card-body">
20
+ <div class="row">
21
+ <div class="col-12 col-lg-6">
22
+ <%= f.field_container :ruleable_type do %>
23
+ <%= f.label :ruleable_type, Spree.t(:service_fee_rule_scope_type) %>
24
+ <%= f.select :ruleable_type,
25
+ [[Spree.t(:service_fee_rule_scope_product), 'Spree::Product'],
26
+ [Spree.t(:service_fee_rule_scope_event), 'Spree::Taxon'],
27
+ [Spree.t(:service_fee_rule_scope_vendor), 'Spree::Vendor']],
28
+ { include_blank: Spree.t(:service_fee_rule_scope_global) },
29
+ class: 'fullwidth select2' %>
30
+ <%= f.error_message_on :ruleable_type %>
31
+ <% end %>
32
+ </div>
33
+ <div class="col-12 col-lg-6">
34
+ <%= f.field_container :ruleable_id do %>
35
+ <%= f.label :ruleable_id, Spree.t(:service_fee_rule_scope_target) %>
36
+ <%= f.select :ruleable_id,
37
+ options_for_select(
38
+ f.object.ruleable.present? ? [[SpreeCmCommissioner::ScopedFeeRule.ruleable_label(f.object.ruleable), f.object.ruleable_id]] : [],
39
+ f.object.ruleable_id
40
+ ),
41
+ { include_hidden: true, include_blank: true },
42
+ class: 'select2autocomplete',
43
+ data: {
44
+ autocomplete_url_value: 'homepage_section_relatables_api_v2',
45
+ autocomplete_return_attr_value: :name_with_slug,
46
+ autocomplete_min_input_value: 1,
47
+ autocomplete_additional_url_params_value: "model_class=#{f.object.ruleable_type}"
48
+ } %>
49
+ <small class="form-text text-muted"><%= Spree.t(:service_fee_rule_scope_target_hint) %></small>
50
+ <%= f.error_message_on :ruleable_id %>
51
+ <% end %>
52
+ </div>
53
+ </div>
54
+
55
+ <%= f.field_container :payment_method_id do %>
56
+ <%= f.label :payment_method_id, Spree.t(:payment_method) %>
57
+ <%= f.select :payment_method_id,
58
+ @payment_methods.map { |pm|
59
+ label = ["#{pm.name} (#{pm.id})", pm.vendor&.name].compact.join(' — ')
60
+ label += " (#{Spree.t(:inactive)})" unless pm.active?
61
+ [label, pm.id]
62
+ },
63
+ { include_blank: true },
64
+ class: 'fullwidth select2' %>
65
+ <%= f.error_message_on :payment_method_id %>
66
+ <% end %>
67
+
68
+ <% if f.object.tenant_id.present? %>
69
+ <%# tenant_id is set once from the URL at creation (see PaymentFeeRulesController#build_resource)
70
+ and is never reassignable via this form -- permitted_resource_params excludes it, so this
71
+ hidden field is inert on submit and exists only so the value survives an invalid re-render. %>
72
+ <%= f.hidden_field :tenant_id %>
73
+ <p class="form-text text-muted">
74
+ <%= Spree.t(:service_fee_rule_scope_tenant, name: f.object.tenant&.name || "##{f.object.tenant_id}") %>
75
+ </p>
76
+ <% end %>
77
+
78
+ <%= f.field_container :currency do %>
79
+ <%= f.label :currency %>
80
+ <%= f.select :currency, @currencies, {}, class: 'fullwidth select2' %>
81
+ <%= f.error_message_on :currency %>
82
+ <% end %>
83
+ </div>
84
+ </div>
85
+
86
+ <% if f.object.new_record? %>
87
+ <div class="alert alert-info">
88
+ <%= Spree.t(:service_fee_rule_pick_type_hint) %>
89
+ </div>
90
+ <% end %>
91
+
92
+ <%= render 'spree/admin/shared/calculator_fields', f: f %>
93
+
94
+ <script>
95
+ $(document).ready(function () {
96
+ var ruleableTypeSelector = "#spree_cm_commissioner_payment_fee_rule_ruleable_type";
97
+ var ruleableIdSelector = "#spree_cm_commissioner_payment_fee_rule_ruleable_id";
98
+ // The tenant the admin was browsing from (index's tenant filter, threaded through the
99
+ // new/edit URL) — narrows the Scope Target autocomplete to that tenant's own
100
+ // vendors/products/events so a tenant-context rule can't accidentally target another
101
+ // tenant's vendor. Not the rule's own `tenant_id` field, which only applies to Global rows.
102
+ var filterTenantId = "<%= j params[:tenant_id] %>";
103
+
104
+ // initialize
105
+ updateRuleableIdOptions();
106
+ loadAutoCompleteParams();
107
+
108
+ $(ruleableTypeSelector).on("change", function () {
109
+ updateRuleableIdOptions();
110
+ loadAutoCompleteParams();
111
+ });
112
+
113
+ function updateRuleableIdOptions() {
114
+ var selectedType = $(ruleableTypeSelector).val() || '';
115
+ var params = "model_class=" + selectedType;
116
+ // "Event" taxons only (taxonomy "Events", depth 1) — see Spree::Taxon.events —
117
+ // not every taxon, which would also list categories/places/etc.
118
+ if (selectedType === "Spree::Taxon") {
119
+ params += "&taxon_scope=events";
120
+ }
121
+ if (filterTenantId) {
122
+ params += "&tenant_id=" + encodeURIComponent(filterTenantId);
123
+ }
124
+ $(ruleableIdSelector).attr('data-autocomplete-additional-url-params-value', params);
125
+
126
+ // Same-named records are told apart by slug, so search it too where it's ransackable:
127
+ // Product#slug, and Taxon#permalink for events. Vendor#slug isn't whitelisted, so vendors
128
+ // only show it in the option label.
129
+ var searchQuery = selectedType === "Spree::Product" ? "name_or_slug_i_cont"
130
+ : selectedType === "Spree::Taxon" ? "name_or_permalink_i_cont"
131
+ : "name_i_cont";
132
+ $(ruleableIdSelector).attr('data-autocomplete-search-query-value', searchQuery);
133
+ }
134
+ });
135
+ </script>
@@ -0,0 +1,11 @@
1
+ <%= content_for :page_title do %>
2
+ <%= link_to Spree.t(:payment_fees), admin_payment_fee_rules_path(status: params[:status], tenant_id: params[:tenant_id]) %> / <%= @object.scope_label %>
3
+ <% end %>
4
+
5
+ <%= form_with model: @object, url: { action: 'update' }, method: :patch, local: true do |f| %>
6
+ <%= render 'form', f: f %>
7
+
8
+ <fieldset>
9
+ <%= render partial: 'spree/admin/shared/edit_resource_links' %>
10
+ </fieldset>
11
+ <% end %>
@@ -0,0 +1,100 @@
1
+ <%= content_for :page_title do %>
2
+ <%= Spree.t(:payment_fees) %>
3
+ <% end %>
4
+
5
+ <% content_for :page_actions do %>
6
+ <%= button_link_to Spree.t(:new_service_fee_rule),
7
+ new_admin_payment_fee_rule_path(status: @status, tenant_id: params[:tenant_id]),
8
+ class: 'btn-success',
9
+ icon: 'add.svg',
10
+ id: 'new_admin_payment_fee_rule_link' %>
11
+ <% end %>
12
+
13
+ <div class="form-group d-inline-block">
14
+ <span class="align-middle btn-group">
15
+ <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
16
+ <%= selected_tenant&.name || 'BookMe+' %>
17
+ </button>
18
+ <span class="dropdown-menu">
19
+ <%= link_to 'BookMe+',
20
+ admin_payment_fee_rules_path(status: @status),
21
+ class: "dropdown-item #{'active' if selected_tenant.nil?}" %>
22
+ <% @tenants.each do |tenant| %>
23
+ <%= link_to tenant.name,
24
+ admin_payment_fee_rules_path(status: @status, tenant_id: tenant.id),
25
+ class: "dropdown-item #{'active' if selected_tenant&.id == tenant.id}" %>
26
+ <% end %>
27
+ </span>
28
+ </span>
29
+
30
+ <div class="btn-group btn-group-sm" role="group">
31
+ <%= link_to Spree.t(:service_fee_rule_all), admin_payment_fee_rules_path(tenant_id: params[:tenant_id]),
32
+ class: "btn btn-outline-secondary #{'active' if @status == 'all'}" %>
33
+ <%= link_to Spree.t(:service_fee_rule_active), admin_payment_fee_rules_path(status: 'active', tenant_id: params[:tenant_id]),
34
+ class: "btn btn-outline-secondary #{'active' if @status == 'active'}" %>
35
+ <%= link_to Spree.t(:service_fee_rule_inactive), admin_payment_fee_rules_path(status: 'inactive', tenant_id: params[:tenant_id]),
36
+ class: "btn btn-outline-secondary #{'active' if @status == 'inactive'}" %>
37
+ </div>
38
+ </div>
39
+
40
+ <% if @payment_fee_rules.any? %>
41
+ <div class="mb-3 bg-white border rounded table-responsive">
42
+ <table class="table" id="listing_payment_fee_rules" data-hook>
43
+ <thead class="text-muted">
44
+ <tr data-hook="payment_fee_rules_header">
45
+ <th><%= Spree.t(:id) %></th>
46
+ <th><%= Spree.t(:service_fee_rule_scope) %></th>
47
+ <th><%= Spree.t(:payment_method) %></th>
48
+ <th><%= Spree.t(:currency) %></th>
49
+ <th><%= Spree.t(:service_fee_rule_active) %></th>
50
+ <th><%= Spree.t(:service_fee_rule_fee) %></th>
51
+ <th><%= sort_link @search, :created_at, Spree.t(:created_at) %></th>
52
+ <th><%= sort_link @search, :updated_at, Spree.t(:updated_at) %></th>
53
+ <th class="actions"></th>
54
+ </tr>
55
+ </thead>
56
+ <tbody>
57
+ <% @payment_fee_rules.each do |rule| %>
58
+ <tr id="<%= spree_dom_id(rule) %>" data-hook="payment_fee_rules_row">
59
+ <td><%= rule.id %></td>
60
+ <td>
61
+ <%= fee_rule_scope_link(rule) %>
62
+ </td>
63
+ <td>
64
+ <% if (payment_method = rule.payment_method) %>
65
+ <%= link_to payment_method.name, spree.edit_admin_payment_method_path(payment_method) %>
66
+ <% if payment_method.vendor %>
67
+ <span class="text-muted">— <%= payment_method.vendor.name %></span>
68
+ <% end %>
69
+ <% end %>
70
+ </td>
71
+ <td><code><%= rule.currency %></code></td>
72
+ <td>
73
+ <% if rule.active? %>
74
+ <span class="badge badge-success"><%= Spree.t(:service_fee_rule_active) %></span>
75
+ <% else %>
76
+ <span class="badge badge-secondary"><%= Spree.t(:service_fee_rule_inactive) %></span>
77
+ <% end %>
78
+ </td>
79
+ <td><strong><%= rule.fee_description %></strong></td>
80
+ <td><%= rule.created_at %></td>
81
+ <td><%= rule.updated_at %></td>
82
+ <td data-hook="admin_payment_fee_rules_row_actions" class="actions">
83
+ <span class="gap-2 d-flex justify-content-end">
84
+ <%= link_to_edit rule, url: edit_admin_payment_fee_rule_path(rule), no_text: true %>
85
+ <%= link_to_delete rule, url: admin_payment_fee_rule_path(rule), no_text: true %>
86
+ </span>
87
+ </td>
88
+ </tr>
89
+ <% end %>
90
+ </tbody>
91
+ </table>
92
+ </div>
93
+
94
+ <%= paginate @payment_fee_rules, theme: 'admin-twitter-bootstrap-4' %>
95
+ <% else %>
96
+ <div class="m-5 text-center no-objects-found">
97
+ <%= Spree.t(:no_resource_found, resource: plural_resource_name(SpreeCmCommissioner::PaymentFeeRule)) %>,
98
+ <%= link_to(Spree.t(:add_one), new_admin_payment_fee_rule_path) if can?(:create, SpreeCmCommissioner::PaymentFeeRule) %>!
99
+ </div>
100
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <%= content_for :page_title do %>
2
+ <%= link_to Spree.t(:payment_fees), admin_payment_fee_rules_path(status: params[:status], tenant_id: params[:tenant_id]) %> / <%= Spree.t(:new) %>
3
+ <% end %>
4
+
5
+ <%= form_with model: @object, url: { action: 'create' }, local: true do |f| %>
6
+ <%= render 'form', f: f %>
7
+
8
+ <fieldset>
9
+ <%= render partial: 'spree/admin/shared/new_resource_links' %>
10
+ </fieldset>
11
+ <% end %>
@@ -0,0 +1,102 @@
1
+ <% t_scope = 'spree_cm_commissioner.store_credit_admin.show' %>
2
+
3
+ <%= render partial: 'spree/admin/store_credit_admin/tabs', locals: { current: @current_tab, tenant_id: @current_tenant_id } %>
4
+
5
+ <% content_for :page_title do %>
6
+ <%= t("#{t_scope}.page_title") %>
7
+ <% end %>
8
+
9
+ <% content_for :page_actions do %>
10
+ <%= button_link_to t("#{t_scope}.run_reconciliation"), run_reconciliation_admin_store_credit_path, method: :post, class: 'btn btn-outline-primary' %>
11
+ <% end %>
12
+
13
+ <%= render partial: 'spree/admin/store_credit_admin/tenant_selector',
14
+ locals: {
15
+ path_for_tenant_id: ->(tid) { admin_store_credit_transactions_path(tenant_id: tid) },
16
+ tenants: @tenants,
17
+ current_tenant_id: @current_tenant_id
18
+ } %>
19
+
20
+ <div class="bg-white border rounded p-3 mb-3">
21
+ <%= search_form_for [:admin, @transactions_search], url: admin_store_credit_transactions_path(tenant_id: @current_tenant_id) do |f| %>
22
+ <div class="row">
23
+ <div class="col-md-2 mb-2">
24
+ <label class="mb-0"><%= t("#{t_scope}.transactions.user_email") %></label>
25
+ <%= f.text_field :user_email_cont, class: 'form-control', placeholder: 'user@example.com' %>
26
+ </div>
27
+ <div class="col-md-2 mb-2">
28
+ <label class="mb-0"><%= t("#{t_scope}.transactions.order_number") %></label>
29
+ <%= f.text_field :order_number_cont, class: 'form-control', placeholder: 'R123456789' %>
30
+ </div>
31
+ <div class="col-md-2 mb-2">
32
+ <label class="mb-0"><%= t("#{t_scope}.transactions.currency") %></label>
33
+ <%= f.select :currency_eq, @store.supported_currency_codes, { include_blank: t("#{t_scope}.transactions.any") }, class: 'form-control' %>
34
+ </div>
35
+ <div class="col-md-2 mb-2">
36
+ <label class="mb-0"><%= t("#{t_scope}.transactions.type") %></label>
37
+ <%= f.select :transaction_type_eq, SpreeCmCommissioner::PrepaidTransaction::TRANSACTION_TYPES, { include_blank: t("#{t_scope}.transactions.any") }, class: 'form-control' %>
38
+ </div>
39
+ <div class="col-md-2 mb-2">
40
+ <label class="mb-0"><%= t("#{t_scope}.transactions.from") %></label>
41
+ <%= f.date_field :created_at_gteq, class: 'form-control' %>
42
+ </div>
43
+ <div class="col-md-2 mb-2">
44
+ <label class="mb-0"><%= t("#{t_scope}.transactions.to") %></label>
45
+ <%= f.date_field :created_at_lteq, class: 'form-control' %>
46
+ </div>
47
+ <div class="col-md-2 mb-2 d-flex align-items-end">
48
+ <%= button Spree.t(:filter_results), 'search' %>
49
+ </div>
50
+ </div>
51
+ <% end %>
52
+ </div>
53
+
54
+ <div class="bg-white border rounded table-responsive">
55
+ <table class="table" data-hook>
56
+ <thead class="text-muted">
57
+ <tr>
58
+ <th><%= t("#{t_scope}.transactions.id") %></th>
59
+ <th><%= t("#{t_scope}.transactions.date") %></th>
60
+ <th><%= t("#{t_scope}.transactions.user") %></th>
61
+ <th><%= t("#{t_scope}.transactions.type") %><%= hint_icon(t("#{t_scope}.transactions.type_hint")) %></th>
62
+ <th><%= t("#{t_scope}.transactions.currency") %></th>
63
+ <th><%= t("#{t_scope}.transactions.amount") %></th>
64
+ <th><%= t("#{t_scope}.transactions.status") %></th>
65
+ <th><%= t("#{t_scope}.transactions.order") %></th>
66
+ <th><%= t("#{t_scope}.transactions.admin") %><%= hint_icon(t("#{t_scope}.transactions.admin_hint")) %></th>
67
+ </tr>
68
+ </thead>
69
+ <tbody>
70
+ <% if @transactions.empty? %>
71
+ <tr>
72
+ <td colspan="10" class="text-center text-muted"><%= t("#{t_scope}.transactions.none") %></td>
73
+ </tr>
74
+ <% end %>
75
+ <% @transactions.each do |txn| %>
76
+ <tr>
77
+ <td>
78
+ <%= link_to txn.id, admin_store_credit_transaction_path(txn, return_to: request.fullpath) %>
79
+ </td>
80
+ <td class="text-nowrap"><%= l(txn.created_at, format: :short) %></td>
81
+ <td>
82
+ <% if txn.user %>
83
+ <%= link_to txn.user.email, edit_admin_user_path(txn.user) %>
84
+ <% end %>
85
+ </td>
86
+ <td><%= txn.display_type %></td>
87
+ <td><%= txn.currency %></td>
88
+ <td><%= txn.display_amount %></td>
89
+ <td><span class="badge badge-<%= txn.status == 'completed' ? 'success' : 'secondary' %>"><%= txn.status %></span></td>
90
+ <td>
91
+ <% if txn.order %>
92
+ <%= link_to txn.order.number, edit_admin_order_path(txn.order) %>
93
+ <% end %>
94
+ </td>
95
+ <td><%= txn.admin&.email %></td>
96
+ </tr>
97
+ <% end %>
98
+ </tbody>
99
+ </table>
100
+ </div>
101
+
102
+ <%= paginate @transactions, theme: 'admin-twitter-bootstrap-4' %>
@@ -0,0 +1,146 @@
1
+ <% t_scope = 'spree_cm_commissioner.store_credit.transaction_detail' %>
2
+ <% back_path = @return_to.presence || spree.admin_store_credit_transactions_path %>
3
+
4
+ <%# This detail page's own route lives under /admin/store_credit (see routes.rb), so its chrome
5
+ matches that section -- the same Overview/Frozen/Transactions/Settings tabs as the main
6
+ store credit dashboard, not the per-user Account/Addresses/... tabs -- even though it's also
7
+ reachable from a specific user's store_credits page. %>
8
+ <%= render partial: 'spree/admin/store_credit_admin/tabs', locals: { current: 'transactions', tenant_id: @transaction.tenant_id } %>
9
+
10
+ <% content_for :title do %>
11
+ <%= "Spree #{Spree.t('administration')}: #{t("#{t_scope}.title", id: @transaction.id)}" %>
12
+ <% end %>
13
+
14
+ <% content_for :page_title do %>
15
+ <%= page_header_back_button(back_path) %>
16
+ <%= t("#{t_scope}.title", id: @transaction.id) %>
17
+ <% end %>
18
+
19
+ <div class="bg-white border rounded p-4">
20
+ <div class="d-flex justify-content-between align-items-center mb-3 pb-3 border-bottom">
21
+ <h4 class="mb-0">
22
+ <%= @transaction.display_type %>
23
+ <% if @reversed %>
24
+ <span class="badge badge-secondary"><%= t('spree_cm_commissioner.store_credit_admin.show.overview.reversed') %></span>
25
+ <% end %>
26
+ </h4>
27
+ <span class="badge badge-<%= @transaction.status == 'completed' ? 'success' : 'secondary' %> px-3 py-2"><%= @transaction.status %></span>
28
+ </div>
29
+
30
+ <div class="row row-cols-2 row-cols-md-4">
31
+ <div class="col mb-3">
32
+ <div class="text-muted small"><%= t("#{t_scope}.id") %></div>
33
+ <div class="font-weight-bold"><%= @transaction.id %></div>
34
+ </div>
35
+ <div class="col mb-3">
36
+ <div class="text-muted small"><%= Spree.t(:date) %></div>
37
+ <div class="font-weight-bold"><%= l(@transaction.created_at, format: :short) %></div>
38
+ </div>
39
+ <div class="col mb-3">
40
+ <div class="text-muted small"><%= Spree.t(:amount) %></div>
41
+ <div class="font-weight-bold"><%= @transaction.display_amount %></div>
42
+ </div>
43
+ <div class="col mb-3">
44
+ <div class="text-muted small"><%= Spree.t(:currency, default: 'Currency') %></div>
45
+ <div class="font-weight-bold"><%= @transaction.currency %></div>
46
+ </div>
47
+ <div class="col mb-3">
48
+ <div class="text-muted small"><%= t("#{t_scope}.balance") %><%= hint_icon(t("#{t_scope}.balance_hint")) %></div>
49
+ <div class="font-weight-bold"><%= @transaction.display_balance_after %></div>
50
+ </div>
51
+ <div class="col mb-3">
52
+ <div class="text-muted small"><%= Spree.t(:order, default: 'Order') %></div>
53
+ <div class="font-weight-bold">
54
+ <% if @transaction.order %>
55
+ <%= link_to @transaction.order.number, spree.edit_admin_order_path(@transaction.order) %>
56
+ <% else %>
57
+ <span class="text-muted">&mdash;</span>
58
+ <% end %>
59
+ </div>
60
+ </div>
61
+ <div class="col mb-3">
62
+ <div class="text-muted small"><%= t("#{t_scope}.admin") %><%= hint_icon(t("#{t_scope}.admin_hint")) %></div>
63
+ <div class="font-weight-bold">
64
+ <% if @transaction.admin %>
65
+ <%= link_to @transaction.admin.email, spree.edit_admin_user_path(@transaction.admin) %>
66
+ <% else %>
67
+ <span class="text-muted">&mdash;</span>
68
+ <% end %>
69
+ </div>
70
+ </div>
71
+ <div class="col mb-3">
72
+ <div class="text-muted small"><%= t("#{t_scope}.idempotency_key") %></div>
73
+ <div class="font-weight-bold"><code class="small"><%= @transaction.idempotency_key %></code></div>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="border-top pt-3">
78
+ <div class="text-muted small"><%= Spree.t(:note, default: 'Note') %></div>
79
+ <div><%= @transaction.reason.presence || content_tag(:span, '—', class: 'text-muted') %></div>
80
+ </div>
81
+
82
+ <% if @transaction.level.present? || @transaction.until_at.present? || @transaction.prior_frozen_by_id.present? || @transaction.prior_freeze_reason.present? %>
83
+ <div class="bg-light rounded p-3 mt-3">
84
+ <h6 class="text-muted mb-3"><%= t("#{t_scope}.freeze_details") %></h6>
85
+ <div class="row row-cols-2 row-cols-md-4">
86
+ <% if @transaction.level.present? %>
87
+ <div class="col mb-2">
88
+ <div class="text-muted small"><%= t("#{t_scope}.level") %></div>
89
+ <div><%= @transaction.level %></div>
90
+ </div>
91
+ <% end %>
92
+ <% if @transaction.until_at.present? %>
93
+ <div class="col mb-2">
94
+ <div class="text-muted small"><%= t("#{t_scope}.until_at") %></div>
95
+ <div><%= l(@transaction.until_at, format: :short) %></div>
96
+ </div>
97
+ <% end %>
98
+ <% if @transaction.prior_frozen_by_id.present? %>
99
+ <div class="col mb-2">
100
+ <div class="text-muted small"><%= t("#{t_scope}.prior_frozen_by") %></div>
101
+ <div><%= Spree.user_class.find_by(id: @transaction.prior_frozen_by_id)&.email %></div>
102
+ </div>
103
+ <% end %>
104
+ <% if @transaction.prior_freeze_reason.present? %>
105
+ <div class="col mb-2">
106
+ <div class="text-muted small"><%= t("#{t_scope}.prior_freeze_reason") %></div>
107
+ <div><%= @transaction.prior_freeze_reason %></div>
108
+ </div>
109
+ <% end %>
110
+ <% if @transaction.prior_freeze_level.present? %>
111
+ <div class="col mb-2">
112
+ <div class="text-muted small"><%= t("#{t_scope}.prior_freeze_level") %></div>
113
+ <div><%= @transaction.prior_freeze_level %></div>
114
+ </div>
115
+ <% end %>
116
+ </div>
117
+ </div>
118
+ <% end %>
119
+
120
+ <% private_metadata = @transaction.private_metadata || {} %>
121
+ <% if private_metadata['requested_amount'].present? || private_metadata['reversed_amount'].present? %>
122
+ <div class="bg-light rounded p-3 mt-3">
123
+ <h6 class="text-muted mb-3"><%= t("#{t_scope}.reversal_details") %></h6>
124
+ <div class="row row-cols-2 row-cols-md-4">
125
+ <div class="col mb-2">
126
+ <div class="text-muted small"><%= t("#{t_scope}.requested_amount") %></div>
127
+ <div><%= Spree::Money.new(private_metadata['requested_amount'], currency: @transaction.currency) %></div>
128
+ </div>
129
+ <div class="col mb-2">
130
+ <div class="text-muted small"><%= t("#{t_scope}.reversed_amount") %></div>
131
+ <div><%= Spree::Money.new(private_metadata['reversed_amount'], currency: @transaction.currency) %></div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ <% end %>
136
+
137
+ <% if %w[top_up admin_credit].include?(@transaction.transaction_type) && @transaction.status == 'completed' && !@reversed && @transaction.order.nil? && can?(:create, Spree::StoreCredit) %>
138
+ <div class="border-top pt-3 mt-3">
139
+ <%= button_to t('spree_cm_commissioner.store_credit.user_credits.reverse'),
140
+ spree.reverse_top_up_admin_user_store_credit_path(@user, @store_credit, transaction_id: @transaction.id),
141
+ method: :post,
142
+ class: 'btn btn-outline-danger',
143
+ data: { confirm: Spree.t(:are_you_sure) } %>
144
+ </div>
145
+ <% end %>
146
+ </div>
@@ -22,7 +22,8 @@
22
22
  autocomplete_url_value: 'products_api_v2',
23
23
  autocomplete_multiple_value: true,
24
24
  autocomplete_search_query_value: 'name_or_slug_i_cont',
25
- autocomplete_placeholder_value: t('.products_placeholder')
25
+ autocomplete_placeholder_value: t('.products_placeholder'),
26
+ offline_product_ids: @offline_product_ids.to_json
26
27
  } %>
27
28
  <small class="form-text text-muted"><%= t('.products_hint') %></small>
28
29
  </div>
@@ -56,3 +57,25 @@
56
57
 
57
58
  <%# Leaving both audience fields blank is a real choice, not an oversight, so it is stated. %>
58
59
  <p class="text-muted small mb-0"><%= t('.audience_hint') %></p>
60
+
61
+ <%# The generic autocomplete only fetches `name`, so offline products are labelled from the preloaded id list —
62
+ re-init its select2 with the same options plus badge templates. Runs after the global spree:load init. %>
63
+ <script>
64
+ document.addEventListener("spree:load", () => {
65
+ const select = document.getElementById("private_sale_product_ids");
66
+ if (!select || !$(select).data("select2")) return;
67
+
68
+ const offlineProductIds = ($(select).data("offline-product-ids") || []).map(String);
69
+
70
+ // Returns a jQuery node (not a string) so select2 renders the badge markup; `.text()` keeps the name escaped.
71
+ const label = (item) => {
72
+ if (!offlineProductIds.includes(String(item.id))) return item.text;
73
+
74
+ return $("<span>").text(item.text)
75
+ .append(" ", $("<span class='badge badge-info ml-1'>").text("Offline"));
76
+ };
77
+ const options = $(select).data("select2").options.options;
78
+
79
+ $(select).select2($.extend({}, options, { templateResult: label, templateSelection: label }));
80
+ });
81
+ </script>
@@ -44,7 +44,12 @@
44
44
  <% next if product.nil? %>
45
45
 
46
46
  <tr class="bg-light">
47
- <td colspan="7"><strong><%= product.name %></strong></td>
47
+ <td colspan="7">
48
+ <strong><%= product.name %></strong>
49
+ <% if product.offline? %>
50
+ <span class="badge badge-info ml-1">Offline</span>
51
+ <% end %>
52
+ </td>
48
53
  <td class="text-right">
49
54
  <%# Own column so these line up under the same x-position for every product, rather than
50
55
  sharing space with the name — icon-only, matching the with-tip pattern used for row