spree_cm_commissioner 2.8.3 → 2.8.4.pre.pre1

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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/admin/guests_controller.rb +1 -1
  4. data/app/controllers/spree/admin/order_ticket_transfers_controller.rb +25 -0
  5. data/app/controllers/spree/admin/stock_managements_controller.rb +1 -1
  6. data/app/controllers/spree/admin/system/waiting_room_controller.rb +15 -0
  7. data/app/controllers/spree/admin/taxons_controller_decorator.rb +5 -0
  8. data/app/controllers/spree/admin/ticket_transfer_rules_controller.rb +47 -0
  9. data/app/controllers/spree/admin/ticket_transfers_controller.rb +61 -0
  10. data/app/controllers/spree/api/v2/storefront/account/recipients_controller.rb +32 -0
  11. data/app/controllers/spree/api/v2/storefront/guests_controller.rb +7 -5
  12. data/app/controllers/spree/api/v2/storefront/ticket_transfer_acceptances_controller.rb +32 -0
  13. data/app/controllers/spree/api/v2/storefront/ticket_transfers_controller.rb +117 -0
  14. data/app/controllers/spree/api/v2/storefront/transit/draft_orders_controller.rb +3 -1
  15. data/app/controllers/spree/api/v2/tenant/transit/draft_orders_controller.rb +3 -1
  16. data/app/controllers/spree/api/v2/tenant/trips_controller.rb +11 -0
  17. data/app/controllers/spree_cm_commissioner/admin/orders_controller_decorator.rb +7 -3
  18. data/app/helpers/spree_cm_commissioner/admin/guest_helper.rb +10 -0
  19. data/app/helpers/spree_cm_commissioner/admin/ticket_transfers_helper.rb +32 -0
  20. data/app/interactors/spree_cm_commissioner/pin_code_sender.rb +62 -5
  21. data/app/interactors/spree_cm_commissioner/waiting_guests_caller.rb +86 -10
  22. data/app/jobs/spree_cm_commissioner/expire_ticket_transfer_job.rb +10 -0
  23. data/app/jobs/spree_cm_commissioner/telegram_gateway/pin_code_sender_job.rb +18 -0
  24. data/app/jobs/spree_cm_commissioner/vendor_route_metrics/decrease_trip_count_job.rb +17 -0
  25. data/app/jobs/spree_cm_commissioner/vendor_route_metrics/increase_trip_count_job.rb +14 -0
  26. data/app/jobs/spree_cm_commissioner/waiting_room/publish_lobby_path_job.rb +17 -0
  27. data/app/jobs/spree_cm_commissioner/waiting_room/stamp_queue_positions_job.rb +84 -0
  28. data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +16 -2
  29. data/app/models/concerns/spree_cm_commissioner/line_item_open_dated_trippable.rb +50 -7
  30. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +0 -1
  31. data/app/models/concerns/spree_cm_commissioner/line_items_filter_scope.rb +5 -0
  32. data/app/models/concerns/spree_cm_commissioner/ticket_transferable.rb +15 -0
  33. data/app/models/spree_cm_commissioner/guest.rb +20 -0
  34. data/app/models/spree_cm_commissioner/line_item_decorator.rb +7 -0
  35. data/app/models/spree_cm_commissioner/notification.rb +10 -1
  36. data/app/models/spree_cm_commissioner/order_decorator.rb +13 -3
  37. data/app/models/spree_cm_commissioner/payment_decorator.rb +19 -0
  38. data/app/models/spree_cm_commissioner/payment_method_decorator.rb +8 -0
  39. data/app/models/spree_cm_commissioner/pin_code.rb +62 -9
  40. data/app/models/spree_cm_commissioner/product_decorator.rb +6 -2
  41. data/app/models/spree_cm_commissioner/reserved_block.rb +2 -0
  42. data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +4 -0
  43. data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +3 -0
  44. data/app/models/spree_cm_commissioner/taxon_ad_banner.rb +12 -0
  45. data/app/models/spree_cm_commissioner/taxon_decorator.rb +1 -0
  46. data/app/models/spree_cm_commissioner/ticket_transfer.rb +191 -0
  47. data/app/models/spree_cm_commissioner/ticket_transfer_rule.rb +88 -0
  48. data/app/models/spree_cm_commissioner/trip.rb +33 -12
  49. data/app/models/spree_cm_commissioner/user_decorator.rb +1 -0
  50. data/app/models/spree_cm_commissioner/vendor_route_metric.rb +25 -0
  51. data/app/notifications/spree_cm_commissioner/ticket_transfer_accepted_notification.rb +48 -0
  52. data/app/notifications/spree_cm_commissioner/ticket_transfer_completed_notification.rb +48 -0
  53. data/app/notifications/spree_cm_commissioner/ticket_transfer_expired_notification.rb +46 -0
  54. data/app/notifications/spree_cm_commissioner/ticket_transfer_received_notification.rb +48 -0
  55. data/app/notifications/spree_cm_commissioner/ticket_transfer_rejected_notification.rb +48 -0
  56. data/app/overrides/spree/admin/products/_form/allow_gift_transfer.html.erb.deface +9 -0
  57. data/app/overrides/spree/admin/products/_form/allow_transfer.html.erb.deface +9 -0
  58. data/app/overrides/spree/admin/shared/_order_tabs/ticket_transfers.html.erb.deface +10 -0
  59. data/app/overrides/spree/admin/shared/sub_menu/_product/ticket_transfers_tab.html.erb.deface +3 -0
  60. data/app/overrides/spree/admin/taxons/_form/assets_form.html.erb.deface +10 -1
  61. data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +31 -8
  62. data/app/queries/spree_cm_commissioner/recipient_searcher_query.rb +14 -0
  63. data/app/queries/spree_cm_commissioner/single_leg_trips_query.rb +57 -5
  64. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +4 -2
  65. data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +4 -2
  66. data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +2 -0
  67. data/app/serializers/spree/v2/tenant/dynamic_field_serializer.rb +1 -1
  68. data/app/serializers/spree_cm_commissioner/v2/storefront/dynamic_field_serializer.rb +1 -1
  69. data/app/serializers/spree_cm_commissioner/v2/storefront/guest_minimal_serializer.rb +30 -0
  70. data/app/serializers/spree_cm_commissioner/v2/storefront/guest_serializer.rb +3 -1
  71. data/app/serializers/spree_cm_commissioner/v2/storefront/line_item_minimal_serializer.rb +57 -0
  72. data/app/serializers/spree_cm_commissioner/v2/storefront/pin_code_serializer.rb +1 -1
  73. data/app/serializers/spree_cm_commissioner/v2/storefront/recipient_serializer.rb +13 -0
  74. data/app/serializers/spree_cm_commissioner/v2/storefront/reserved_block_serializer.rb +1 -1
  75. data/app/serializers/spree_cm_commissioner/v2/storefront/ticket_transfer_minimal_serializer.rb +17 -0
  76. data/app/serializers/spree_cm_commissioner/v2/storefront/ticket_transfer_serializer.rb +20 -0
  77. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_query_result_serializer.rb +4 -1
  78. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +2 -1
  79. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_stop_serializer.rb +1 -0
  80. data/app/services/spree_cm_commissioner/cart/add_guest.rb +6 -1
  81. data/app/services/spree_cm_commissioner/cart/create_guest.rb +61 -0
  82. data/app/services/spree_cm_commissioner/cart/remove_guest.rb +6 -1
  83. data/app/services/spree_cm_commissioner/check_ins/create_bulk.rb +11 -5
  84. data/app/services/spree_cm_commissioner/checkout/complete_decorator.rb +22 -0
  85. data/app/services/spree_cm_commissioner/inventory_items/reset.rb +1 -1
  86. data/app/services/spree_cm_commissioner/open_dated_trips/redeem.rb +80 -36
  87. data/app/services/spree_cm_commissioner/reserved_blocks/reserve.rb +2 -1
  88. data/app/services/spree_cm_commissioner/telegram_gateway/pin_code_sender.rb +99 -0
  89. data/app/services/spree_cm_commissioner/ticket_transfers/accept.rb +132 -0
  90. data/app/services/spree_cm_commissioner/ticket_transfers/cancel.rb +54 -0
  91. data/app/services/spree_cm_commissioner/ticket_transfers/complete.rb +68 -0
  92. data/app/services/spree_cm_commissioner/ticket_transfers/eligibility_checker.rb +99 -0
  93. data/app/services/spree_cm_commissioner/ticket_transfers/expire.rb +47 -0
  94. data/app/services/spree_cm_commissioner/ticket_transfers/fee_calculator.rb +57 -0
  95. data/app/services/spree_cm_commissioner/ticket_transfers/initiate.rb +98 -0
  96. data/app/services/spree_cm_commissioner/ticket_transfers/preview_rule.rb +38 -0
  97. data/app/services/spree_cm_commissioner/ticket_transfers/reject.rb +55 -0
  98. data/app/services/spree_cm_commissioner/ticket_transfers/transfer_guest.rb +29 -0
  99. data/app/services/spree_cm_commissioner/transit_order/create.rb +48 -14
  100. data/app/services/spree_cm_commissioner/trips/add_ons/create.rb +124 -0
  101. data/app/services/spree_cm_commissioner/trips/add_ons/update_price.rb +36 -0
  102. data/app/services/spree_cm_commissioner/trips/create_multi_leg.rb +1 -2
  103. data/app/services/spree_cm_commissioner/trips/variants/create.rb +1 -2
  104. data/app/services/spree_cm_commissioner/vendor_route_metrics/decrease_trip_count.rb +19 -0
  105. data/app/services/spree_cm_commissioner/vendor_route_metrics/increase_trip_count.rb +24 -0
  106. data/app/services/spree_cm_commissioner/waiting_room/publish_lobby_path.rb +48 -0
  107. data/app/services/spree_cm_commissioner/waiting_room_lobby_metadata_fetcher.rb +47 -0
  108. data/app/services/telegram_gateway_adapter/client.rb +124 -0
  109. data/app/views/spree/admin/guests/_table.html.erb +82 -87
  110. data/app/views/spree/admin/order_ticket_transfers/show.html.erb +7 -0
  111. data/app/views/spree/admin/shared/_ticket_transfer_details.html.erb +226 -0
  112. data/app/views/spree/admin/shared/_ticket_transfer_tabs.html.erb +14 -0
  113. data/app/views/spree/admin/system/waiting_room/show.html.erb +53 -0
  114. data/app/views/spree/admin/ticket_transfer_rules/_form.html.erb +193 -0
  115. data/app/views/spree/admin/ticket_transfer_rules/edit.html.erb +11 -0
  116. data/app/views/spree/admin/ticket_transfer_rules/index.html.erb +79 -0
  117. data/app/views/spree/admin/ticket_transfer_rules/new.html.erb +11 -0
  118. data/app/views/spree/admin/ticket_transfers/_search_form.html.erb +70 -0
  119. data/app/views/spree/admin/ticket_transfers/_tabs.html.erb +14 -0
  120. data/app/views/spree/admin/ticket_transfers/index.html.erb +74 -0
  121. data/app/views/spree/admin/ticket_transfers/show.html.erb +13 -0
  122. data/app/views/spree/admin/ticket_transfers/state_changes.html.erb +47 -0
  123. data/config/locales/en.yml +68 -6
  124. data/config/locales/km.yml +74 -4
  125. data/config/routes.rb +22 -0
  126. data/db/migrate/20260303101440_add_allow_transfer_to_spree_products.rb +5 -0
  127. data/db/migrate/20260303101907_create_cm_ticket_transfers.rb +32 -0
  128. data/db/migrate/20260303102316_add_state_to_cm_guests.rb +7 -0
  129. data/db/migrate/20260408100000_create_cm_ticket_transfer_rules.rb +28 -0
  130. data/db/migrate/20260410120000_add_transfer_type_to_cm_ticket_transfers.rb +5 -0
  131. data/db/migrate/20260422120000_add_settlement_status_to_cm_ticket_transfers.rb +6 -0
  132. data/db/migrate/20260507000000_add_index_to_cm_ticket_transfers_completed_at.rb +5 -0
  133. data/db/migrate/20260520000000_add_number_to_cm_ticket_transfers.rb +6 -0
  134. data/db/migrate/20260605120908_add_description_to_cm_dynamic_fields.rb +5 -0
  135. data/db/migrate/20260608000000_add_allow_gift_transfer_to_spree_products.rb +5 -0
  136. data/db/migrate/20260608000000_create_cm_vendor_route_metrics.rb +25 -0
  137. data/db/migrate/20260608152251_add_gender_to_cm_reserved_blocks.rb +5 -0
  138. data/db/migrate/20260610000001_add_delivery_channel_to_cm_pin_codes.rb +6 -0
  139. data/db/migrate/20260610000001_drop_is_open_dated_from_cm_trips.rb +6 -0
  140. data/db/migrate/20260614000001_change_code_limit_in_cm_pin_codes.rb +5 -0
  141. data/db/migrate/20260615000002_add_index_to_code_on_cm_pin_codes.rb +5 -0
  142. data/lib/spree_cm_commissioner/test_helper/factories/ticket_transfer_factory.rb +18 -0
  143. data/lib/spree_cm_commissioner/test_helper/factories/vendor_route_metric_factory.rb +10 -0
  144. data/lib/spree_cm_commissioner/transit/trip_form.rb +0 -13
  145. data/lib/spree_cm_commissioner/trip_query_result.rb +46 -6
  146. data/lib/spree_cm_commissioner/trip_result.rb +9 -7
  147. data/lib/spree_cm_commissioner/version.rb +1 -1
  148. data/lib/tasks/backfill_route_metrics.rake +21 -0
  149. data/lib/tasks/backfill_vendor_route_metrics.rake +23 -0
  150. metadata +89 -6
  151. data/app/services/spree_cm_commissioner/trips/create_open_dated_trip.rb +0 -98
  152. data/app/services/spree_cm_commissioner/trips/update_open_dated_trip.rb +0 -67
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0774f6d0c4fad7ae396f14326fd6d0965164b74d89f355c3966bc8d1fd9563f9
4
- data.tar.gz: 002d84c9f1273b54f01d5a25bdecc5f3b475a12e07b32d8e1846c19107fe776f
3
+ metadata.gz: 6b4cb4f14de6554d09bdd97872267ca1f23e73d144401b49a8969203e601b910
4
+ data.tar.gz: 454758bb34c6df3a4f1e2272b65bb19f57e7d829c06c045d7f748a72a56cb42f
5
5
  SHA512:
