spree_cm_commissioner 2.5.1.pre.pre1 → 2.5.1.pre.pre2

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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/admin/guests_controller.rb +5 -5
  4. data/app/controllers/spree/api/chatrace/check_ins_controller.rb +6 -4
  5. data/app/controllers/spree/api/chatrace/guests_controller.rb +5 -18
  6. data/app/controllers/spree/api/v2/operator/check_in_bulks_controller.rb +6 -5
  7. data/app/controllers/spree/api/v2/operator/check_in_sessions_controller.rb +32 -0
  8. data/app/controllers/spree/api/v2/operator/check_ins_controller.rb +5 -5
  9. data/app/controllers/spree/api/v2/operator/event_qrs_controller.rb +2 -0
  10. data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +1 -1
  11. data/app/controllers/spree/api/v2/storefront/self_check_in_controller.rb +7 -7
  12. data/app/controllers/spree/api/v2/storefront/transit/draft_orders_controller.rb +4 -4
  13. data/app/controllers/spree/api/v2/tenant/popular_route_places_controller.rb +60 -0
  14. data/app/controllers/spree/api/v2/tenant/routes_controller.rb +50 -0
  15. data/app/controllers/spree/api/v2/tenant/transit/draft_orders_controller.rb +46 -0
  16. data/app/controllers/spree/events/guests_controller.rb +9 -10
  17. data/app/controllers/spree/transit/trips_controller.rb +3 -3
  18. data/app/finders/spree_cm_commissioner/places/find_with_route.rb +12 -12
  19. data/app/finders/spree_cm_commissioner/route_metrics/find_popular.rb +44 -0
  20. data/app/finders/spree_cm_commissioner/routes/find.rb +94 -0
  21. data/app/finders/spree_cm_commissioner/routes/find_popular.rb +20 -35
  22. data/app/interactors/spree_cm_commissioner/event_qr_generator.rb +2 -1
  23. data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +11 -4
  24. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +10 -1
  25. data/app/interactors/spree_cm_commissioner/trip_clone_creator.rb +4 -3
  26. data/app/interactors/spree_cm_commissioner/trip_stops_creator.rb +2 -2
  27. data/app/jobs/spree_cm_commissioner/guests/preload_check_in_session_ids_job.rb +10 -0
  28. data/app/jobs/spree_cm_commissioner/route_metrics/decrease_trip_count_job.rb +10 -0
  29. data/app/jobs/spree_cm_commissioner/route_metrics/increase_fulfilled_order_count_job.rb +10 -0
  30. data/app/jobs/spree_cm_commissioner/route_metrics/increase_order_count_job.rb +10 -0
  31. data/app/jobs/spree_cm_commissioner/route_metrics/increase_trip_count_job.rb +10 -0
  32. data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +2 -2
  33. data/app/models/concerns/spree_cm_commissioner/route_order_countable.rb +2 -2
  34. data/app/models/spree_cm_commissioner/check_in.rb +34 -7
  35. data/app/models/spree_cm_commissioner/check_in_session.rb +8 -3
  36. data/app/models/spree_cm_commissioner/guest.rb +60 -28
  37. data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +2 -2
  38. data/app/models/spree_cm_commissioner/place.rb +5 -8
  39. data/app/models/spree_cm_commissioner/pricing_actions/create_guest_adjustments.rb +52 -0
  40. data/app/models/spree_cm_commissioner/pricing_actions/create_line_item_adjustments.rb +6 -0
  41. data/app/models/spree_cm_commissioner/pricing_rule.rb +4 -0
  42. data/app/models/spree_cm_commissioner/pricing_rules/age_group.rb +45 -0
  43. data/app/models/spree_cm_commissioner/pricing_rules/nationality.rb +21 -2
  44. data/app/models/spree_cm_commissioner/pricing_rules/nationality_group.rb +41 -0
  45. data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
  46. data/app/models/spree_cm_commissioner/route.rb +46 -5
  47. data/app/models/spree_cm_commissioner/route_metric.rb +21 -0
  48. data/app/models/spree_cm_commissioner/route_photo.rb +12 -0
  49. data/app/models/spree_cm_commissioner/taxon_decorator.rb +1 -0
  50. data/app/models/spree_cm_commissioner/trip.rb +8 -33
  51. data/app/models/spree_cm_commissioner/trip_stop.rb +16 -2
  52. data/app/models/spree_cm_commissioner/vendor_decorator.rb +3 -1
  53. data/app/queries/spree_cm_commissioner/trip_query.rb +2 -2
  54. data/app/serializers/spree/v2/tenant/transit_cart_serializer.rb +11 -0
  55. data/app/serializers/spree_cm_commissioner/v2/operator/check_in_serializer.rb +1 -2
  56. data/app/serializers/spree_cm_commissioner/v2/operator/check_in_session_serializer.rb +9 -0
  57. data/app/serializers/spree_cm_commissioner/v2/operator/guest_serializer.rb +1 -1
  58. data/app/serializers/spree_cm_commissioner/v2/storefront/guest_serializer.rb +2 -1
  59. data/app/serializers/spree_cm_commissioner/v2/storefront/route_serializer.rb +3 -1
  60. data/app/serializers/spree_cm_commissioner/v2/storefront/transit_line_item_serializer.rb +17 -0
  61. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_stop_serializer.rb +1 -1
  62. data/app/services/spree_cm_commissioner/check_ins/create_bulk.rb +65 -0
  63. data/app/services/spree_cm_commissioner/check_ins/destroy_bulk.rb +54 -0
  64. data/app/services/spree_cm_commissioner/guests/preload_check_in_session_ids.rb +22 -0
  65. data/app/services/spree_cm_commissioner/pricing_models/create_with_rule_groups.rb +56 -0
  66. data/app/services/spree_cm_commissioner/pricing_models/update_with_rule_groups.rb +69 -0
  67. data/app/services/spree_cm_commissioner/pricing_rules/build_params.rb +16 -7
  68. data/app/services/spree_cm_commissioner/route_metrics/decrease_trip_count.rb +31 -0
  69. data/app/services/spree_cm_commissioner/{routes/increment_fulfilled_order_count.rb → route_metrics/increase_fulfilled_order_count.rb} +3 -3
  70. data/app/services/spree_cm_commissioner/{routes/increment_order_count.rb → route_metrics/increase_order_count.rb} +3 -3
  71. data/app/services/spree_cm_commissioner/route_metrics/increase_trip_count.rb +31 -0
  72. data/app/services/spree_cm_commissioner/{routes/base_update_order_metrics.rb → route_metrics/update_route_metrics.rb} +11 -16
  73. data/app/services/spree_cm_commissioner/routes/create.rb +51 -0
  74. data/app/services/spree_cm_commissioner/routes/update.rb +25 -0
  75. data/app/{interactors/spree_cm_commissioner/transit/draft_order_creator.rb → services/spree_cm_commissioner/transit_order/create.rb} +13 -16
  76. data/app/services/spree_cm_commissioner/trips/create_single_leg.rb +123 -0
  77. data/app/services/spree_cm_commissioner/trips/service_calendars/create_or_update.rb +54 -0
  78. data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +88 -0
  79. data/app/services/spree_cm_commissioner/trips/variants/create.rb +103 -0
  80. data/app/views/spree/transit/trip_stops/index.html.erb +4 -2
  81. data/app/views/spree_cm_commissioner/guest_mailer/send_ticket_to_guest.html.erb +0 -1
  82. data/config/initializers/spree_permitted_attributes.rb +11 -0
  83. data/config/locales/en.yml +2 -0
  84. data/config/locales/km.yml +2 -0
  85. data/config/routes.rb +8 -0
  86. data/db/migrate/20251224033103_migrate_cm_routes_to_cm_route_metrics.rb +17 -0
  87. data/db/migrate/20251224033910_migrate_cm_vendor_routes_to_cm_routes.rb +30 -0
  88. data/db/migrate/20251225100000_add_age_group_to_cm_guests.rb +6 -0
  89. data/db/migrate/20260105024742_add_type_to_cm_pricing_actions.rb +11 -0
  90. data/db/migrate/20260105072450_migrate_cm_trip_stops_to_support_trip_connection.rb +12 -0
  91. data/db/migrate/20260108101406_add_allow_booking_to_cm_trips.rb +5 -0
  92. data/db/migrate/20260121024645_add_nationality_group_to_cm_guests.rb +5 -0
  93. data/docs/pricing_model/age_group.md +40 -0
  94. data/docs/pricing_model/nationality_group.md +35 -0
  95. data/lib/spree_cm_commissioner/test_helper/factories/check_in_factory.rb +3 -1
  96. data/lib/spree_cm_commissioner/test_helper/factories/check_in_session_factory.rb +26 -0
  97. data/lib/spree_cm_commissioner/test_helper/factories/guest_factory.rb +4 -0
  98. data/lib/spree_cm_commissioner/test_helper/factories/pricing_action_factory.rb +4 -0
  99. data/lib/spree_cm_commissioner/test_helper/factories/pricing_rule_factory.rb +45 -1
  100. data/lib/spree_cm_commissioner/test_helper/factories/route_factory.rb +7 -6
  101. data/lib/spree_cm_commissioner/test_helper/factories/route_metric_factory.rb +12 -0
  102. data/lib/spree_cm_commissioner/test_helper/factories/route_photo_factory.rb +5 -0
  103. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +4 -1
  104. data/lib/spree_cm_commissioner/test_helper/factories/trip_stop_factory.rb +3 -1
  105. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +2 -0
  106. data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +22 -0
  107. data/lib/spree_cm_commissioner/transit/route_stop.rb +61 -0
  108. data/lib/spree_cm_commissioner/transit/route_stop_collection.rb +175 -0
  109. data/lib/spree_cm_commissioner/transit/trip_form.rb +81 -0
  110. data/lib/spree_cm_commissioner/transit/trip_stop_form.rb +61 -0
  111. data/lib/spree_cm_commissioner/version.rb +1 -1
  112. data/lib/spree_cm_commissioner.rb +4 -0
  113. metadata +54 -20
  114. data/app/interactors/spree_cm_commissioner/check_in_bulk_creator.rb +0 -71
  115. data/app/interactors/spree_cm_commissioner/check_in_destroyer.rb +0 -43
  116. data/app/jobs/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_job.rb +0 -10
  117. data/app/jobs/spree_cm_commissioner/transit/route_order_count_incrementer_job.rb +0 -10
  118. data/app/jobs/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_job.rb +0 -13
  119. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_decrementer_job.rb +0 -10
  120. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_incrementer_job.rb +0 -10
  121. data/app/models/concerns/spree_cm_commissioner/route_trip_count_callbacks.rb +0 -48
  122. data/app/models/spree_cm_commissioner/trip_connection.rb +0 -36
  123. data/app/models/spree_cm_commissioner/vendor_route.rb +0 -9
  124. data/app/services/spree_cm_commissioner/routes/decrement_previous_trip_count.rb +0 -30
  125. data/app/services/spree_cm_commissioner/routes/decrement_trip_count.rb +0 -33
  126. data/app/services/spree_cm_commissioner/routes/increment_trip_count.rb +0 -33
  127. data/lib/spree_cm_commissioner/test_helper/factories/trip_connection_factory.rb +0 -6
