spree_cm_commissioner 2.5.1.pre.pre2 → 2.5.1.pre.pre3

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/api/chatrace/guests_controller.rb +37 -20
  4. data/app/controllers/spree/api/v2/operator/check_in_bulks_controller.rb +9 -0
  5. data/app/controllers/spree/api/v2/storefront/account/qr_data_controller.rb +30 -0
  6. data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +1 -1
  7. data/app/controllers/spree/api/v2/storefront/transit/draft_orders_controller.rb +4 -4
  8. data/app/controllers/spree/transit/trips_controller.rb +3 -3
  9. data/app/finders/spree_cm_commissioner/places/find_with_route.rb +12 -12
  10. data/app/finders/spree_cm_commissioner/routes/find_popular.rb +35 -20
  11. data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +4 -11
  12. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +1 -10
  13. data/app/{services/spree_cm_commissioner/transit_order/create.rb → interactors/spree_cm_commissioner/transit/draft_order_creator.rb} +16 -13
  14. data/app/interactors/spree_cm_commissioner/trip_clone_creator.rb +3 -4
  15. data/app/interactors/spree_cm_commissioner/trip_stops_creator.rb +2 -2
  16. data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +2 -2
  17. data/app/jobs/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_job.rb +10 -0
  18. data/app/jobs/spree_cm_commissioner/transit/route_order_count_incrementer_job.rb +10 -0
  19. data/app/jobs/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_job.rb +13 -0
  20. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_decrementer_job.rb +10 -0
  21. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_incrementer_job.rb +10 -0
  22. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +16 -11
  23. data/app/models/concerns/spree_cm_commissioner/route_order_countable.rb +2 -2
  24. data/app/models/concerns/spree_cm_commissioner/route_trip_count_callbacks.rb +48 -0
  25. data/app/models/concerns/spree_cm_commissioner/user_identity.rb +0 -6
  26. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +4 -0
  27. data/app/models/spree_cm_commissioner/guest.rb +4 -4
  28. data/app/models/spree_cm_commissioner/place.rb +8 -5
  29. data/app/models/spree_cm_commissioner/product_decorator.rb +0 -1
  30. data/app/models/spree_cm_commissioner/route.rb +5 -46
  31. data/app/models/spree_cm_commissioner/trip.rb +33 -8
  32. data/app/models/spree_cm_commissioner/trip_connection.rb +36 -0
  33. data/app/models/spree_cm_commissioner/trip_stop.rb +2 -16
  34. data/app/models/spree_cm_commissioner/user_decorator.rb +30 -18
  35. data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -3
  36. data/app/models/spree_cm_commissioner/vendor_route.rb +9 -0
  37. data/app/queries/spree_cm_commissioner/guest_searcher_query.rb +25 -2
  38. data/app/queries/spree_cm_commissioner/trip_query.rb +2 -2
  39. data/app/serializers/spree/v2/storefront/user_serializer_decorator.rb +2 -1
  40. data/app/serializers/spree_cm_commissioner/v2/operator/guest_serializer.rb +1 -0
  41. data/app/serializers/spree_cm_commissioner/v2/operator/line_item_serializer.rb +2 -2
  42. data/app/serializers/spree_cm_commissioner/v2/operator/{line_item_order_serializer.rb → order_serializer.rb} +1 -2
  43. data/app/serializers/spree_cm_commissioner/v2/operator/user_serializer.rb +11 -0
  44. data/app/serializers/spree_cm_commissioner/v2/operator/variant_serializer.rb +9 -0
  45. data/app/serializers/spree_cm_commissioner/v2/storefront/route_serializer.rb +1 -3
  46. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_stop_serializer.rb +1 -1
  47. data/app/services/spree_cm_commissioner/{route_metrics/update_route_metrics.rb → routes/base_update_order_metrics.rb} +16 -11
  48. data/app/services/spree_cm_commissioner/routes/decrement_previous_trip_count.rb +30 -0
  49. data/app/services/spree_cm_commissioner/routes/decrement_trip_count.rb +33 -0
  50. data/app/services/spree_cm_commissioner/{route_metrics/increase_fulfilled_order_count.rb → routes/increment_fulfilled_order_count.rb} +3 -3
  51. data/app/services/spree_cm_commissioner/{route_metrics/increase_order_count.rb → routes/increment_order_count.rb} +3 -3
  52. data/app/services/spree_cm_commissioner/routes/increment_trip_count.rb +33 -0
  53. data/app/services/spree_cm_commissioner/seeds/user_usernames.rb +86 -0
  54. data/app/services/spree_cm_commissioner/users/qr_data/extract_login.rb +18 -0
  55. data/app/services/spree_cm_commissioner/users/qr_data/generate.rb +14 -0
  56. data/app/services/spree_cm_commissioner/users/qr_data/invalidate.rb +19 -0
  57. data/app/services/spree_cm_commissioner/users/qr_data/verify.rb +33 -0
  58. data/app/services/spree_cm_commissioner/users/username/generate.rb +68 -0
  59. data/app/views/spree/transit/trip_stops/index.html.erb +2 -4
  60. data/app/views/spree_cm_commissioner/guest_mailer/send_ticket_to_guest.html.erb +1 -0
  61. data/config/initializers/spree_permitted_attributes.rb +1 -11
  62. data/config/routes.rb +2 -6
  63. data/db/migrate/20260126110528_seed_user_initial_usernames.rb +5 -0
  64. data/lib/spree_cm_commissioner/test_helper/factories/route_factory.rb +6 -7
  65. data/lib/spree_cm_commissioner/test_helper/factories/trip_connection_factory.rb +6 -0
  66. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +1 -4
  67. data/lib/spree_cm_commissioner/test_helper/factories/trip_stop_factory.rb +1 -3
  68. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_type_factory.rb +1 -1
  69. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +1 -2
  70. data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +0 -22
  71. data/lib/spree_cm_commissioner/version.rb +1 -1
  72. data/lib/spree_cm_commissioner.rb +0 -4
  73. metadata +29 -38
  74. data/app/controllers/spree/api/v2/tenant/popular_route_places_controller.rb +0 -60
  75. data/app/controllers/spree/api/v2/tenant/routes_controller.rb +0 -50
  76. data/app/controllers/spree/api/v2/tenant/transit/draft_orders_controller.rb +0 -46
  77. data/app/finders/spree_cm_commissioner/route_metrics/find_popular.rb +0 -44
  78. data/app/finders/spree_cm_commissioner/routes/find.rb +0 -94
  79. data/app/jobs/spree_cm_commissioner/route_metrics/decrease_trip_count_job.rb +0 -10
  80. data/app/jobs/spree_cm_commissioner/route_metrics/increase_fulfilled_order_count_job.rb +0 -10
  81. data/app/jobs/spree_cm_commissioner/route_metrics/increase_order_count_job.rb +0 -10
  82. data/app/jobs/spree_cm_commissioner/route_metrics/increase_trip_count_job.rb +0 -10
  83. data/app/models/spree_cm_commissioner/route_metric.rb +0 -21
  84. data/app/models/spree_cm_commissioner/route_photo.rb +0 -12
  85. data/app/serializers/spree/v2/tenant/transit_cart_serializer.rb +0 -11
  86. data/app/serializers/spree_cm_commissioner/v2/storefront/transit_line_item_serializer.rb +0 -17
  87. data/app/services/spree_cm_commissioner/route_metrics/decrease_trip_count.rb +0 -31
  88. data/app/services/spree_cm_commissioner/route_metrics/increase_trip_count.rb +0 -31
  89. data/app/services/spree_cm_commissioner/routes/create.rb +0 -51
  90. data/app/services/spree_cm_commissioner/routes/update.rb +0 -25
  91. data/app/services/spree_cm_commissioner/trips/create_single_leg.rb +0 -123
  92. data/app/services/spree_cm_commissioner/trips/service_calendars/create_or_update.rb +0 -54
  93. data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +0 -88
  94. data/app/services/spree_cm_commissioner/trips/variants/create.rb +0 -103
  95. data/db/migrate/20251224033103_migrate_cm_routes_to_cm_route_metrics.rb +0 -17
  96. data/db/migrate/20251224033910_migrate_cm_vendor_routes_to_cm_routes.rb +0 -30
  97. data/db/migrate/20260105072450_migrate_cm_trip_stops_to_support_trip_connection.rb +0 -12
  98. data/db/migrate/20260108101406_add_allow_booking_to_cm_trips.rb +0 -5
  99. data/lib/spree_cm_commissioner/test_helper/factories/route_metric_factory.rb +0 -12
  100. data/lib/spree_cm_commissioner/test_helper/factories/route_photo_factory.rb +0 -5
  101. data/lib/spree_cm_commissioner/transit/route_stop.rb +0 -61
  102. data/lib/spree_cm_commissioner/transit/route_stop_collection.rb +0 -175
  103. data/lib/spree_cm_commissioner/transit/trip_form.rb +0 -81
  104. data/lib/spree_cm_commissioner/transit/trip_stop_form.rb +0 -61