6
- metadata.gz: 260d54db1f9dd63dbfef02edf2944d02e2c582972b8903815462e438be9cbfb6f5561a7b4a4b64c2cda4af18908c8c7a91958213c06764d1f7ee4d8e2c16dec3
7
- data.tar.gz: 003d6ecd547b0215851266f06e823f2f7c5e7a8da35f5b4a8051784855a4d095d088b79f700dcadca8820d561d7a9989a3ee0d9eb7940943c3c5777cc7a4742c
6
+ metadata.gz: 3dea963b405471ddcf3a1b7819bab6d7ffca7b76f0567b6a0092262b5b8d980db83fc052c4913f06ff42921727bc4168c2c044beeeb38ddc65ef242071918f34
7
+ data.tar.gz: f9a841e7c01222124a041e65adf42b7e6d57b53fdc8e731c08e595dbceb601fb45fbe7ef6850df709d454b75220c4e58da2af1f8c2d0bb37d58ab00bc4c2031c
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.8.3)
37
+ spree_cm_commissioner (2.8.4.pre.pre1)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -55,7 +55,7 @@ module Spree
55
55
  if result.success?
56
56
  flash[:success] = "Guest #{@guest.full_name} has been checked in."
57
57
  else
58
- flash[:error] = result.message.to_s.titleize
58
+ flash[:error] = result.error.value.to_s.titleize
59
59
  end