@@ -0,0 +1,94 @@
1
+ # Finder for Route records with common filtering options.
2
+ # Usage:
3
+ # finder = SpreeCmCommissioner::Routes::Find.new(vendor: vendor, params: params)
4
+ # finder = SpreeCmCommissioner::Routes::Find.new(tenant: tenant, params: params)
5
+ # finder.execute
6
+ # returns ActiveRecord::Relation of SpreeCmCommissioner::Route
7
+
8
+ module SpreeCmCommissioner
9
+ module Routes
10
+ class Find
11
+ def initialize(vendor: nil, tenant: nil, params: {})
12
+ @vendor = vendor
13
+ @tenant = tenant
14
+ @params = params || {}
15
+
16
+ @ids = Array(@params[:ids]).map(&:to_s)
17
+ @name = @params.dig(:filter, :name) || @params[:name]
18
+ @route_type = @params.dig(:filter, :route_type) || @params[:route_type]
19
+ @origin_place_id = @params.dig(:filter, :origin_place_id)
20
+ @destination_place_id = @params.dig(:filter, :destination_place_id)
21
+ @query = @params.dig(:filter, :query) || @params[:query]
22
+ @limit = (@params[:limit] || @params.dig(:page, :limit) || 50).to_i
23
+ @order = @params[:order] || 'created_at DESC'
24
+ end
25
+
26
+ def execute
27
+ results = base_scope
28
+ results = by_ids(results) if ids?
29
+ results = by_name(results) if name?
30
+ results = by_route_type(results) if route_type?
31
+ results = by_origin_place(results) if origin_place_id.present?
32
+ results = by_destination_place(results) if destination_place_id.present?
33
+ results = by_query(results) if query?
34
+
35
+ results.order(order_clause).limit(limit)
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :vendor, :tenant, :params, :ids, :name, :route_type, :origin_place_id, :destination_place_id, :query, :limit, :order
41
+
42
+ def base_scope
43
+ scope = SpreeCmCommissioner::Route.includes(:origin_place, :destination_place, :route_photos)
44
+ scope = scope.where(vendor_id: vendor.id) if vendor.present?
45
+ scope = scope.where(tenant_id: tenant.id) if tenant.present?
46
+ scope
47
+ end
48
+
49
+ def ids?
50
+ ids.present?
51
+ end
52
+
53
+ def name?
54
+ name.present?
55
+ end
56
+
57
+ def route_type?
58
+ route_type.present?
59
+ end
60
+
61
+ def query?
62
+ query.present?
63
+ end
64
+
65
+ def by_ids(scope)
66
+ scope.where(id: ids)
67
+ end
68
+
69
+ def by_name(scope)
70
+ scope.where('cm_routes.route_name ILIKE ?', "%#{name}%")
71
+ end
72
+
73
+ def by_route_type(scope)
74
+ scope.where(route_type: route_type.to_s)
75
+ end
76
+
77
+ def by_origin_place(scope)
78
+ scope.where(origin_place_id: origin_place_id)
79
+ end
80
+
81
+ def by_destination_place(scope)
82
+ scope.where(destination_place_id: destination_place_id)
83
+ end
84
+
85
+ def by_query(scope)
86
+ scope.where('cm_routes.route_name ILIKE :q OR cm_routes.short_name ILIKE :q', q: "%#{query}%")
87
+ end
88
+
89
+ def order_clause
90
+ order
91
+ end
92
+ end
93
+ end
94
+ end
@@ -1,46 +1,31 @@
1
- # Finds routes ordered by popularity based on order metrics.
2
- #
3
- # Routes are sorted by:
4
- # 1. fulfilled_order_count (completed orders) - DESC
5
- # 2. order_count (total orders including incomplete) - DESC
6
- #
7
- # @param route_type [Symbol, String] Optional. Filter by route type from associated trips (e.g., :ferry, :bus)
8
- #
9
- # @return [ActiveRecord::Relation<SpreeCmCommissioner::Route>] Routes with origin and destination places loaded,
10
- # ordered from most to least popular
11
- #
12
- # @example Get all popular routes
13
- # finder = SpreeCmCommissioner::Routes::FindPopular.new
14
- # finder.execute # => Returns all routes sorted by fulfillment and order counts
15
- #
16
- # @example Get popular ferry routes
17
- # finder = SpreeCmCommissioner::Routes::FindPopular.new
18
- # finder.execute(route_type: :ferry) # => Returns ferry routes sorted by popularity
19
- #
20
- # @example Get popular bus routes
21
- # finder = SpreeCmCommissioner::Routes::FindPopular.new
22
- # finder.execute(route_type: :bus) # => Returns bus routes sorted by popularity
1
+ # Finder to get popular route metrics scoped to a vendor or tenant.
2
+ # Orders by fulfilled_order_count DESC, then order_count DESC.
3
+ # Usage:
4
+ # finder = SpreeCmCommissioner::Routes::FindPopular.new(vendor: vendor, route_type: :bus, limit: 10)
5
+ # finder.execute
23
6
 