@@ -0,0 +1,48 @@
1
+ module SpreeCmCommissioner
2
+ module RouteTripCountCallbacks
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ after_create :increase_trip_count
7
+ before_destroy :decrease_trip_count
8
+
9
+ # When route context may change, enqueue an async job after commit that
10
+ # reads the previous values from `previous_changes` to decrement counts
11
+ # on the old route/vendor. This avoids keeping in-memory state and keeps
12
+ # the Trip update fast.
13
+ after_commit :enqueue_decrement_previous_route_trip_counts, on: :update
14
+
15
+ after_commit :increment_new_route_trip_counts, on: :update, if: :changed_route_context?
16
+ end
17
+
18
+ private
19
+
20
+ def decrease_trip_count
21
+ SpreeCmCommissioner::Transit::RouteTripCountDecrementerJob.perform_now(trip_id: id)
22
+ end
23
+
24
+ def increase_trip_count
25
+ SpreeCmCommissioner::Transit::RouteTripCountIncrementerJob.perform_later(trip_id: id)
26
+ end
27
+
28
+ # Read previous_changes to find prior route context and enqueue the job.
29
+ def enqueue_decrement_previous_route_trip_counts
30
+ prev_route_id = previous_changes['route_id']&.first
31
+ prev_origin_id = previous_changes['origin_place_id']&.first
32
+ prev_destination_id = previous_changes['destination_place_id']&.first
33
+
34
+ return unless prev_route_id.present? && prev_origin_id.present? && prev_destination_id.present?
35
+
36
+ SpreeCmCommissioner::Transit::RoutePreviousTripCountDecrementerJob.perform_later(
37
+ previous_route_id: prev_route_id
38
+ )
39
+ end
40
+
41
+ # Safely increment the trip_count for the trip's (new) route after update.
42
+ def increment_new_route_trip_counts
43
+ return if origin_place_id.blank? || destination_place_id.blank? || route_id.blank?
44
+
45
+ SpreeCmCommissioner::Transit::RouteTripCountIncrementerJob.perform_later(trip_id: id)
46
+ end
47
+ end
48
+ end
@@ -51,12 +51,6 @@ module SpreeCmCommissioner
51
51
  end