60
60
 
61
61
  redirect_to collection_url
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ module Admin
3
+ class OrderTicketTransfersController < Spree::Admin::ResourceController
4
+ include Spree::Admin::OrderConcern
5
+
6
+ helper SpreeCmCommissioner::Admin::TicketTransfersHelper
7
+
8
+ before_action :load_order
9
+
10
+ def show
11
+ @ticket_transfer = model_class.includes(:order, from_guest: :event).find(params[:id])
12
+ end
13
+
14
+ private
15
+
16
+ def model_class
17
+ SpreeCmCommissioner::TicketTransfer
18
+ end
19
+
20
+ def object_name
21
+ 'spree_cm_commissioner_ticket_transfer'
22
+ end
23
+ end
24
+ end
25
+ end
@@ -92,7 +92,7 @@ module Spree
92
92
  .call
93
93
  .index_by(&:inventory_item_id)
94
94
  @reserved_stocks = Spree::LineItem
95
- .complete
95
+ .affecting_inventory
96
96
  .where(variant_id: @variants.pluck(:id))
97
97
  .group('spree_line_items.variant_id')
98
98
  .sum(:quantity)
@@ -4,7 +4,10 @@ module Spree
4
4
  class WaitingRoomController < Spree::Admin::BaseController
5
5
  def show