24
7
  module SpreeCmCommissioner
25
8
  module Routes
26
9
  class FindPopular
27
- def execute(route_type: nil, query: nil)
28
- scope(route_type: route_type, query: query)
10
+ def initialize(vendor: nil, tenant: nil, route_type: nil, limit: 20)
11
+ @vendor = vendor
12
+ @tenant = tenant
13
+ @route_type = route_type
14
+ @limit = limit.to_i
29
15
  end
30
16
 
31
- private
32
-
33
- def scope(route_type:, query:)
34
- includes_associations = %i[origin_place destination_place]
35
- includes_associations << :trips if route_type.present?
36
- result = SpreeCmCommissioner::Route.includes(*includes_associations)
37
-
38
- result = result.joins(:trips).where(cm_trips: { route_type: route_type.to_sym }) if route_type.present?
17
+ def execute
18
+ scope = SpreeCmCommissioner::Route.includes(:origin_place, :destination_place, :route_photos)
19
+ scope = scope.where(vendor_id: vendor.id) if vendor.present?
20
+ scope = scope.where(tenant_id: tenant.id) if tenant.present?
21
+ scope = scope.where(route_type: route_type.to_sym) if route_type.present?
22
+ scope.order(fulfilled_order_count: :desc, order_count: :desc)
23
+ .limit(limit)
24
+ end
39
25
 