52
52
  end
53
53
 
54
- # override:spree_auth_devise:app/models/spree/user.rb
55
- def set_login
56
- self.login ||= phone_number if phone_number
57
- self.login ||= email if email
58
- end
59
-
60
54
  # override:devise:lib/devise/models/validatable.rb
61
55
  def email_required?
62
56
  require_login_identity_all_blank_for(:email)
@@ -85,6 +85,10 @@ module SpreeCmCommissioner
85
85
  options.end_time
86
86
  end
87
87
 
88
+ def bib_required?
89
+ bib_prefix.present?
90
+ end
91
+
88
92
  def post_paid?
89
93
  options.payment_option == 'post-paid'
90
94
  end
@@ -62,7 +62,7 @@ module SpreeCmCommissioner
62
62
  before_validation :assign_seat_number_with_block, if: -> { will_save_change_to_block_id? }
63
63
 
64
64
  before_save :preload_eligible_check_in_session_ids, if: -> { new_record? }
65
- before_create :generate_bib, if: -> { line_item.reload && variant.bib_pre_generation_on_create? }
65
+ before_create :generate_bib_if_needed, if: -> { line_item.reload && variant.bib_pre_generation_on_create? }
66
66
  after_create :preload_order_block_ids, if: -> { block_id.present? }