6
6
  @fetcher = SpreeCmCommissioner::WaitingRoomSystemMetadataFetcher.new
7
+ @lobby_fetcher = SpreeCmCommissioner::WaitingRoomLobbyMetadataFetcher.new
8
+
7
9
  @fetcher.load_document_data
10
+ @lobby_fetcher.load_document_data
8
11
 
9
12
  @active_sesions_count = SpreeCmCommissioner::WaitingRoomSession.active.count
10
13
  end
@@ -29,6 +32,18 @@ module Spree
29
32
 
30
33
  redirect_back fallback_location: admin_system_waiting_room_path
31
34
  end
35
+
36
+ def publish_lobby_path
37
+ result = SpreeCmCommissioner::WaitingRoom::PublishLobbyPath.call
38
+
39
+ if result.success?
40
+ flash[:success] = "Published waiting guests records path: #{result.value[:records_path]}"
41
+ else
42
+ flash[:error] = result.error.to_s
43
+ end
44
+
45
+ redirect_back fallback_location: admin_system_waiting_room_path
46
+ end
32
47
  end
33
48
  end
34
49
  end
@@ -27,6 +27,10 @@ module Spree
27
27
  remove_asset(@taxon.home_banner)
28
28
  end
29
29
 
30
+ def remove_ad_banner
31
+ remove_asset(@taxon.ad_banner)
32
+ end
33
+
30
34
  def remove_video_banner
31
35
  remove_asset(@taxon.video_banner)
32
36
  end
@@ -48,6 +52,7 @@ module Spree
48
52
  @taxon.build_app_banner(attachment: permitted_resource_params.delete(:app_banner)) if permitted_resource_params[:app_banner]
49
53
  @taxon.build_web_banner(attachment: permitted_resource_params.delete(:web_banner)) if permitted_resource_params[:web_banner]
50
54
  @taxon.build_home_banner(attachment: permitted_resource_params.delete(:home_banner)) if permitted_resource_params[:home_banner]
55
+ @taxon.build_ad_banner(attachment: permitted_resource_params.delete(:ad_banner)) if permitted_resource_params[:ad_banner]
51
56
  @taxon.build_video_banner(attachment: permitted_resource_params.delete(:video_banner)) if permitted_resource_params[:video_banner]
52
57
  end
53
58
  end
