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,6 @@
1
+ module SpreeCmCommissioner
2
+ module PricingActions
3
+ class CreateLineItemAdjustments < SpreeCmCommissioner::PricingAction
4
+ end
5
+ end
6
+ end
@@ -8,6 +8,10 @@ module SpreeCmCommissioner
8
8
  raise 'eligible? should be implemented in a sub-class of SpreeCmCommissioner::PricingRule'
9
9
  end
10
10
 
11
+ def guest_eligible?(_guest)
12
+ raise 'guest_eligible? should be implemented in a sub-class of SpreeCmCommissioner::PricingRule'
13
+ end
14
+
11
15
  def self.available_rule_types
12
16
  SpreeCmCommissioner::PricingRules.constants.map(&:to_s)
13
17
  end
@@ -0,0 +1,45 @@
1
+ module SpreeCmCommissioner
2
+ module PricingRules
3
+ class AgeGroup < SpreeCmCommissioner::PricingRule
4
+ AGE_GROUPS = {
5
+ child: [0, 12],
6
+ adult: [13, 99]
7
+ }.freeze
8
+
9
+ RULE_TYPES = %w[all any none].freeze
10
+
11
+ store_accessor :private_metadata, :rule_type, :age_groups
12
+
13
+ def eligible?(line_item)
14
+ return false if line_item.guests.blank?
15
+
16
+ case rule_type
17
+ when 'all'
18
+ line_item.guests.all? { |guest| guest_eligible?(guest) }
19
+ when 'any'
20
+ line_item.guests.any? { |guest| guest_eligible?(guest) }
21
+ when 'none'
22
+ line_item.guests.none? { |guest| guest_eligible?(guest) }
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def guest_eligible?(guest)
29
+ guest_age_group = resolve_guest_age_group(guest)
30
+ return false if guest_age_group.nil?
31
+
32
+ Array.wrap(age_groups).map(&:to_sym).include?(guest_age_group.to_sym)
33
+ end
34
+
35
+ private
36
+
37
+ def resolve_guest_age_group(guest)
38
+ return guest.age_group if guest.age_group.present?
39
+ return guest.saved_guest.age_group if guest.saved_guest&.age_group.present?
40
+
41
+ nil
42
+ end
43
+ end
44
+ end
45
+ end
@@ -14,12 +14,31 @@ module SpreeCmCommissioner
14
14
  return false if guest_nationalities.empty?
15
15
 
16
16
  case rule_type
17
+
17
18
  when 'all'
18
19
  guest_nationalities.all? { |n| targets.include?(n) }
19
20
  when 'any'
20
21
  guest_nationalities.any? { |n| targets.include?(n) }
21
22
  when 'none'
22
- guest_nationalities.none? { |n| targets.include?(n) }
23
+ guest_nationalities.any? { |n| targets.exclude?(n) }
24
+ else
25
+ false
26
+ end
27
+ end
28
+
29
+ def guest_eligible?(guest)
30
+ return false if nationalities.blank?
31
+
32
+ targets = Array.wrap(nationalities).map(&:downcase)
33
+ guest_nat = guest_nationality(guest)
34
+
35
+ return false if guest_nat.blank?
36
+
37
+ case rule_type
38
+ when 'all', 'any'
39
+ targets.include?(guest_nat)
40
+ when 'none'
41
+ targets.exclude?(guest_nat)
23
42
  else
24
43
  false
25
44
  end
@@ -28,7 +47,7 @@ module SpreeCmCommissioner
28
47
  private
29
48
 
30
49
  def guest_nationality(guest)
31
- guest.nationality&.name.to_s.downcase
50
+ (guest.nationality&.name || guest.saved_guest&.nationality&.name).to_s.downcase
32
51
  end
33
52
  end
34
53
  end