67
67
  after_update :preload_order_block_ids, if: :saved_change_to_block_id?
68
68
  before_destroy :cancel_reserved_block!, if: -> { reserved_block.present? }
@@ -241,14 +241,14 @@ module SpreeCmCommissioner
241
241
  end
242
242
 
243
243
  def bib_required?
244
- line_item.variant.bib_prefix.present?
244
+ line_item.variant.bib_required?
245
245
  end
246
246
 
247
247
  def bib_display_prefix?
248
248
  line_item.variant.bib_display_prefix?
249
249
  end
250
250
 
251
- def generate_bib
251
+ def generate_bib_if_needed
252
252
  return if bib_prefix.present?
253
253
  return unless bib_required?
254
254
  return if event_id.blank?
@@ -264,7 +264,7 @@ module SpreeCmCommissioner
264
264
  end
265
265
 
266
266
  def generate_bib!
267
- generate_bib
267
+ generate_bib_if_needed
268
268
  save!
269
269
  end
270
270
 
@@ -21,12 +21,15 @@ 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 :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
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
26
26
 
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 }
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 }
30
33
 
31
34
  def self.ransackable_attributes(auth_object = nil)
32
35
  super & %w[name code]
@@ -48,7 +48,6 @@ 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
52
51
 
53
52
  base.belongs_to :event, class_name: 'Spree::Taxon', optional: true
54
53
 
@@ -1,53 +1,12 @@
1
1
  module SpreeCmCommissioner
2
2
  class Route < Base
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
-
3
+ has_many :vendor_routes, class_name: 'SpreeCmCommissioner::VendorRoute', dependent: :destroy
4
+ has_many :vendors, through: :vendor_routes
17
5
  has_many :trips, inverse_of: :route
18
6
 
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?
7
+ belongs_to :origin_place, class_name: 'SpreeCmCommissioner::Place'
8
+ belongs_to :destination_place, class_name: 'SpreeCmCommissioner::Place'
49
9
 
50
- route_stops.errors.each { |error| errors.add(:route_stops, error) }
51
- end
10
+ validates :origin_place_id, uniqueness: { scope: :destination_place_id }
52
11
  end
53
12
  end
@@ -2,6 +2,7 @@ module SpreeCmCommissioner
2
2
  class Trip < Base
3
3
  include SpreeCmCommissioner::StoreMetadata
4
4
  include SpreeCmCommissioner::RouteType
5
+ include SpreeCmCommissioner::RouteTripCountCallbacks
5
6
 
6
7
  attr_accessor :hours, :minutes, :seconds
7
8
 
@@ -14,9 +15,7 @@ module SpreeCmCommissioner
14
15
 
15
16
  # before create, duplicate seat layout from vehicle_type if present
16
17
  before_validation :duplicate_seat_layout_from_vehicle, if: -> { seat_layout.nil? && vehicle_type&.seat_layout.present? }
17
-
18
- after_create :increment_route_metric_trip_count
19
- before_destroy :decrement_route_metric_trip_count
18
+ before_validation :assign_route_and_vendor_route, if: :changed_route_attributes?
20
19
 
21
20
  belongs_to :vendor, class_name: 'Spree::Vendor', inverse_of: :trips, optional: false
22
21
  belongs_to :product, class_name: 'Spree::Product', inverse_of: :trip, optional: false
@@ -27,7 +26,7 @@ module SpreeCmCommissioner
27
26
 
28
27
  belongs_to :origin_place, class_name: 'SpreeCmCommissioner::Place', optional: false
29
28
  belongs_to :destination_place, class_name: 'SpreeCmCommissioner::Place', optional: false
30
- belongs_to :route, class_name: 'SpreeCmCommissioner::Route', inverse_of: :trips, optional: true
29
+ belongs_to :route, inverse_of: :trips
31
30
 