40
- result = result.where('cm_routes.route_name ILIKE ?', "%#{query}%") if query.present?
26
+ private
41
27
 
42
- result.distinct.order(fulfilled_order_count: :desc, order_count: :desc)
43
- end
28
+ attr_reader :vendor, :tenant, :route_type, :limit
44
29
  end
45
30
  end
46
31
  end
@@ -1,6 +1,6 @@
1
1
  module SpreeCmCommissioner
2
2
  class EventQrGenerator < BaseInteractor
3
- delegate :operator, :event, :expired_in_mn, to: :context
3
+ delegate :operator, :event, :check_in_session, :expired_in_mn, to: :context
4
4
 
5
5
  EventQrStruct = Struct.new(
6
6
  :id,
@@ -18,6 +18,7 @@ module SpreeCmCommissioner
18
18
 
19
19
  def jwt_token
20
20
  payload = { event_id: event.id, operator_id: operator.id, exp: exp }
21
+ payload[:check_in_session_id] = check_in_session.id if check_in_session.present?
21
22
 
22
23
  context.jwt_token ||= JWT.encode(
23
24
  payload,
@@ -33,11 +33,18 @@ module SpreeCmCommissioner
33
33
  end
34
34
  end
35
35
 
36
+ # Returns dates for which inventory should be generated, based on calendar or default period.
36
37
  def inventory_dates_for(variant)
37
- start_date = Time.zone.tomorrow
38
- end_date = Time.zone.today + pre_inventory_days_for(variant)
38
+ calendar = variant.product&.service_calendar
39
39
 
40
- (start_date..end_date)
40
+ if calendar.present?
41
+ calendar.start_date.upto(calendar.end_date).select { |date| calendar.service_available?(date) }
42
+ else
43
+ start_date = Time.zone.tomorrow
44
+ end_date = Time.zone.today + pre_inventory_days_for(variant)
45
+
46
+ start_date.upto(end_date).to_a
47
+ end
41
48
  end
42
49
 
43
50
  def inventory_exist?(variant, inventory_date)
@@ -68,7 +75,7 @@ module SpreeCmCommissioner
68
75
  def variants
69
76
  scope = Spree::Variant.active.with_permanent_stock.where(is_master: false)
70
77
  scope = scope.where(id: variant_ids) if variant_ids.present?
71
- scope
78
+ scope.includes(product: :service_calendar)
72
79
  end
73
80
  end
74
81
  end
@@ -8,7 +8,8 @@ module SpreeCmCommissioner
8
8
 
9
9
  return context.fail!(message: Spree.t(:doesnt_track_inventory)) unless variant.track_inventory?
10
10
 
11
- stock_location = Spree::StockLocation.find(stock_location_id)
11
+ stock_location = resolve_stock_location(variant, stock_location_id)
12
+
12
13
  stock_movement = stock_location.stock_movements.build(stock_movement_params)
13
14
  stock_movement.stock_item = stock_location.set_up_stock_item(variant)
14
15
 
@@ -22,6 +23,14 @@ module SpreeCmCommissioner
22
23
 
23
24
  private
24
25
 
26
+ def resolve_stock_location(variant, stock_location_id = nil)
27
+ if stock_location_id.present?
28
+ Spree::StockLocation.find(stock_location_id)
29
+ else
30
+ variant.vendor.stock_locations.first || variant.vendor.send(:create_stock_location)
31
+ end
32
+ end
33
+
25
34
  def adjust_inventory_items_async(variant_id, quantity)
26
35
  args = { variant_id: variant_id, quantity: quantity }
27
36
  CmAppLogger.log(label: "#{self.class.name}#adjust_inventory_items_async", data: args) do
@@ -91,9 +91,10 @@ module SpreeCmCommissioner
91
91
  stop_params = params.dig(:trip_stops_attributes, index.to_s)
92
92
  return unless stop_params
93
93
 
94
- cloned_stop.departure_time = parse_time(stop_params[:departure_time])
95
- cloned_stop.arrival_time = parse_time(stop_params[:arrival_time])
96
- cloned_stop.stop_type = stop_params[:stop_type].presence
94
+ cloned_stop.departure_time = parse_time(stop_params[:departure_time])
95
+ cloned_stop.arrival_time = parse_time(stop_params[:arrival_time])
96
+ cloned_stop.allow_boarding = stop_params[:allow_boarding] if stop_params.key?(:allow_boarding)
97
+ cloned_stop.allow_drop_off = stop_params[:allow_drop_off] if stop_params.key?(:allow_drop_off)
97
98
  cloned_stop.location_place_id = stop_params[:location_place_id].presence
98
99
  end
99
100
 
@@ -26,7 +26,7 @@ module SpreeCmCommissioner
26
26
  trip: trip,
27
27
  stop_place: origin_place,
28
28
  location_place: origin_place,
29
- stop_type: :boarding
29
+ allow_boarding: true
30
30
  }
31
31
 
32
32
  attributes[:departure_time] = departure_time
@@ -42,7 +42,7 @@ module SpreeCmCommissioner
42
42
  trip: trip,
43
43
  stop_place: destination_place,
44
44
  location_place: destination_place,
45
- stop_type: :drop_off
45
+ allow_drop_off: true
46
46
  }