@@ -0,0 +1,41 @@
1
+ module SpreeCmCommissioner
2
+ module PricingRules
3
+ class NationalityGroup < SpreeCmCommissioner::PricingRule
4
+ RULE_TYPES = %w[all any none].freeze
5
+
6
+ store_accessor :private_metadata, :rule_type, :nationality_groups
7
+
8
+ def eligible?(line_item)
9
+ return false if nationality_groups.blank? || line_item.guests.blank?
10
+
11
+ case rule_type
12
+ when 'all'
13
+ line_item.guests.all? { |guest| guest_eligible?(guest) }
14
+ when 'any'
15
+ line_item.guests.any? { |guest| guest_eligible?(guest) }
16
+ when 'none'
17
+ line_item.guests.none? { |guest| guest_eligible?(guest) }
18
+ else
19
+ false
20
+ end
21
+ end
22
+
23
+ def guest_eligible?(guest)
24
+ guest_nationality_group = resolve_guest_nationality_group(guest)
25
+ return false if guest_nationality_group.nil?
26
+ return false if nationality_groups.blank?
27
+
28
+ Array.wrap(nationality_groups).map(&:to_sym).include?(guest_nationality_group.to_sym)
29
+ end
30
+
31
+ private
32
+
33
+ def resolve_guest_nationality_group(guest)
34
+ return guest.nationality_group if guest.nationality_group.present?
35
+ return guest.saved_guest.nationality_group if guest.saved_guest&.nationality_group.present?
36
+
37
+ nil
38
+ end
39
+ end
40
+ end
41
+ end
@@ -48,6 +48,7 @@ module SpreeCmCommissioner
48
48
  base.accepts_nested_attributes_for :product_places, allow_destroy: true
49
49
 
50
50
  base.has_one :trip, class_name: 'SpreeCmCommissioner::Trip', dependent: :destroy
51
+ base.has_one :service_calendar, as: :calendarable, class_name: 'SpreeCmCommissioner::ServiceCalendar', dependent: :destroy
51
52
 
52
53
  base.belongs_to :event, class_name: 'Spree::Taxon', optional: true
53
54
 
@@ -1,12 +1,53 @@
1
1
  module SpreeCmCommissioner
2
2
  class Route < Base
3
- has_many :vendor_routes, class_name: 'SpreeCmCommissioner::VendorRoute', dependent: :destroy
4
- has_many :vendors, through: :vendor_routes
3
+ include SpreeCmCommissioner::RouteType
4
+
5
+ belongs_to :vendor, class_name: 'Spree::Vendor', optional: false
6
+ belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant', optional: true
7
+
8
+ belongs_to :origin_place, class_name: 'SpreeCmCommissioner::Place', optional: false
9
+ belongs_to :destination_place, class_name: 'SpreeCmCommissioner::Place', optional: false
10
+
11
+ has_one :primary_photo, -> { order(position: :asc) },
12
+ class_name: 'SpreeCmCommissioner::RoutePhoto',
13
+ as: :viewable,
14
+ dependent: :destroy
15
+ has_many :route_photos, class_name: 'SpreeCmCommissioner::RoutePhoto', as: :viewable, dependent: :destroy
16
+
5
17
  has_many :trips, inverse_of: :route
6
18
 
7
- belongs_to :origin_place, class_name: 'SpreeCmCommissioner::Place'
8
- belongs_to :destination_place, class_name: 'SpreeCmCommissioner::Place'
19
+ validates :route_name, presence: true
20
+
21
+ validate :validate_route_stops
22
+
23
+ delegate :multi_leg?, to: :route_stops
24
+
25
+ def self.ransackable_attributes(_auth_object = nil)
26
+ %w[route_type short_name route_name]
27
+ end
28
+
29
+ def route_stops
30
+ Transit::RouteStopCollection.from_hash(read_attribute(:route_stops))
31
+ end
32
+
33
+ def route_stops=(value)
34
+ if value.is_a?(Transit::RouteStopCollection)
35
+ @route_stops = value
36
+ self[:route_stops] = value.to_h
37
+ elsif value.is_a?(Array)
38
+ @route_stops = Transit::RouteStopCollection.from_hash(value)
39
+ self[:route_stops] = @route_stops.to_h
40
+ else
41
+ raise ArgumentError, 'route_stops must be an Array or RouteStopCollection'
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def validate_route_stops
48
+ return if route_stops.valid?
9
49
 