32
31
  has_many :trip_blazer_queries, as: :queryable, class_name: 'SpreeCmCommissioner::BlazerQueryable'
33
32
  has_many :blazer_queries, through: :trip_blazer_queries, source: :blazer_query, class_name: 'Blazer::Query'
@@ -49,6 +48,10 @@ module SpreeCmCommissioner
49
48
  self.whitelisted_ransackable_associations = %w[product vehicle vehicle_type]
50
49
  self.whitelisted_ransackable_attributes = %w[origin_place_id destination_place_id]
51
50
 
51
+ def changed_route_attributes?
52
+ origin_place_id_changed? || destination_place_id_changed? || vendor_id_changed?
53
+ end
54
+
52
55
  def convert_duration_to_seconds
53
56
  return if hours.blank? && minutes.blank? && seconds.blank?
54
57
 
@@ -101,12 +104,34 @@ module SpreeCmCommissioner
101
104
  )
102
105
  end
103
106
 
104
- def increment_route_metric_trip_count
105
- SpreeCmCommissioner::RouteMetrics::IncreaseTripCountJob.perform_later(trip_id: id)
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?
106
121
  end
107
122
 
108
- def decrement_route_metric_trip_count
109
- SpreeCmCommissioner::RouteMetrics::DecreaseTripCountJob.perform_later(trip_id: id)
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
110
135
  end
111
136
  end
112
137
  end
@@ -0,0 +1,36 @@
1
+ module SpreeCmCommissioner
2
+ class TripConnection < Base
3
+ belongs_to :from_trip, class_name: 'SpreeCmCommissioner::Trip'
4
+ belongs_to :to_trip, class_name: 'SpreeCmCommissioner::Trip'
5
+
6
+ validate :both_trip_cannot_be_the_same
7
+ before_validation :calculate_connection_time_minutes
8
+ validates :from_trip_id, uniqueness: { scope: :to_trip_id }
9
+
10
+ private
11
+
12
+ def calculate_connection_time_minutes
13
+ return if from_trip.nil? || to_trip.nil?
14
+
15
+ arrival_seconds = from_trip.arrival_time.seconds_since_midnight
16
+ departure_seconds = to_trip.departure_time.seconds_since_midnight
17
+
18
+ layover_seconds = departure_seconds - arrival_seconds
19
+ layover_seconds += 86_400 if layover_seconds.negative?
20
+
21
+ connection_time_in_minutes = layover_seconds / 60
22
+
23
+ if connection_time_in_minutes.between?(0, 180)
24
+ self.connection_time_minutes = connection_time_in_minutes.to_i
25
+ else
26
+ errors.add(:base, 'Connection time must be less than 3 hours')
27
+ end
28
+ end
29
+
30
+ def both_trip_cannot_be_the_same
31
+ return unless from_trip.id == to_trip.id
32
+
33
+ errors.add(:base, 'Both trip cannot be the same')
34
+ end
35
+ end
36
+ end
@@ -1,39 +1,25 @@
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 }
4
5
 
5
6
  belongs_to :trip, class_name: 'SpreeCmCommissioner::Trip', optional: false
6
7
  belongs_to :stop_place, class_name: 'SpreeCmCommissioner::Place', optional: false
7
8
  belongs_to :location_place, class_name: 'SpreeCmCommissioner::Place', optional: false
8
9
 
9
- belongs_to :board_to_trip, class_name: 'SpreeCmCommissioner::Trip', optional: true
10
-
11
10
  before_validation :set_stop_name
12
-
13
11
  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) }
18
12
 
19
13
  def set_stop_name
20
14
  self.stop_name = stop_place.name if stop_place.present?
21
15
  end
22
16
 
23
17
  def self.ransackable_attributes(_auth_object = nil)
24
- %w[stop_name allow_boarding allow_drop_off]
18
+ %w[stop_name stop_type]
25
19
  end
26
20
 
27
21
  def self.ransackable_associations(_auth_object = nil)
28
22
  ['stop_place']
29
23
  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
38
24
  end
39
25
  end
@@ -31,6 +31,8 @@ module SpreeCmCommissioner
31
31
 
32
32
  base.has_many :wished_items, class_name: 'Spree::WishedItem', through: :wishlists