47
47
 
48
48
  attributes[:arrival_time] = departure_time + duration_seconds.seconds
@@ -0,0 +1,10 @@
1
+ module SpreeCmCommissioner
2
+ module Guests
3
+ class PreloadCheckInSessionIdsJob < ApplicationUniqueJob
4
+ def perform(options)
5
+ event = Spree::Taxon.find(options[:event_id])
6
+ SpreeCmCommissioner::Guests::PreloadCheckInSessionIds.call(event: event)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module SpreeCmCommissioner
2
+ module RouteMetrics
3
+ class DecreaseTripCountJob < ApplicationUniqueJob
4
+ def perform(options = {})
5
+ trip = SpreeCmCommissioner::Trip.find(options[:trip_id])
6
+ SpreeCmCommissioner::RouteMetrics::DecreaseTripCount.call(trip: trip)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module SpreeCmCommissioner
2
+ module RouteMetrics
3
+ class IncreaseFulfilledOrderCountJob < ApplicationUniqueJob
4
+ def perform(options = {})
5
+ order = Spree::Order.find(options[:order_id])
6
+ SpreeCmCommissioner::RouteMetrics::IncreaseFulfilledOrderCount.call(order: order)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module SpreeCmCommissioner
2
+ module RouteMetrics
3
+ class IncreaseOrderCountJob < ApplicationUniqueJob
4
+ def perform(options = {})
5
+ order = Spree::Order.find(options[:order_id])
6
+ SpreeCmCommissioner::RouteMetrics::IncreaseOrderCount.call(order: order)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module SpreeCmCommissioner
2
+ module RouteMetrics
3
+ class IncreaseTripCountJob < ApplicationUniqueJob
4
+ def perform(options = {})
5
+ trip = SpreeCmCommissioner::Trip.find(options[:trip_id])
6
+ SpreeCmCommissioner::RouteMetrics::IncreaseTripCount.call(trip: trip)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -1,8 +1,8 @@
1
1
  module SpreeCmCommissioner
2
2
  module Stock
3
3
  class PermanentInventoryItemsGeneratorJob < ApplicationUniqueJob
4
- def perform
5
- SpreeCmCommissioner::Stock::PermanentInventoryItemsGenerator.call
4
+ def perform(options = {})
5
+ SpreeCmCommissioner::Stock::PermanentInventoryItemsGenerator.call(options)
6
6
  end
7
7
  end
8
8
  end
@@ -7,13 +7,13 @@ module SpreeCmCommissioner
7
7
  def increment_route_fulfilled_order_count
8
8
  return unless has_trip_ids?
9
9
 
10
- SpreeCmCommissioner::Transit::RouteFulfilledOrderCountIncrementerJob.perform_later(order_id: id)
10
+ SpreeCmCommissioner::RouteMetrics::IncreaseFulfilledOrderCountJob.perform_later(order_id: id)
11
11
  end