10
- validates :origin_place_id, uniqueness: { scope: :destination_place_id }
50
+ route_stops.errors.each { |error| errors.add(:route_stops, error) }
51
+ end
11
52
  end
12
53
  end
@@ -0,0 +1,21 @@
1
+ module SpreeCmCommissioner
2
+ class RouteMetric < Base
3
+ include SpreeCmCommissioner::RouteType
4
+
5
+ belongs_to :origin_place, class_name: 'SpreeCmCommissioner::Place', optional: false
6
+ belongs_to :destination_place, class_name: 'SpreeCmCommissioner::Place', optional: false
7
+
8
+ validates :route_type, presence: true
9
+ validates :route_type, uniqueness: { scope: %i[origin_place_id destination_place_id] }
10
+
11
+ validate :origin_and_destination_cannot_be_the_same
12
+
13
+ private
14
+
15
+ def origin_and_destination_cannot_be_the_same
16
+ return unless origin_place_id == destination_place_id
17
+
18
+ errors.add(:base, 'Origin and destination cannot be the same')
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ module SpreeCmCommissioner
2
+ class RoutePhoto < Asset
3
+ # 16x9
4
+ def asset_styles
5
+ {
6
+ mini: '160x90>',
7
+ small: '480x270>',
8
+ medium: '960x540>'
9
+ }
10
+ end
11
+ end
12
+ end
@@ -77,6 +77,7 @@ module SpreeCmCommissioner
77
77
  base.has_many :taxon_places, class_name: 'SpreeCmCommissioner::TaxonPlace', dependent: :destroy
78
78
  base.has_many :places, through: :taxon_places
79
79
  base.has_many :check_in_sessions, class_name: 'SpreeCmCommissioner::CheckInSession', foreign_key: :event_id, dependent: :destroy
80
+ base.has_many :active_check_in_sessions, -> { active }, class_name: 'SpreeCmCommissioner::CheckInSession', foreign_key: :event_id
80
81
 
81
82
  base.has_many :import_orders, as: :importable, class_name: 'SpreeCmCommissioner::Imports::ImportOrder', dependent: :destroy
82
83
 
@@ -2,7 +2,6 @@ module SpreeCmCommissioner
2
2
  class Trip < Base
3
3
  include SpreeCmCommissioner::StoreMetadata
4
4
  include SpreeCmCommissioner::RouteType
5
- include SpreeCmCommissioner::RouteTripCountCallbacks
6
5
 
7
6
  attr_accessor :hours, :minutes, :seconds
8
7
 
@@ -15,7 +14,9 @@ module SpreeCmCommissioner
15
14
 
16
15
  # before create, duplicate seat layout from vehicle_type if present
17
16
  before_validation :duplicate_seat_layout_from_vehicle, if: -> { seat_layout.nil? && vehicle_type&.seat_layout.present? }
18
- before_validation :assign_route_and_vendor_route, if: :changed_route_attributes?
17
+
18
+ after_create :increment_route_metric_trip_count
19
+ before_destroy :decrement_route_metric_trip_count
19
20
 
20
21
  belongs_to :vendor, class_name: 'Spree::Vendor', inverse_of: :trips, optional: false
21
22
  belongs_to :product, class_name: 'Spree::Product', inverse_of: :trip, optional: false
@@ -26,7 +27,7 @@ module SpreeCmCommissioner
26
27
 
27
28
  belongs_to :origin_place, class_name: 'SpreeCmCommissioner::Place', optional: false
28
29
  belongs_to :destination_place, class_name: 'SpreeCmCommissioner::Place', optional: false
29
- belongs_to :route, inverse_of: :trips
30
+ belongs_to :route, class_name: 'SpreeCmCommissioner::Route', inverse_of: :trips, optional: true
30
31
 
31
32
  has_many :trip_blazer_queries, as: :queryable, class_name: 'SpreeCmCommissioner::BlazerQueryable'