33
33
  base.has_many :promotions, through: :promotion_rules, class_name: 'Spree::Promotion'
34
+ base.has_many :user_places, class_name: 'SpreeCmCommissioner::UserPlace'
35
+ base.has_many :places, through: :user_places, class_name: 'SpreeCmCommissioner::Place'
34
36
 
35
37
  base.has_one :profile, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::UserProfile'
36
38
  base.has_one :customer, class_name: 'SpreeCmCommissioner::Customer'
@@ -43,26 +45,27 @@ module SpreeCmCommissioner
43
45
  base.scope :by_non_tenant, -> { where(tenant_id: nil) }
44
46
  base.scope :vendor_users, -> (vendor_id) { joins(:role_users => :role).where(spree_roles: { vendor_id: vendor_id }).distinct }
45
47
 
46
- base.whitelisted_ransackable_attributes = %w[email first_name last_name gender phone_number]
47
-
48
- base.before_save :update_otp_enabled
49
- base.attr_accessor :assigned_roles
50
- base.validates_password_strength :password, if: -> { requires_strong_password? && password.present? }
51
-
52
- # Store has_incomplete_guest_info in public_metadata for easy frontend access
53
48
  base.store :public_metadata, accessors: [:has_incomplete_guest_info], coder: JSON
49
+ base.store :private_metadata, accessors: %i[otp_secret otp_required_for_login consumed_timestep], coder: JSON
50
+
54
51
  base.store_public_metadata :primary_service_type, :string
52
+ base.store_public_metadata :qr_data_version, :integer, default: 1
53
+ base.store_public_metadata :qr_data_invalidated_at, :datetime
54
+ base.store_public_metadata :branch_ids, :array, default: []
55
+
56
+ # Validations
55
57
  base.validates :primary_service_type,
56
58
  inclusion: { in: SpreeCmCommissioner::ServiceType::SERVICE_TYPES.map(&:to_s) },
57
59
  allow_nil: true
58
- base.store_public_metadata :branch_ids, :array, default: []
59
-
60
- base.devise :two_factor_authenticatable
61
-
62
- base.store :private_metadata, accessors: %i[otp_secret otp_required_for_login consumed_timestep], coder: JSON
60
+ base.validates_password_strength :password, if: -> { requires_strong_password? && password.present? }
63
61
 
64
- define_user_places(base)
62
+ # Login/username affect QR data, so invalidate it on change.
63
+ base.before_validation :invalidate_qr_data, if: :will_save_change_to_login?
64
+ base.before_save :update_otp_enabled
65
65
 
66
+ base.attr_accessor :assigned_roles
67
+ base.whitelisted_ransackable_attributes = %w[email first_name last_name gender phone_number]
68
+ base.devise :two_factor_authenticatable
66
69
  def base.end_users
67
70
  joins('LEFT JOIN spree_vendor_users ON spree_users.id = spree_vendor_users.user_id').where(spree_vendor_users: { user_id: nil })
68
71
  end
@@ -78,6 +81,10 @@ module SpreeCmCommissioner
78
81
  end
79
82
  end
80
83
 
84
+ def qr_data
85
+ SpreeCmCommissioner::Users::QrData::Generate.call(user: self).value
86
+ end
87
+
81
88
  def otp_secret
82
89
  private_metadata['otp_secret']
83
90
  end
@@ -90,15 +97,15 @@ module SpreeCmCommissioner
90
97
  private_metadata['consumed_timestep']
91
98
  end
92
99
 
93
- def self.define_user_places(base)
94
- base.has_many :user_places, class_name: 'SpreeCmCommissioner::UserPlace'
95
- base.has_many :places, through: :user_places, class_name: 'SpreeCmCommissioner::Place'
96
- end
97
-
98
100
  def permissions_for_vendor(vendor_id)
99
101
  permissions.joins(role_permissions: :role).where(spree_roles: { vendor_id: vendor_id })
100
102
  end
101
103
 
104
+ # override to auto-generate clean usernames (eg. JohnTraveler1 format)
105
+ def set_login
106
+ self.login ||= SpreeCmCommissioner::Users::Username::Generate.call(user: self)
107
+ end
108
+
102
109
  # override