12
12
 
13
13
  def increment_route_order_count
14
14
  return unless has_trip_ids?
15
15
 
16
- SpreeCmCommissioner::Transit::RouteOrderCountIncrementerJob.perform_later(order_id: id)
16
+ SpreeCmCommissioner::RouteMetrics::IncreaseOrderCountJob.perform_later(order_id: id)
17
17
  end
18
18
  end
19
19
  end
@@ -7,13 +7,15 @@ module SpreeCmCommissioner
7
7
  enum entry_type: { normal: 0, VIP: 1 }
8
8
  enum check_in_method: { manual: 0, scan: 1, sensor: 2, nfc: 3 }
9
9
 
10
- belongs_to :guest, class_name: 'SpreeCmCommissioner::Guest'
11
- belongs_to :check_in_by, class_name: 'Spree::User'
10
+ belongs_to :guest, class_name: 'SpreeCmCommissioner::Guest', optional: false
11
+ belongs_to :check_in_by, class_name: 'Spree::User', optional: true
12
+
12
13
  has_one :line_item, class_name: 'Spree::LineItem', through: :guest
13
14
 
14
15
  belongs_to :checkinable, polymorphic: true
15
16
  belongs_to :check_in_session, class_name: 'SpreeCmCommissioner::CheckInSession', optional: true
16
- before_validation :set_entry_type, on: :create
17
+
18
+ before_validation :set_entry_type, on: :create, if: -> { guest.present? }
17
19
 
18
20
  accepts_nested_attributes_for :guest, allow_destroy: true
19
21
 
@@ -21,11 +23,36 @@ module SpreeCmCommissioner
21
23
  self.entry_type = guest.entry_type
22
24
  end
23
25
 
26
+ # Ensure check_in_session is present when required and is eligible for the guest/ticket
27
+ validate :validate_check_in_session, if: -> { guest.present? }
28
+
24
29
  validates :guest_id,
25
- uniqueness: {
26
- scope: :check_in_session_id,
27
- message: I18n.t('spree.check_in.already_checked_in')
28
- },
30
+ uniqueness: { scope: :check_in_session_id, message: I18n.t('spree.check_in.already_checked_in') },
29
31
  if: -> { check_in_session_id.present? }
32
+
33
+ validates :guest_id,
34
+ uniqueness: { message: I18n.t('spree.check_in.already_checked_in') },
35
+ if: -> { check_in_session_id.blank? }
36
+
37
+ private
38
+
39
+ def validate_check_in_session
40
+ # Case 1: Most common scenario for events without sessions We allow this case to check-in by default.
41
+ # - No check-in session provided from operator app (`check_in_session_id.blank?`)
42
+ # - Guest has no eligible sessions, most likely because the event has no sessions
43
+ return true if check_in_session_id.blank? && guest.eligible_check_in_session_ids.empty?
44
+
45
+ # Case 2: Events with sessions, but operator did not provide session id
46
+ if check_in_session_id.blank? && guest.eligible_check_in_session_ids.any?
47
+ return errors.add(:check_in_session_id, I18n.t('spree.check_in.check_in_session_required'))
48
+ end
49
+
50
+ # Case 3: Provided session is not eligible for the guest (eg. special session for VIP tickets/guest only)
51
+ if check_in_session_id.present? && guest.eligible_check_in_session_ids.exclude?(check_in_session_id)
52
+ return errors.add(:check_in_session_id, I18n.t('spree.check_in.check_in_session_is_not_allowed_or_invalid'))
53
+ end
54
+
55
+ true
56
+ end
30
57
  end
31
58
  end
@@ -1,16 +1,21 @@
1
1
  module SpreeCmCommissioner
2
2
  class CheckInSession < SpreeCmCommissioner::Base
3
+ acts_as_list scope: :event_id
4
+
3
5
  belongs_to :event, class_name: 'Spree::Taxon', optional: false
6
+
4
7
  has_many :check_in_rules, class_name: 'SpreeCmCommissioner::CheckInRule', dependent: :destroy
5
8
 
9
+ scope :active, -> { where(is_archived: false) }
10
+
6
11
  accepts_nested_attributes_for :check_in_rules, allow_destroy: true
7
12
 
8
- before_create :set_position
13
+ after_commit :preload_check_in_session_ids_to_event_guests
9
14
 
10
15
  private
11
16
 
12
- def set_position
13
- self.position ||= event.check_in_sessions.count + 1
17
+ def preload_check_in_session_ids_to_event_guests
18
+ SpreeCmCommissioner::Guests::PreloadCheckInSessionIdsJob.perform_later(event_id: event.id)
14
19
  end
15
20
  end
16
21
  end
@@ -3,13 +3,15 @@ module SpreeCmCommissioner
3
3
  class Guest < SpreeCmCommissioner::Base # rubocop:disable Metrics/ClassLength
4
4
  include SpreeCmCommissioner::KycBitwise
5
5
  include SpreeCmCommissioner::PhoneNumberSanitizer
6
- include SpreeCmCommissioner::StoreMetadata
7
6
  include SpreeCmCommissioner::Integrations::IntegrationMappable