32
33
  has_many :blazer_queries, through: :trip_blazer_queries, source: :blazer_query, class_name: 'Blazer::Query'
@@ -48,10 +49,6 @@ module SpreeCmCommissioner
48
49
  self.whitelisted_ransackable_associations = %w[product vehicle vehicle_type]
49
50
  self.whitelisted_ransackable_attributes = %w[origin_place_id destination_place_id]
50
51
 
51
- def changed_route_attributes?
52
- origin_place_id_changed? || destination_place_id_changed? || vendor_id_changed?
53
- end
54
-
55
52
  def convert_duration_to_seconds
56
53
  return if hours.blank? && minutes.blank? && seconds.blank?
57
54
 
@@ -104,34 +101,12 @@ module SpreeCmCommissioner
104
101
  )
105
102
  end
106
103
 
107
- # Check if any of the route context attributes are changing
108
- def will_route_context_change?
109
- # Returns true if any of the route context attributes will change on save.
110
- will_save_change_to_route_id? ||
111
- will_save_change_to_origin_place_id? ||
112
- will_save_change_to_destination_place_id? ||
113
- will_save_change_to_vendor_id?
114
- end
115
-
116
- def changed_route_context?
117
- saved_change_to_route_id? ||
118
- saved_change_to_origin_place_id? ||
119
- saved_change_to_destination_place_id? ||
120
- saved_change_to_vendor_id?
104
+ def increment_route_metric_trip_count
105
+ SpreeCmCommissioner::RouteMetrics::IncreaseTripCountJob.perform_later(trip_id: id)
121
106
  end
122
107
 
123
- # Find or create the Route for this trip (by origin/destination) and
124
- # assign it to the trip. Also populate a default route_name from the
125
- # product when the route is new/blank.
126
- def assign_route_and_vendor_route
127
- route = SpreeCmCommissioner::Route.find_or_create_by!(
128
- origin_place_id: origin_place_id,
129
- destination_place_id: destination_place_id
130
- )
131
- route.route_name = product.name if route.route_name.blank?
132
- route.save! if route.changed?
133
-
134
- self.route_id = route.id
108
+ def decrement_route_metric_trip_count
109
+ SpreeCmCommissioner::RouteMetrics::DecreaseTripCountJob.perform_later(trip_id: id)
135
110
  end
136
111
  end
137
112
  end
@@ -1,25 +1,39 @@
1
1
  module SpreeCmCommissioner
2
2
  class TripStop < Base
3
3
  acts_as_list column: :sequence, scope: :trip_id
4
- enum :stop_type, { boarding: 0, drop_off: 1 }
5
4
 
6
5
  belongs_to :trip, class_name: 'SpreeCmCommissioner::Trip', optional: false
7
6
  belongs_to :stop_place, class_name: 'SpreeCmCommissioner::Place', optional: false
8
7
  belongs_to :location_place, class_name: 'SpreeCmCommissioner::Place', optional: false
9
8
 
9
+ belongs_to :board_to_trip, class_name: 'SpreeCmCommissioner::Trip', optional: true
10
+
10
11
  before_validation :set_stop_name
12
+
11
13
  validates :stop_place_id, uniqueness: { scope: :trip_id }
14
+ validate :boarding_trip_requires_allow_boarding
15
+
16
+ scope :boarding, -> { where(allow_boarding: true) }
17
+ scope :drop_off, -> { where(allow_drop_off: true) }
12
18
 
13
19
  def set_stop_name
14
20
  self.stop_name = stop_place.name if stop_place.present?
15
21
  end
16
22
 
17
23
  def self.ransackable_attributes(_auth_object = nil)
18
- %w[stop_name stop_type]
24
+ %w[stop_name allow_boarding allow_drop_off]
19
25
  end
20
26
 
21
27
  def self.ransackable_associations(_auth_object = nil)
22
28
  ['stop_place']
23
29
  end
30
+
31
+ private
32
+
33
+ def boarding_trip_requires_allow_boarding
34
+ return unless board_to_trip_id.present? && !allow_boarding?
35
+
36
+ errors.add(:board_to_trip, 'requires allow_boarding to be true')
37
+ end
24
38
  end