103
110
  def has_spree_role?(role_name) # rubocop:disable Naming/PredicateName
104
111
  spree_roles.non_vendor.exists?(name: role_name)
@@ -148,6 +155,11 @@ module SpreeCmCommissioner
148
155
  phone_number
149
156
  end
150
157
 
158
+ def invalidate_qr_data
159
+ self.qr_data_version += 1
160
+ self.qr_data_invalidated_at = Time.zone.now
161
+ end
162
+
151
163
  def ensure_unique_database_delivery_method(attributes)
152
164
  recipient = self
153
165
 
@@ -82,9 +82,7 @@ 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 :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'
85
+ base.has_many :vendor_routes, class_name: 'SpreeCmCommissioner::VendorRoute'
88
86
 
89
87
  base.has_many :homepage_section_relatables,
90
88
  as: :relatable,
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ class VendorRoute < Base
3
+ belongs_to :vendor, class_name: 'Spree::Vendor', optional: false
4
+ belongs_to :route, class_name: 'SpreeCmCommissioner::Route', optional: false
5
+
6
+ has_many :trips, -> (vr) { where(vendor_id: vr.vendor_id, route_id: vr.route_id) }, class_name: 'SpreeCmCommissioner::Trip'
7
+ validates :vendor_id, uniqueness: { scope: :route_id }
8
+ end
9
+ end
@@ -8,7 +8,7 @@ module SpreeCmCommissioner
8
8
 
9
9
  def event_id = params[:event_id]
10
10
 
11
- def call # rubocop:disable Metrics/PerceivedComplexity
11
+ def call # rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
12
12
  return SpreeCmCommissioner::Guest.none if event_id.blank?
13
13
 
14
14
  if params[:qr_data].present? && order_qr_data?
@@ -17,6 +17,11 @@ module SpreeCmCommissioner
17
17
  search_by_line_item_qr
18
18
  elsif params[:qr_data].present? && guest_qr_data?
19
19
  search_by_guest_qr
20
+ elsif params[:qr_data].present? && user_qr_data?
21
+ search_by_user_qr
22
+ # QR data provided but didn't match any format, return none
23
+ elsif params[:qr_data].present?
24
+ SpreeCmCommissioner::Guest.none
20
25
  elsif params[:term].present?
21
26
  search_by_term
22
27
  elsif params[:ids].present?
@@ -45,6 +50,13 @@ module SpreeCmCommissioner
45
50
  )
46
51
  end
47
52
 
53
+ def search_by_user_qr
54
+ result = SpreeCmCommissioner::Users::QrData::Verify.call(qr_data: params[:qr_data])
55
+ return SpreeCmCommissioner::Guest.none if result.failure?
56
+
57
+ result.value.guests.complete_or_canceled.where(event_id: event_id)
58
+ end
59
+
48
60
  def order_qr_data?
49
61
  matches = construct_matches
50
62
  matches&.size == 2
@@ -57,7 +69,18 @@ module SpreeCmCommissioner
57
69
 
58
70
  def guest_qr_data?
59
71
  matches = construct_matches
60
- matches.nil?
72
+ uuid_format = params[:qr_data].match?(/\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/)
73
+ matches.nil? && uuid_format
74
+ end
75
+
76
+ # When login is present in the QR payload, we treat it as user QR data.
77
+ # This method only extracts login from qr_data; it does not verify or load any records, so it is fast.
78
+ # Once we have identified it as user QR data, we perform the actual lookup and verification elsewhere.
79
+ def user_qr_data?
80
+ @user_qr_data ||= begin
81
+ result = SpreeCmCommissioner::Users::QrData::ExtractLogin.call(qr_data: params[:qr_data])
82
+ result.success? ? result.value : nil
83
+ end.present?
61
84
  end
62
85
 
63
86
  def construct_matches
@@ -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.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
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
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
@@ -5,7 +5,8 @@ module Spree
5
5
  def self.prepended(base)
6
6
  base.attributes :first_name, :last_name, :gender, :phone_number, :intel_phone_number,