@@ -0,0 +1,47 @@
1
+ module Spree
2
+ module Admin
3
+ class TicketTransferRulesController < Spree::Admin::ResourceController
4
+ def index
5
+ @search = model_class.ransack(params[:q])
6
+ @ticket_transfer_rules = @search.result
7
+ .order(ruleable_type: :asc, ruleable_id: :asc, currency: :asc)
8
+ .page(params[:page])
9
+ .per(25)
10
+ end
11
+
12
+ private
13
+
14
+ def model_class
15
+ SpreeCmCommissioner::TicketTransferRule
16
+ end
17
+
18
+ def object_name
19
+ 'spree_cm_commissioner_ticket_transfer_rule'
20
+ end
21
+
22
+ def collection_url
23
+ admin_ticket_transfer_rules_url
24
+ end
25
+
26
+ def location_after_save
27
+ edit_admin_ticket_transfer_rule_path(@object)
28
+ end
29
+
30
+ def permitted_resource_params
31
+ params.require(:spree_cm_commissioner_ticket_transfer_rule).permit(
32
+ :ruleable_type,
33
+ :ruleable_id,
34
+ :active,
35
+ :currency,
36
+ :fee_percentage,
37
+ :fee_flat_amount,
38
+ :cooldown_hours,
39
+ :max_transfers_per_ticket,
40
+ :event_cutoff_hours,
41
+ :min_price,
42
+ :max_price
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,61 @@
1
+ module Spree
2
+ module Admin
3
+ class TicketTransfersController < Spree::Admin::ResourceController
4
+ helper SpreeCmCommissioner::Admin::TicketTransfersHelper
5
+
6
+ def index
7
+ @search = model_class.includes(:from_user, :to_user, :from_guest).ransack(params[:q])
8
+ @ticket_transfers = @search.result.order(created_at: :desc).page(params[:page]).per(15)
9
+ end
10
+
11
+ def show
12
+ @ticket_transfer = model_class.includes(:order, from_guest: [:event, { line_item: :order }]).find(params[:id])
13
+ end
14
+
15
+ def state_changes
16
+ @ticket_transfer = model_class.find(params[:id])
17
+ @state_changes = @ticket_transfer.state_changes.includes(:user).order(created_at: :desc)
18
+ end
19
+
20
+ def cancel
21
+ @ticket_transfer = model_class.find(params[:id])
22
+ result = SpreeCmCommissioner::TicketTransfers::Cancel.call(
23
+ ticket_transfer: @ticket_transfer,
24
+ acting_user: try_spree_current_user
25
+ )
26
+
27
+ if result.success?
28
+ flash[:success] = 'Ticket transfer has been cancelled' # rubocop:disable Rails/I18nLocaleTexts
29
+ else
30
+ flash[:error] = result.error.value
31
+ end
32
+ redirect_to admin_ticket_transfer_path(@ticket_transfer)
33
+ end
34
+
35
+ def update_settlement_status
36
+ @ticket_transfer = model_class.find(params[:id])
37
+ new_status = params[:settlement_status]
38
+
39
+ unless @ticket_transfer.purchase? && @ticket_transfer.completed?
40
+ flash[:error] = 'Settlement status can only be updated for completed purchase transfers' # rubocop:disable Rails/I18nLocaleTexts
41
+ redirect_to admin_ticket_transfer_path(@ticket_transfer)
42
+ return
43
+ end
44
+
45
+ if @ticket_transfer.update(settlement_status: new_status)
46
+ @ticket_transfer.log_settlement_status_changes(user_id: try_spree_current_user&.id)
47
+ flash[:success] = "Settlement status updated to #{new_status}"
48
+ else
49
+ flash[:error] = @ticket_transfer.errors.full_messages.to_sentence.presence || 'Failed to update settlement status'
50
+ end
51
+ redirect_to admin_ticket_transfer_path(@ticket_transfer)
52
+ end
53
+
54
+ private
55
+
56
+ def model_class
57
+ SpreeCmCommissioner::TicketTransfer
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,32 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ module Account
6
+ class RecipientsController < ::Spree::Api::V2::ResourceController
7
+ before_action :require_spree_current_user
8
+
9
+ def show
10
+ identifier = params[:q].to_s.strip
11
+ return render_error_payload('Missing q parameter', 400) if identifier.blank?
12
+
13
+ user = SpreeCmCommissioner::RecipientSearcherQuery.new(identifier, MultiTenant.current_tenant_id).call
14
+
15
+ if user
16
+ render_serialized_payload { serialize_resource(user) }
17
+ else
18
+ render_error_payload('User not found', 404)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def resource_serializer
25
+ SpreeCmCommissioner::V2::Storefront::RecipientSerializer
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -31,13 +31,15 @@ module Spree
31
31
  def create
32
32
  spree_authorize! :update, spree_current_order, order_token
33
33
 
34
- resource = scope.new(guest_params)
34
+ result = SpreeCmCommissioner::Cart::CreateGuest.call(
35
+ line_item: parent,
36
+ options: guest_params
37
+ )
35
38
 
36
- if resource.save
37
- resource.save_and_move_to_next_stage
38
- render_serialized_payload(201) { serialize_resource(resource) }
39
+ if result.success?
40
+ render_serialized_payload(201) { serialize_resource(result.value) }
39
41
  else
40
- render_error_payload(resource.errors, 422)
42
+ render_error_payload(result.value.errors, 422)
41
43
  end
42
44
  end
43
45
 
@@ -0,0 +1,32 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ class TicketTransferAcceptancesController < ::Spree::Api::V2::ResourceController
6
+ before_action :require_spree_current_user
7
+
8
+ def create
9
+ ticket_transfer = SpreeCmCommissioner::TicketTransfer.find_by!(token: params[:ticket_transfer_id])
10
+
11
+ result = SpreeCmCommissioner::TicketTransfers::Accept.call(
12
+ ticket_transfer: ticket_transfer,
13
+ buyer: spree_current_user
14
+ )
15
+
16
+ if result.success?
17
+ render_serialized_payload(201) { serialize_resource(result.value[:ticket_transfer]) }
18
+ else
19
+ render_error_payload(result.error)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def resource_serializer
26
+ SpreeCmCommissioner::V2::Storefront::TicketTransferSerializer
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,117 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ class TicketTransfersController < ::Spree::Api::V2::ResourceController
6
+ before_action :require_spree_current_user
7
+
8
+ def index
9
+ render_serialized_payload do
10
+ serialize_collection(paginated_collection)
11
+ end
12
+ end
13
+
14
+ def show
15
+ ticket_transfer = model_class.includes(:order, from_guest: [:event, { line_item: :product }]).find_by!(token: params[:id])
16
+ return render_error_payload(I18n.t('ticket_transfer.unauthorized_view'), 403) unless authorized_to_view?(ticket_transfer)
17
+
18
+ render_serialized_payload { serialize_resource(ticket_transfer) }
19
+ end
20
+
21
+ def preview_rule
22
+ result = SpreeCmCommissioner::TicketTransfers::PreviewRule.call(
23
+ from_guest_id: params[:guest_id],
24
+ currency: params[:currency] || 'USD'
25
+ )
26
+
27
+ if result.success?
28
+ render json: { data: result.value }
29
+ else
30
+ render_error_payload(result.error)
31
+ end
32
+ end
33
+
34
+ def create
35
+ result = SpreeCmCommissioner::TicketTransfers::Initiate.call(
36
+ from_user_id: spree_current_user.id,
37
+ to_user_id: ticket_transfer_params[:to_user_id],
38
+ from_guest_id: ticket_transfer_params[:guest_id],
39
+ price: ticket_transfer_params[:price] || 0,
40
+ currency: ticket_transfer_params[:currency] || 'USD',
41
+ expires_at: ticket_transfer_params[:expires_at],
42
+ transfer_type: ticket_transfer_params[:transfer_type] || 'purchase'
43
+ )
44
+
45
+ if result.success?
46
+ render_serialized_payload(201) { serialize_resource(result.value[:ticket_transfer]) }
47
+ else
48
+ render_error_payload(result.error)
49
+ end
50
+ end
51
+
52
+ def cancel
53
+ ticket_transfer = model_class.find_by!(token: params[:id])
54
+
55
+ result = SpreeCmCommissioner::TicketTransfers::Cancel.call(
56
+ ticket_transfer: ticket_transfer,
57
+ acting_user: spree_current_user
58
+ )
59
+
60
+ if result.success?
61
+ render_serialized_payload { serialize_resource(result.value[:ticket_transfer]) }
62
+ else
63
+ render_error_payload(result.error)
64
+ end
65
+ end
66
+
67
+ def reject
68
+ ticket_transfer = model_class.find_by!(token: params[:id])
69
+
70
+ result = SpreeCmCommissioner::TicketTransfers::Reject.call(
71
+ ticket_transfer: ticket_transfer,
72
+ acting_user: spree_current_user
73
+ )
74
+
75
+ if result.success?
76
+ render_serialized_payload { serialize_resource(result.value[:ticket_transfer]) }
77
+ else
78
+ render_error_payload(result.error)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def model_class
85
+ SpreeCmCommissioner::TicketTransfer
86
+ end
87
+
88
+ def collection
89
+ SpreeCmCommissioner::TicketTransfer.includes(:order,
90
+ from_guest: [:event, { line_item: :product }]
91
+ ).where(from_user_id: spree_current_user.id)
92
+ end
93
+
94
+ def resource_serializer
95
+ SpreeCmCommissioner::V2::Storefront::TicketTransferSerializer
96
+ end
97
+
98
+ def collection_serializer
99
+ SpreeCmCommissioner::V2::Storefront::TicketTransferSerializer
100
+ end
101
+
102
+ def ticket_transfer_params
103
+ params.require(:ticket_transfer).permit(:to_user_id, :guest_id, :price, :currency, :expires_at, :transfer_type)
104
+ end
105
+
106
+ # When sending to a specific user, only sender and that buyer can access.
107
+ def authorized_to_view?(ticket_transfer)
108
+ return true if ticket_transfer.to_user_id.blank?
109
+
110
+ current_user_id = spree_current_user&.id
111
+ ticket_transfer.from_user_id == current_user_id || ticket_transfer.to_user_id == current_user_id
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -11,6 +11,7 @@ module Spree
11
11
  # - inbound_date: Date of inbound transit (optional)
12
12
  # - outbound_legs: Array of outbound leg details (required)
13
13
  # - inbound_legs: Array of inbound leg details (optional)
14
+ # - include_return: Whether to add the trip's "return included" add-on as a line item (optional)
14
15
  def create
15
16
  @outbound_legs = params[:outbound_legs].is_a?(Array) && params[:outbound_legs].any? ? build_legs(:outbound, params[:outbound_legs]) : []
16
17
  @inbound_legs = params[:inbound_legs].is_a?(Array) && params[:inbound_legs].any? ? build_legs(:inbound, params[:inbound_legs]) : []
@@ -20,7 +21,8 @@ module Spree
20
21
  inbound_date: params[:inbound_date]&.to_date,
21
22
  outbound_legs: @outbound_legs,
22
23
  inbound_legs: @inbound_legs,
23
- user: spree_current_user
24
+ user: spree_current_user,
25
+ include_return: ActiveModel::Type::Boolean.new.cast(params[:include_return])
24
26
  )