25
39
  end
@@ -82,7 +82,9 @@ module SpreeCmCommissioner
82
82
  base.has_many :subscriptions, through: :customers, class_name: 'SpreeCmCommissioner::Subscription'
83
83
  base.has_many :subscription_orders, through: :subscriptions, class_name: 'Spree::Order', source: :orders
84
84
  base.has_many :promotion_categories, class_name: 'Spree::PromotionCategory', foreign_key: :vendor_id, dependent: :destroy
85
- base.has_many :vendor_routes, class_name: 'SpreeCmCommissioner::VendorRoute'
85
+ base.has_many :routes, class_name: 'SpreeCmCommissioner::Route', dependent: :destroy
86
+ base.has_many :popular_routes, -> { order(fulfilled_order_count: :desc, order_count: :desc) },
87
+ class_name: 'SpreeCmCommissioner::Route'
86
88
 
87
89
  base.has_many :homepage_section_relatables,
88
90
  as: :relatable,
@@ -77,8 +77,8 @@ module SpreeCmCommissioner
77
77
 
78
78
  scope
79
79
  .joins(<<~SQL.squish)
80
- INNER JOIN cm_trip_stops AS boarding ON boarding.trip_id = cm_trips.id AND boarding.stop_type = 0
81
- INNER JOIN cm_trip_stops AS drop_off ON drop_off.trip_id = cm_trips.id AND drop_off.stop_type = 1
80
+ INNER JOIN cm_trip_stops AS boarding ON boarding.trip_id = cm_trips.id AND boarding.allow_boarding = true
81
+ INNER JOIN cm_trip_stops AS drop_off ON drop_off.trip_id = cm_trips.id AND drop_off.allow_drop_off = true
82
82
  INNER JOIN cm_places AS origin_places ON origin_places.id = cm_trips.origin_place_id
83
83
  INNER JOIN cm_places AS dest_places ON dest_places.id = cm_trips.destination_place_id
84
84
  INNER JOIN spree_variants AS master ON master.product_id = cm_trips.product_id AND master.is_master = true
@@ -0,0 +1,11 @@
1
+ module Spree
2
+ module V2
3
+ module Tenant
4
+ class TransitCartSerializer < CartSerializer
5
+ has_many :line_items, serializer: SpreeCmCommissioner::V2::Storefront::TransitLineItemSerializer
6
+
7
+ cache_options store: nil
8
+ end
9
+ end
10
+ end
11
+ end
@@ -2,8 +2,6 @@ module SpreeCmCommissioner
2
2
  module V2
3
3
  module Operator
4
4
  class CheckInSerializer < BaseSerializer
5
- set_type :check_in
6
-
7
5
  attributes :guest_id,
8
6
  :verification_state,
9
7
  :check_in_type,
@@ -13,6 +11,7 @@ module SpreeCmCommissioner
13
11
  :token
14
12
 
15
13
  belongs_to :check_in_by, serializer: ::Spree::V2::Storefront::UserSerializer
14
+ belongs_to :check_in_session, serializer: ::SpreeCmCommissioner::V2::Operator::CheckInSessionSerializer
16
15
  belongs_to :guest, serializer: ::SpreeCmCommissioner::V2::Operator::GuestSerializer
17
16
  belongs_to :line_item, serializer: ::SpreeCmCommissioner::V2::Operator::LineItemSerializer
18
17
  end
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Operator
4
+ class CheckInSessionSerializer < BaseSerializer
5
+ attributes :name, :description, :event_id, :start_time, :end_time, :is_archived, :position
6
+ end
7
+ end
8
+ end
9
+ end
@@ -13,7 +13,7 @@ module SpreeCmCommissioner
13
13
 
14
14
  belongs_to :occupation, serializer: Spree::V2::Storefront::TaxonSerializer
15
15
 