7
7
  :country_code, :otp_enabled, :otp_email, :otp_phone_number,
8
- :confirm_pin_code_enabled, :tenant_id, :has_incomplete_guest_info
8
+ :confirm_pin_code_enabled, :tenant_id, :has_incomplete_guest_info,
9
+ :login, :qr_data, :qr_data_version, :qr_data_invalidated_at
9
10
 
10
11
  base.has_one :profile, serializer: ::Spree::V2::Storefront::UserProfileSerializer
11
12
  base.has_many :device_tokens, serializer: Spree::V2::Storefront::UserDeviceTokenSerializer
@@ -12,6 +12,7 @@ module SpreeCmCommissioner
12
12
  attribute :require_kyc_field, &:require_kyc_field?
13
13
 
14
14
  belongs_to :occupation, serializer: Spree::V2::Storefront::TaxonSerializer
15
+ belongs_to :user, serializer: SpreeCmCommissioner::V2::Operator::UserSerializer
15
16
 
16
17
  has_many :check_ins, serializer: SpreeCmCommissioner::V2::Operator::CheckInSerializer
17
18
  has_one :line_item, serializer: SpreeCmCommissioner::V2::Operator::LineItemSerializer
@@ -6,8 +6,8 @@ module SpreeCmCommissioner
6
6
 
7
7
  attributes :number, :name, :quantity, :options_text, :qr_data, :kyc_fields, :available_social_contact_platforms
8
8
 
9
- belongs_to :order, serializer: SpreeCmCommissioner::V2::Operator::LineItemOrderSerializer
10
- has_one :variant, serializer: SpreeCmCommissioner::V2::Storefront::EventVariantSerializer
9
+ belongs_to :order, serializer: SpreeCmCommissioner::V2::Operator::OrderSerializer
10
+ has_one :variant, serializer: SpreeCmCommissioner::V2::Operator::VariantSerializer
11
11
  has_many :guests, serializer: SpreeCmCommissioner::V2::Operator::GuestSerializer
12
12
  end
13
13
  end
@@ -1,10 +1,9 @@
1
1
  module SpreeCmCommissioner
2
2
  module V2
3
3
  module Operator
4
- class LineItemOrderSerializer < BaseSerializer
4
+ class OrderSerializer < BaseSerializer
5
5
  set_type :order
6
6
 
7
- has_one :user, serializer: Spree::V2::Storefront::UserSerializer
8
7
  belongs_to :billing_address,
9
8
  id_method_name: :bill_address_id,
10
9
  record_type: :address,
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Operator
4
+ class UserSerializer < BaseSerializer
5
+ attributes :email, :login,
6
+ :phone_number, :intel_phone_number, :country_code,
7
+ :first_name, :last_name, :gender
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Operator
4
+ class VariantSerializer < BaseSerializer
5
+ attributes :color, :ticket_type, :seat_type
6
+ end
7
+ end
8
+ end
9
+ end
@@ -2,10 +2,8 @@ module SpreeCmCommissioner
2
2
  module V2
3
3
  module Storefront
4
4
  class RouteSerializer < BaseSerializer
5
- attributes :route_name, :route_type, :short_name
5
+ attributes :route_name
6
6
 
7
- has_many :route_photos, serializer: ::SpreeCmCommissioner::V2::Storefront::AssetSerializer,
8
- if: proc { |record, _params| record.respond_to?(:route_photos) }
9
7
  has_one :origin_place, serializer: ::Spree::V2::Storefront::PlaceSerializer
10
8
  has_one :destination_place, serializer: ::Spree::V2::Storefront::PlaceSerializer
11
9
  end
@@ -2,7 +2,7 @@ module SpreeCmCommissioner
2
2
  module V2
3
3
  module Storefront
4
4
  class TripStopSerializer < BaseSerializer
5
- attributes :id, :allow_boarding, :allow_drop_off, :stop_name, :arrival_time, :departure_time
5
+ attributes :id, :stop_type, :stop_name, :arrival_time, :departure_time
6
6
 
7
7
  has_one :stop_place, serializer: ::SpreeCmCommissioner::V2::Storefront::TripPlaceSerializer
8
8
  end