25
27
 
26
28
  if result.success?
@@ -11,6 +11,7 @@ module Spree
11
11
  # - inbound_date: Date of inbound transit (optional)
12
12
  # - outbound_legs: Array of outbound leg details (required)
13
13
  # - inbound_legs: Array of inbound leg details (optional)
14
+ # - include_return: Whether to add the trip's "return included" add-on as a line item (optional)
14
15
  def create
15
16
  @outbound_legs = params[:outbound_legs].is_a?(Array) && params[:outbound_legs].any? ? build_legs(:outbound, params[:outbound_legs]) : []
16
17
  @inbound_legs = params[:inbound_legs].is_a?(Array) && params[:inbound_legs].any? ? build_legs(:inbound, params[:inbound_legs]) : []
@@ -20,7 +21,8 @@ module Spree
20
21
  inbound_date: params[:inbound_date]&.to_date,
21
22
  outbound_legs: @outbound_legs,
22
23
  inbound_legs: @inbound_legs,
23
- user: spree_current_user
24
+ user: spree_current_user,
25
+ include_return: ActiveModel::Type::Boolean.new.cast(params[:include_return])
24
26
  )
25
27
 
26
28
  if result.success?
@@ -25,6 +25,17 @@ module Spree
25
25
  def resource_serializer