16
- has_one :check_in, serializer: SpreeCmCommissioner::V2::Operator::CheckInSerializer
16
+ has_many :check_ins, serializer: SpreeCmCommissioner::V2::Operator::CheckInSerializer
17
17
  has_one :line_item, serializer: SpreeCmCommissioner::V2::Operator::LineItemSerializer
18
18
  end
19
19
  end
@@ -27,7 +27,8 @@ module SpreeCmCommissioner
27
27
  has_many :guest_dynamic_fields, serializer: SpreeCmCommissioner::V2::Storefront::GuestDynamicFieldSerializer
28
28
 
29
29
  has_one :id_card, serializer: Spree::V2::Storefront::IdCardSerializer
30
- has_one :check_in
30
+
31
+ has_many :check_ins, serializer: SpreeCmCommissioner::V2::Storefront::CheckInSerializer
31
32
 
32
33
  # allowed_checkout updates frequently
33
34
  cache_options store: nil
@@ -2,8 +2,10 @@ module SpreeCmCommissioner
2
2
  module V2
3
3
  module Storefront
4
4
  class RouteSerializer < BaseSerializer
5
- attributes :route_name
5
+ attributes :route_name, :route_type, :short_name
6
6
 
7
+ has_many :route_photos, serializer: ::SpreeCmCommissioner::V2::Storefront::AssetSerializer,
8
+ if: proc { |record, _params| record.respond_to?(:route_photos) }
7
9
  has_one :origin_place, serializer: ::Spree::V2::Storefront::PlaceSerializer
8
10
  has_one :destination_place, serializer: ::Spree::V2::Storefront::PlaceSerializer
9
11
  end
@@ -0,0 +1,17 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Storefront
4
+ class TransitLineItemSerializer < BaseSerializer
5
+ # Nested-only API shape
6
+ attributes :from_date,
7
+ :to_date,
8
+ :trip_id,
9
+ :direction,
10
+ :boarding_trip_stop_id,
11
+ :drop_off_trip_stop_id
12
+
13
+ cache_options store: nil
14
+ end
15
+ end
16
+ end
17
+ end
@@ -2,7 +2,7 @@ module SpreeCmCommissioner
2
2
  module V2
3
3
  module Storefront
4
4
  class TripStopSerializer < BaseSerializer
5
- attributes :id, :stop_type, :stop_name, :arrival_time, :departure_time
5
+ attributes :id, :allow_boarding, :allow_drop_off, :stop_name, :arrival_time, :departure_time
6
6
 
7
7
  has_one :stop_place, serializer: ::SpreeCmCommissioner::V2::Storefront::TripPlaceSerializer
8
8
  end