8
7
 
9
8
  delegate :kyc, to: :line_item, allow_nil: true
10
9
  delegate :allowed_upload_later?, to: :line_item, allow_nil: true
11
10
 
11
+ enum entry_type: { normal: 0, VIP: 1 }
12
12
  enum gender: { :other => 0, :male => 1, :female => 2 }
13
+ enum age_group: { :adult => 0, :child => 1 }
14
+ enum nationality_group: { :local => 0, :foreigner => 1 }
13
15
  enum social_contact_platform: {
14
16
  :other => 0,
15
17
  :telegram => 1,
@@ -36,12 +38,15 @@ module SpreeCmCommissioner
36
38
  belongs_to :saved_guest, class_name: 'SpreeCmCommissioner::SavedGuest', optional: true
37
39
 
38
40
  has_one :reserved_block, class_name: 'SpreeCmCommissioner::ReservedBlock', dependent: :nullify
41
+ has_one :earliest_check_in, -> { order(:confirmed_at) }, class_name: 'SpreeCmCommissioner::CheckIn'
39
42
 
40
- scope :checked_ins, -> { joins(:check_in) }
41
- scope :no_show, -> { left_outer_joins(:check_in).where(cm_check_ins: { id: nil }) }
43
+ scope :checked_ins, -> { joins(:earliest_check_in) }
44
+ scope :no_show, -> { left_outer_joins(:earliest_check_in).where(cm_check_ins: { id: nil }) }
42
45
 
43
46
  has_many :state_changes, as: :stateful, class_name: 'Spree::StateChange'
44
47
  has_many :guest_dynamic_fields, dependent: :destroy, class_name: 'SpreeCmCommissioner::GuestDynamicField'
48
+ has_many :check_ins, class_name: 'SpreeCmCommissioner::CheckIn'
49
+
45
50
  accepts_nested_attributes_for :guest_dynamic_fields, allow_destroy: true, reject_if: :reject_guest_dynamic_field?
46
51
 
47
52
  belongs_to :event, class_name: 'Spree::Taxon'
@@ -56,6 +61,7 @@ module SpreeCmCommissioner
56
61
  before_validation :assign_seat_number_with_bib, if: -> { bib_number.present? }
57
62
  before_validation :assign_seat_number_with_block, if: -> { will_save_change_to_block_id? }
58
63
 
64
+ before_save :preload_eligible_check_in_session_ids, if: -> { new_record? }
59
65
  before_create :generate_bib, if: -> { line_item.reload && variant.bib_pre_generation_on_create? }
60
66
  after_create :preload_order_block_ids, if: -> { block_id.present? }
61
67
  after_update :preload_order_block_ids, if: :saved_change_to_block_id?
@@ -300,6 +306,57 @@ module SpreeCmCommissioner
300
306
  line_item.order.update(preload_block_ids: block_ids)
301
307
  end
302
308
 
309
+ def eligible_check_in_session_ids
310
+ # Ensure public_metadata is a hash
311
+ self.public_metadata = JSON.parse(public_metadata) if public_metadata.is_a?(String)
312
+
313
+ # Return as array, even if key missing
314
+ Array(public_metadata['eligible_check_in_session_ids'])
315
+ end
316
+
317
+ def eligible_check_in_session_ids=(ids)
318
+ # Ensure public_metadata is a hash
319
+ self.public_metadata = {} if public_metadata.nil?
320
+ self.public_metadata = JSON.parse(public_metadata) if public_metadata.is_a?(String)
321
+
322
+ public_metadata['eligible_check_in_session_ids'] = ids
323
+ end
324
+
325
+ def preload_eligible_check_in_session_ids
326
+ event = self.event || line_item&.event
327
+ return if event.blank?
328
+
329
+ # only preload eligible and active check-in sessions, otherwise should not checkinable.
330
+ self.eligible_check_in_session_ids = event.active_check_in_sessions
331
+ .select { |session| eligible_for_session?(session) }
332
+ .map(&:id)
333
+ .uniq
334
+ end
335
+
336
+ def eligible_for_session?(check_in_session)
337
+ rules = check_in_session.check_in_rules
338
+ return true if rules.empty?
339
+
340
+ rules.any? do |rule|
341
+ case rule.ruleable_type
342
+ when 'Spree::Product'
343
+ line_item.product_id == rule.ruleable_id
344
+ when 'Spree::Variant'
345
+ line_item.variant_id == rule.ruleable_id
346
+ when 'Spree::OptionValue'
347
+ line_item.variant.option_value_ids.include?(rule.ruleable_id)
348
+ else
349
+ false
350
+ end
351
+ end
352
+ end
353
+
354
+ def eligible_check_in_sessions
355
+ return SpreeCmCommissioner::CheckInSession.none if eligible_check_in_session_ids.blank?
356
+
357
+ SpreeCmCommissioner::CheckInSession.where(id: eligible_check_in_session_ids)
358
+ end
359
+
303
360
  def cancel_reserved_block!
304
361
  reserved_block.update!(
305
362
  status: :canceled,
@@ -400,31 +457,6 @@ module SpreeCmCommissioner
400
457
  line_item&.product&.dynamic_fields&.during_check_in&.any?
401
458
  end
402
459
 
403
- def cm_check_in_rules
404
- SpreeCmCommissioner::CheckInRule.where(ruleable_id: line_item.product_id)
405
- end
406
-
407
- def eligible_check_in_sessions
408
- check_in_session_ids = []
409
-
410
- cm_check_in_rules.includes(:check_in_session).find_each do |rule|
411
- case rule.ruleable_type
412
- when 'Spree::Product'
413
- check_in_session_ids << rule.check_in_session_id if line_item.product_id == rule.ruleable_id
414
- when 'Spree::Variant'
415
- check_in_session_ids << rule.check_in_session_id if line_item.variant_id == rule.ruleable_id
416
- when 'Spree::OptionValue'
417
- check_in_session_ids << rule.check_in_session_id if line_item.variant.option_value_ids.include?(rule.ruleable_id)
418
- end
419
- end
420
-
421
- SpreeCmCommissioner::CheckInSession.where(id: check_in_session_ids.uniq)
422
- end
423
-
424
- def can_check_in?(session)
425
- eligible_check_in_sessions.include?(session)
426
- end
427
-
428
460
  def product_dynamic_fields
429
461
  @product_dynamic_fields ||= line_item.product.dynamic_fields
430
462
  end
@@ -74,9 +74,9 @@ module SpreeCmCommissioner
74
74
 
75
75
  case dynamic_field.data_fill_stage.to_sym
76
76
  when :pre_registration, :post_registration
77
- guest.line_item.order.completed? && guest.check_in.blank?
77
+ guest.line_item.order.completed? && guest.check_ins.empty?
78
78
  when :during_check_in
79
- guest.check_in.present?
79
+ guest.check_ins.exists?
80
80
  else
81
81
  false
82
82
  end
@@ -21,15 +21,12 @@ module SpreeCmCommissioner
21
21
  has_many :taxon_places, class_name: 'SpreeCmCommissioner::TaxonPlace', dependent: :destroy
22
22
  has_many :taxons, through: :taxon_places
23
23
 
24
- has_many :routes_as_origin, class_name: 'SpreeCmCommissioner::Route', foreign_key: :origin_place_id
25
- has_many :routes_as_destination, class_name: 'SpreeCmCommissioner::Route', foreign_key: :destination_place_id
24
+ has_many :route_metrics_as_origin, class_name: 'SpreeCmCommissioner::RouteMetric', foreign_key: :origin_place_id
25
+ has_many :route_metrics_as_destination, class_name: 'SpreeCmCommissioner::RouteMetric', foreign_key: :destination_place_id
26
26
 
27
- has_many :trips_as_origin, through: :routes_as_origin, source: :trips
28
- has_many :trips_as_destination, through: :routes_as_destination, source: :trips
29
-
30
- # Scopes for route-based filtering
31
- scope :with_routes_as_origin, -> { joins(:routes_as_origin).distinct }
32
- scope :with_routes_as_destination, -> { joins(:routes_as_destination).distinct }
27
+ # Scopes for route metric filtering
28
+ scope :with_route_metrics_as_origin, -> { joins(:route_metrics_as_origin).distinct }
29
+ scope :with_route_metrics_as_destination, -> { joins(:route_metrics_as_destination).distinct }
33
30
 
34
31
  def self.ransackable_attributes(auth_object = nil)
35
32
  super & %w[name code]
@@ -0,0 +1,52 @@
1
+ module SpreeCmCommissioner
2
+ module PricingActions
3
+ class CreateGuestAdjustments < SpreeCmCommissioner::PricingAction
4
+ def perform(line_item)
5
+ return if pricing_rule_group.pricing_rules.blank?
6
+ return if line_item.guests.blank?
7
+
8
+ eligible_guests = find_eligible_guests(line_item)
9
+ return if eligible_guests.empty?
10
+
11
+ total_amount = eligible_guests.sum do |guest|
12
+ compute_guest_amount(line_item, guest)
13
+ end
14
+
15
+ return if total_amount.zero?
16
+
17
+ create_adjustment(line_item, total_amount)
18
+ end
19
+
20
+ private
21
+
22
+ def find_eligible_guests(line_item)
23
+ line_item.guests.select do |guest|
24
+ guest_eligible_for_all_rules?(guest)
25
+ end
26
+ end
27
+
28
+ def guest_eligible_for_all_rules?(guest)
29
+ pricing_rule_group.pricing_rules.all? do |rule|
30
+ rule.respond_to?(:guest_eligible?) && rule.guest_eligible?(guest)
31
+ end
32
+ end
33
+
34
+ def compute_guest_amount(line_item, _guest)
35
+ per_guest_item = Struct.new(:amount, :currency, :quantity)
36
+ .new(line_item.amount_per_guest, line_item.currency, 1)
37
+ calculator&.compute(per_guest_item) || 0
38
+ end
39
+
40
+ def create_adjustment(line_item, amount)
41
+ line_item.adjustments.create!(
42
+ amount: amount,
43
+ source: self,
44
+ adjustable: line_item,
45
+ order: line_item.order,
46
+ label: pricing_rule_group.pricing_model.name,
47
+ mandatory: true
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end