26
26
  Spree::V2::Tenant::TripSerializer
27
27
  end
28
+
29
+ # override
30
+ def serializer_params
31
+ super.merge(
32
+ include_details: include_details?
33
+ )
34
+ end
35
+
36
+ def include_details?
37
+ params.fetch(:include_details, 'true') == 'true'
38
+ end
28
39
  end
29
40
  end
30
41
  end
@@ -40,9 +40,13 @@ module SpreeCmCommissioner
40
40
 
41
41
  # override
42
42
  def cancel
43
- internal_note = params.dig(:order, :internal_note)
44
- @order.canceled_by(try_spree_current_user, cancellation_reason: internal_note)
45
- flash[:success] = Spree.t(:order_canceled)
43
+ if @order.guests_in_transfer?
44
+ flash[:error] = Spree.t('admin.orders.cannot_cancel_guest_transfer')
45
+ else
46
+ internal_note = params.dig(:order, :internal_note)
47
+ @order.canceled_by(try_spree_current_user, cancellation_reason: internal_note)
48
+ flash[:success] = Spree.t(:order_canceled)
49
+ end
46
50
  redirect_back fallback_location: spree.edit_admin_order_url(@order)
47
51
  end
48
52
 
@@ -61,6 +61,16 @@ module SpreeCmCommissioner
61
61
  'badge-info'
62
62
  end
63
63
  end
64
+
65
+ def guest_state_badge(guest)
66
+ badge_class = case guest.state
67
+ when 'active' then 'badge-success'
68
+ when 'locked' then 'badge-warning'
69
+ when 'transferred' then 'badge-secondary'
70
+ else 'badge-info'
71
+ end
72
+ content_tag(:span, guest.state.humanize, class: "badge #{badge_class}")
73
+ end
64
74
  end
65
75
  end
66
76
  end