@@ -0,0 +1,65 @@
1
+ module SpreeCmCommissioner
2
+ module CheckIns
3
+ class CreateBulk
4
+ prepend Spree::ServiceModule::Base
5
+
6
+ def call(check_ins_attributes:, check_in_by: nil, confirmed_at: nil)
7
+ return failure(:guest_ids_must_not_blank) if check_ins_attributes.blank?
8
+
9
+ confirmed_at ||= Time.current
10
+ guest_ids = check_ins_attributes.pluck(:guest_id)
11
+ indexed_guests = SpreeCmCommissioner::Guest.where(id: guest_ids).index_by(&:id)
12
+
13
+ check_ins = ActiveRecord::Base.transaction do
14
+ check_ins_attributes.map do |attrs|
15
+ guest = indexed_guests[attrs[:guest_id].to_i]
16
+ raise ActiveRecord::RecordNotFound, "Couldn't find Guest with 'id'=#{attrs[:guest_id]}" if guest.blank?
17
+
18
+ check_in = check_in!(guest, attrs, check_in_by, confirmed_at)
19
+ update_guest!(guest, attrs[:guest_attributes]) if attrs[:guest_attributes].present?
20
+
21
+ check_in
22
+ end
23
+ end
24
+
25
+ success(check_ins: check_ins)
26
+ rescue ActiveRecord::RecordInvalid => e
27
+ failure(:invalid_record, e.record.errors.full_messages.join(', '))
28
+ rescue ActiveRecord::RecordNotFound => e
29
+ failure(:record_not_found, e.message)
30
+ end
31
+
32
+ private
33
+
34
+ def check_in!(guest, check_in_attributes, check_in_by, confirmed_at)
35
+ check_in = SpreeCmCommissioner::CheckIn.new(
36
+ guest: guest,
37
+ check_in_session_id: check_in_attributes[:check_in_session_id],
38
+ check_in_by: check_in_by,
39
+ checkinable: guest.event,
40
+ confirmed_at: check_in_attributes[:confirmed_at] || confirmed_at
41
+ )
42
+
43
+ # save will validate attributes, uniqueness of check-in etc.
44
+ check_in.save!
45
+
46
+ # Track state change with session ID in the name for audit trail
47
+ name = check_in_attributes[:check_in_session_id].present? ? "guest_#{check_in_attributes[:check_in_session_id]}" : 'guest'
48
+ guest.state_changes.create!(
49
+ user: check_in_by,
50
+ previous_state: 'unchecked_in',
51
+ next_state: 'checked_in',
52
+ name: name
53
+ )
54
+
55
+ check_in
56
+ end
57
+
58
+ def update_guest!(guest, guest_attributes)
59
+ return if guest_attributes.blank?
60
+
61
+ guest.update!(guest_attributes)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,54 @@
1
+ module SpreeCmCommissioner
2
+ module CheckIns
3
+ class DestroyBulk
4
+ prepend Spree::ServiceModule::Base
5
+
6
+ def call(check_ins_attributes:, destroyed_by: nil)
7
+ return failure(:guest_ids_must_not_blank) if check_ins_attributes.blank?
8
+
9
+ check_ins = []
10
+
11
+ ActiveRecord::Base.transaction do
12
+ check_ins = check_ins_attributes.map do |attrs|
13
+ guest = SpreeCmCommissioner::Guest.find(attrs[:guest_id])
14
+ destroy_check_in!(guest, attrs, destroyed_by)
15
+ end.compact
16
+
17
+ success(check_ins: check_ins)
18
+ end
19
+ rescue ActiveRecord::RecordNotFound => e
20
+ failure(:record_not_found, e.message)
21
+ rescue ActiveRecord::RecordInvalid => e
22
+ failure(:invalid_record, e.record.errors.full_messages.join(', '))
23
+ rescue StandardError => e
24
+ failure(:failed_to_destroy_check_ins, e.message)
25
+ end
26
+
27
+ private
28
+
29
+ def destroy_check_in!(guest, check_in_attributes, destroyed_by)
30
+ # Find the check-in to destroy
31
+ check_in = if check_in_attributes[:check_in_session_id].present?
32
+ guest.check_ins.find_by(check_in_session_id: check_in_attributes[:check_in_session_id])
33
+ else
34
+ guest.earliest_check_in
35
+ end
36
+
37
+ return nil if check_in.blank?
38
+
39
+ check_in.destroy!
40
+
41
+ # Track state change with session ID in the name for audit trail
42
+ name = check_in_attributes[:check_in_session_id].present? ? "guest_#{check_in_attributes[:check_in_session_id]}" : 'guest'
43
+ guest.state_changes.create!(
44
+ user: destroyed_by,
45
+ previous_state: 'checked_in',
46
+ next_state: 'unchecked_in',
47
+ name: name
48
+ )
49
+
50
+ check_in
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,22 @@
1
+ module SpreeCmCommissioner
2
+ module Guests
3
+ class PreloadCheckInSessionIds
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(event:)
7
+ return failure(nil, 'Event cannot be blank') if event.blank?
8
+
9
+ event.guests
10
+ .includes(event: { check_in_sessions: :check_in_rules })
11
+ .find_each(batch_size: 100) do |guest|
12
+ guest.preload_eligible_check_in_session_ids
13
+ guest.save(validate: false) # Skip validations for performance
14
+ end
15
+
16
+ success(true)
17
+ rescue StandardError => e
18
+ failure(nil, e.message)
19
+ end
20
+ end
21
+ end
22
+ end