@@ -0,0 +1,32 @@
1
+ module SpreeCmCommissioner
2
+ module Admin
3
+ module TicketTransfersHelper
4
+ def ticket_transfer_state_badge(state)
5
+ badge_class = case state
6
+ when 'pending' then 'badge-warning'
7
+ when 'accepted' then 'badge-info'
8
+ when 'completed' then 'badge-success'
9
+ when 'rejected' then 'badge-danger'
10
+ when 'expired' then 'badge-danger' # rubocop:disable Lint/DuplicateBranch
11
+ when 'cancelled' then 'badge-secondary'
12
+ else 'badge-secondary' # rubocop:disable Lint/DuplicateBranch
13
+ end
14
+
15
+ content_tag(:span, state.humanize, class: "badge #{badge_class}")
16
+ end
17
+
18
+ def ticket_transfer_settlement_status_badge(ticket_transfer)
19
+ badge_class = case ticket_transfer.settlement_status
20
+ when 'pending' then 'badge-warning'
21
+ when 'ready' then 'badge-info'
22
+ when 'disputed' then 'badge-danger'
23
+ when 'resolved' then 'badge-primary'
24
+ when 'completed' then 'badge-success'
25
+ else 'badge-secondary'
26
+ end
27
+
28
+ content_tag(:span, ticket_transfer.settlement_status.humanize, class: "badge #{badge_class}")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -6,7 +6,7 @@ module SpreeCmCommissioner
6
6
  context.fail!(message: I18n.t('pincode_sender.pincode.blank')) if context.pin_code.nil?
7
7
 
8
8
  if context.pin_code.phone_number?
9
- send_sms
9
+ send_phone_otp
10
10
  else
11
11
  send_email
12
12
  end
@@ -16,18 +16,73 @@ module SpreeCmCommissioner
16
16
 
17
17
  private
18
18
 
19
- def send_sms
19
+ # Channel selection for phone-number OTPs lives here so the routing is
20
+ # easy to find and reason about. Telegram Gateway is primary; SMS is the fallback.
21
+ def send_phone_otp
22
+ if telegram_gateway_available?
23
+ send_via_telegram_gateway
24
+ else
25
+ send_via_sms
26
+ end
27
+ end
28
+
29
+ def telegram_gateway_available?
30
+ return false unless telegram_gateway_enabled?
31
+ return false if telegram_gateway_api_key.blank?
32
+
33
+ ability = telegram_gateway_adapter.check_send_ability(context.pin_code.contact)
34
+ context.telegram_gateway_request_id = ability.request_id if ability.ok?
35
+ ability.ok?
36
+ rescue TelegramGatewayAdapter::Error, Faraday::Error
37
+ false
38
+ end
39
+
40
+ def send_via_telegram_gateway
20
41
  from_number = sms_from_number
21
42
  return if from_number.blank?
22
43
 
23
- options = {
44
+ # Save the chosen channel + the Telegram request_id synchronously so
45
+ # (a) the API response already reflects the channel for the mobile UI,
46
+ # (b) verification can reach Telegram even if the user submits the OTP
47
+ # before the async send job has run. For Telegram-delivered codes
48
+ # the `code` column stores Telegram's request_id (Telegram generates
49
+ # and holds the actual OTP).
50
+ context.pin_code.update!(
51
+ delivery_channel: :telegram_gateway,
52
+ code: context.telegram_gateway_request_id
53
+ )
54
+
55
+ SpreeCmCommissioner::TelegramGateway::PinCodeSenderJob.perform_later(
56
+ pin_code_id: context.pin_code.id,
57
+ from: from_number,
58
+ request_id: context.telegram_gateway_request_id,
59
+ tenant_id: context.pin_code&.application&.tenant_id
60
+ )
61
+ end
62
+
63
+ def send_via_sms
64
+ from_number = sms_from_number
65
+ return if from_number.blank?
66
+
67
+ SpreeCmCommissioner::SmsPinCodeJob.perform_later(
68
+ pin_code_id: context.pin_code.id,
24
69
  from: from_number,
25
70
  to: context.pin_code.contact,
26
71
  body: I18n.t('pincode_sender.sms.body', code: context.pin_code.code, readable_type: context.pin_code.readable_type),
27
72
  tenant_id: context.pin_code&.application&.tenant_id
28
- }
73
+ )
74
+ end
75
+
76
+ def telegram_gateway_enabled?
77
+ ENV['TELEGRAM_GATEWAY_ENABLED'] == 'yes'
78
+ end
29
79
 
30
- SpreeCmCommissioner::SmsPinCodeJob.perform_later(options)
80
+ def telegram_gateway_api_key
81
+ @telegram_gateway_api_key ||= ENV.fetch('TELEGRAM_GATEWAY_API_KEY', nil)
82
+ end
83
+
84
+ def telegram_gateway_adapter
85
+ @telegram_gateway_adapter ||= TelegramGatewayAdapter::Client.new
31
86
  end
32
87
 
33
88
  def sms_from_number
@@ -38,6 +93,8 @@ module SpreeCmCommissioner
38
93
  end
39
94
 
40
95
  def send_email
96
+ context.pin_code.update!(delivery_channel: :email)
97
+
41
98
  SpreeCmCommissioner::PinCodeMailer.send_pin_code(
42
99
  context.pin_code.id,
43
100
  context.pin_code.readable_type,