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,88 @@
1
+ module SpreeCmCommissioner
2
+ module Trips
3
+ # Service class responsible for updating an existing single-leg trip.
4
+ # Handles updates to trip, product, variant, stops, and calendar in the booking system.
5
+ class UpdateSingleLeg
6
+ prepend Spree::ServiceModule::Base
7
+
8
+ # Main service method that orchestrates trip updates within a database transaction.
9
+ # Updates trip/product, variant if price provided, stops, and calendar conditionally.
10
+ # Returns success with updated objects or failure with error message.
11
+ def call(trip:, trip_form:)
12
+ ApplicationRecord.transaction do
13
+ update_trip_and_product(trip, trip_form)
14
+ update_variant(trip, trip_form) if trip_form.price.present?
15
+ update_trip_stops(trip, trip_form)
16
+ calendar = update_calendar(trip.product, trip_form.service_calendar) if trip_form.service_calendar.present?
17
+
18
+ success(trip: trip, variant: trip.product.variants.first, calendar: calendar)
19
+ rescue StandardError => e
20
+ CmAppLogger.error(
21
+ label: 'SpreeCmCommissioner::Trips::UpdateSingleLeg#call',
22
+ data: {
23
+ error_class: e.class.name,
24
+ error_message: e.message
25
+ }
26
+ )
27
+ failure(nil, 'Failed to update trip')
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # Updates the trip record and associated product with attributes from the form.
34
+ def update_trip_and_product(trip, trip_form)
35
+ trip.update!(trip_attrs(trip_form))
36
+ trip.product.update!(product_attrs(trip_form))
37
+ end
38
+
39
+ # Updates the variant's price if a new price is provided in the form.
40
+ def update_variant(trip, trip_form)
41
+ variant = trip.product.variants.where.not(id: trip.product.master.id).first
42
+ return unless variant
43
+
44
+ variant.update!(price: trip_form.price)
45
+ trip.product.update!(price: trip_form.price)
46
+ end
47
+
48
+ # Iterates through stops and applies updates only if attributes are present.
49
+ def update_trip_stops(trip, trip_form)
50
+ trip.trip_stops.each_with_index do |stop, idx|
51
+ ts_form = trip_form.trip_stops[idx]
52
+ next unless ts_form
53
+
54
+ updates = {
55
+ allow_boarding: ts_form.allow_boarding?,
56
+ allow_drop_off: ts_form.allow_drop_off
57
+ }.compact
58
+
59
+ stop.update!(updates) if updates.any?
60
+ end
61
+ end
62
+
63
+ # Updates or creates the service calendar for the product using calendar form data.
64
+ def update_calendar(product, calendar_form)
65
+ result = SpreeCmCommissioner::Trips::ServiceCalendars::CreateOrUpdate.call(
66
+ calendarable: product,
67
+ name: calendar_form.name,
68
+ start_date: calendar_form.start_date,
69
+ end_date: calendar_form.end_date,
70
+ weekdays: calendar_form.weekdays,
71
+ exception_rules: calendar_form.exception_rules || []
72
+ )
73
+
74
+ raise result.error unless result.success?
75
+
76
+ result.value[:calendar]
77
+ end
78
+
79
+ def trip_attrs(form)
80
+ { allow_booking: form.allow_booking, allow_seat_selection: form.allow_seat_selection }.compact
81
+ end
82
+
83
+ def product_attrs(form)
84
+ { name: form.name, short_name: form.short_name }.compact
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,103 @@
1
+ module SpreeCmCommissioner
2
+ module Trips
3
+ module Variants
4
+ # Service class responsible for creating product and variant for a trip.
5
+ # Handles validations, product creation with options, variant pricing, and stock setup.
6
+ class Create
7
+ prepend ::Spree::ServiceModule::Base
8
+
9
+ # Main method that validates inputs and creates product, options, variant, and stock in transaction.
10
+ # Returns success with product and variant or failure with error message.
11
+ def call(vendor:, trip_form:, price:, capacity:)
12
+ return failure(nil, 'vendor must be present') if vendor.blank?
13
+ return failure(nil, 'trip_form must be present') if trip_form.blank?
14
+ return failure(nil, 'price must be present') if price.blank?
15
+ return failure(nil, 'capacity must be present') if capacity.blank?
16
+ return failure(nil, 'capacity must be greater than 0') if capacity <= 0
17
+
18
+ ApplicationRecord.transaction do
19
+ product = create_product!(vendor, trip_form)
20
+ option_values = setup_option_values!(product)
21
+ variant = create_variant!(product, price, option_values)
22
+
23
+ create_stock_item!(variant, capacity)
24
+
25
+ success(product: product, variant: variant)
26
+ end
27
+ rescue StandardError => e
28
+ CmAppLogger.error(
29
+ label: 'SpreeCmCommissioner::Trips::Variants::Create#call',
30
+ data: {
31
+ error_class: e.class.name,
32
+ error_message: e.message
33
+ }
34
+ )
35
+ failure(nil, e.message)
36
+ end
37
+
38
+ private
39
+
40
+ # Creates a transit product with name based on stops, assigns vendor, type, and options.
41
+ # Includes seat-type option type and default store association.
42
+ def create_product!(vendor, trip_form)
43
+ vendor_stops = stops(vendor, trip_form)
44
+ first_stop = vendor_stops[trip_form.trip_stops.first.stop_id].place
45
+ last_stop = vendor_stops[trip_form.trip_stops.last.stop_id].place
46
+
47
+ Spree::Product.create!(
48
+ name: trip_form.name.presence || "#{first_stop.name} - #{last_stop.name}",
49
+ short_name: trip_form.short_name,
50
+ vendor: vendor,
51
+ product_type: 'transit',
52
+ status: 'active',
53
+ available_on: Time.current,
54
+ price: trip_form.price,
55
+ shipping_category: Spree::ShippingCategory.find_or_create_by!(name: 'Default'),
56
+ option_types: [Spree::OptionType.find_by(name: 'seat-type')].compact,
57
+ stores: [Spree::Store.default].compact
58
+ )
59
+ end
60
+
61
+ # Retrieves and indexes stops from the vendor for the trip form's stop IDs.
62
+ def stops(vendor, trip_form)
63
+ vendor.stops.where(id: trip_form.trip_stops.map(&:stop_id)).index_by(&:id)
64
+ end
65
+
66
+ # Sets up or finds the seat-type option type and creates normal option value.
67
+ # Associates the option type with the product if not already present.
68
+ def setup_option_values!(product)
69
+ option_type = Spree::OptionType.find_or_create_by!(
70
+ name: 'seat-type',
71
+ presentation: 'Seat Type'
72
+ )
73
+
74
+ product.option_types << option_type unless product.option_types.include?(option_type)
75
+
76
+ option_value = option_type.option_values.find_or_create_by_name!(option_type, 'Normal')
77
+
78
+ [option_value]
79
+ end
80
+
81
+ # Creates a product variant with the given price and associated option values.
82
+ # Saves the variant to the database.
83
+ def create_variant!(product, price, option_values)
84
+ variant = product.variants.new(price: price)
85
+ variant.option_values = option_values
86
+ variant.save!
87
+ variant
88
+ end
89
+
90
+ # Creates a stock item for the variant with the specified capacity.
91
+ # Uses stock movement creator to initialize inventory.
92
+ def create_stock_item!(variant, capacity)
93
+ result = SpreeCmCommissioner::Stock::StockMovementCreator.call(
94
+ variant_id: variant.id,
95
+ current_store: variant.product.stores.first,
96
+ stock_movement_params: { quantity: capacity }
97
+ )
98
+ result.stock_item
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -9,7 +9,8 @@
9
9
  <tr data-hook="option_header">
10
10
  <th class="no-border handel-head"></th>
11
11
  <th><%= Spree.t(:name) %></th>
12
- <th><%= Spree.t(:stop_type) %></th>
12
+ <th><%= Spree.t(:allow_boarding) %></th>
13
+ <th><%= Spree.t(:allow_drop_off) %></th>
13
14
  <th><%= Spree.t(:sequence) %></th>
14
15
  <th><%= Spree.t(:created_at) %></th>
15
16
  </tr>
@@ -21,7 +22,8 @@
21
22
  <%= svg_icon name: "grip-vertical.svg", width: '18', height: '18' %>
22
23
  </td>
23
24
  <td><%= stop.stop_name %></td>
24
- <td> <%= stop.stop_type %></td>
25
+ <td> <%= stop.allow_boarding ? 'Yes' : 'No' %></td>
26
+ <td> <%= stop.allow_drop_off ? 'Yes' : 'No' %></td>
25
27
  <td> <%= stop.sequence %></td>
26
28
  <td> <%= stop.created_at.to_date %></td>
27
29
  </tr>
@@ -151,7 +151,6 @@
151
151
  <% @trip.trip_stops.order(:sequence).each do |stop| %>
152
152
  <tr>
153
153
  <td>
154
- <span class="stop-type <%= stop.stop_type %>"><%= stop.stop_type.to_s.titleize %></span>
155
154
  <div class="stop-name"><strong><%= stop.stop_name %></strong></div>
156
155
  <% if stop.respond_to?(:location_place) && stop.location_place.present? %>
157
156
  <% if stop.location_place.respond_to?(:address) && stop.location_place.address.present? %>
@@ -1,5 +1,8 @@
1
1
  module Spree
2
2
  module PermittedAttributes
3
+ ATTRIBUTES << :route_attributes unless ATTRIBUTES.include?(:route_attributes)
4
+ mattr_reader :route_attributes
5
+
3
6
  @@vendor_attributes << :logo
4
7
 
5
8
  # Permitted all guest attributes for now as permitting only some guest attributes is not working by design
@@ -85,5 +88,13 @@ module Spree
85
88
  originator_type
86
89
  originator_id
87
90
  ]
91
+
92
+ @@route_attributes = %i[
93
+ route_name
94
+ short_name
95
+ vendor_id
96
+ route_type
97
+ route_stops
98
+ ]
88
99
  end
89
100
  end
@@ -480,6 +480,8 @@ en:
480
480
  success: "Guest uncheck-in in successfully"
481
481
  check_in:
482
482
  already_checked_in: "Guest has already checked in for this session"
483
+ check_in_session_required: "Check-in session required"
484
+ check_in_session_is_not_allowed_or_invalid: "Check-in session is not allowed or invalid for this guest/ticket"
483
485
 
484
486
  activerecord:
485
487
  attributes:
@@ -24,6 +24,8 @@ km:
24
24
  success: "Guest uncheck-in in successfully"
25
25
  check-in:
26
26
  already_checked_in: "ភ្ញៀវបានចូលរួចហើយ"
27
+ check_in_session_required: "ត្រូវតែមានវគ្គចុះឈ្មោះចូល"
28
+ check_in_session_is_not_allowed_or_invalid: "វគ្គចុះឈ្មោះនេះមិនត្រឹមត្រូវ សម្រាប់ភ្ញៀវ ឬសំបុត្រនេះទេ"
27
29
 
28
30
  sms:
29
31
  to:
data/config/routes.rb CHANGED
@@ -560,6 +560,12 @@ Spree::Core::Engine.add_routes do
560
560
  resources :trip_places, only: :index
561
561
  resources :trip_search, only: [:index]
562
562
  resources :trips, only: %i[show]
563
+ resources :popular_route_places, only: [:index]
564
+ resources :routes, only: [:index]
565
+
566
+ namespace :transit do
567
+ resources :draft_orders, only: %i[create]
568
+ end
563
569
 
564
570
  namespace :intercity_taxi do
565
571
  resource :draft_orders, only: %i[create update]
@@ -721,6 +727,8 @@ Spree::Core::Engine.add_routes do
721
727
  resources :dashboard_crew_events, only: %i[index]
722
728
  resources :event_qrs, only: [:show]
723
729
  resources :recalculate_tickets, only: [:create]
730
+ resources :check_in_sessions, only: %i[index]
731
+
724
732
  resources :taxons, only: %i[show] do
725
733
  resource :event_ticket_aggregators, only: %i[show]
726
734
  resource :pie_chart_event_aggregators, only: %i[show]
@@ -0,0 +1,17 @@
1
+ class MigrateCmRoutesToCmRouteMetrics < ActiveRecord::Migration[7.0]
2
+ def change
3
+ # 1. Rename cm_routes which we use to track order, trip count to cm_route_metrics instead.
4
+ remove_index :cm_routes, [:origin_place_id, :destination_place_id] if index_exists?(:cm_routes, [:origin_place_id, :destination_place_id])
5
+ rename_table :cm_routes, :cm_route_metrics unless table_exists?(:cm_route_metrics)
6
+ add_column :cm_route_metrics, :route_type, :integer, default: 0, null: false unless column_exists?(:cm_route_metrics, :route_type)
7
+
8
+ # 2. Drop the old index before renaming table to avoid index name length issues & re-add the index with a shorter name
9
+ unless index_exists?(:cm_route_metrics, [:route_type, :origin_place_id, :destination_place_id], name: 'index_cm_route_metrics_on_origin_dest')
10
+ add_index :cm_route_metrics, [:route_type, :origin_place_id, :destination_place_id], unique: true, name: 'index_cm_route_metrics_on_origin_dest'
11
+ end
12
+
13
+ # 3. Remove old route_id column from trips.
14
+ # it has route_metrics value, and trip don't need to have association with metric directly.
15
+ remove_column :cm_trips, :route_id if column_exists?(:cm_trips, :route_id)
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ class MigrateCmVendorRoutesToCmRoutes < ActiveRecord::Migration[7.0]
2
+ def change
3
+ # 1. Remove cm_vendor_routes and add cm_routes instead for routes that associated with vendor.
4
+ drop_table :cm_vendor_routes, if_exists: true
5
+ create_table :cm_routes, if_not_exists: true do |t|
6
+ t.string :route_name
7
+ t.string :short_name
8
+ t.integer :order_count
9
+ t.integer :fulfilled_order_count
10
+ t.integer :route_type
11
+ t.jsonb :route_stops, default: "[]"
12
+ t.references :vendor, foreign_key: { to_table: :spree_vendors }, null: false
13
+ t.references :tenant, foreign_key: { to_table: :cm_tenants }, null: true
14
+
15
+ t.references :origin_place, foreign_key: { to_table: :cm_places }, null: false
16
+ t.references :destination_place, foreign_key: { to_table: :cm_places }, null: false
17
+
18
+ t.integer :lock_version, default: 0, null: false
19
+ t.timestamps
20
+ end
21
+
22
+ # 2. Add indexes for popular_routes association (vendor_id + order by fulfilled_order_count, order_count)
23
+ add_index :cm_routes, [:vendor_id, :route_type, :fulfilled_order_count, :order_count],
24
+ order: { fulfilled_order_count: :desc, order_count: :desc },
25
+ name: 'index_cm_routes_on_vendor_id_route_type_and_popular' unless index_exists?(:cm_routes, [:vendor_id, :route_type, :fulfilled_order_count, :order_count])
26
+
27
+ # 3. Add new route reference to cm_trips
28
+ add_reference :cm_trips, :route, foreign_key: { to_table: :cm_routes }, null: true unless column_exists?(:cm_trips, :route_id)
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ class AddAgeGroupToCmGuests < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :cm_guests, :age_group, :integer, if_not_exists: true
4
+ add_index :cm_guests, :age_group, if_not_exists: true
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ class AddTypeToCmPricingActions < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :cm_pricing_actions,
4
+ :type,
5
+ :string,
6
+ default: 'SpreeCmCommissioner::PricingActions::CreateLineItemAdjustments',
7
+ if_not_exists: true
8
+
9
+ add_index :cm_pricing_actions, :type, if_not_exists: true
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ class MigrateCmTripStopsToSupportTripConnection < ActiveRecord::Migration[7.0]
2
+ def change
3
+ # 1. Replace stop_type with allow_boarding & allow_drop_off instead, it is more flexible.
4
+ # Useful for middle stops where both boarding & drop off can be allowed or both can be disallowed.
5
+ remove_column :cm_trip_stops, :stop_type if column_exists?(:cm_trip_stops, :stop_type)
6
+ add_column :cm_trip_stops, :allow_boarding, :boolean unless column_exists?(:cm_trip_stops, :allow_boarding)
7
+ add_column :cm_trip_stops, :allow_drop_off, :boolean unless column_exists?(:cm_trip_stops, :allow_drop_off)
8
+
9
+ # 2. Add references to link trip stops to trips for boarding, this will enable trip connections. Where current trip is main trip.
10
+ add_reference :cm_trip_stops, :board_to_trip, foreign_key: { to_table: :cm_trips }, null: true unless column_exists?(:cm_trip_stops, :board_to_trip_id)
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ class AddAllowBookingToCmTrips < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :cm_trips, :allow_booking, :boolean, default: true, if_not_exists: true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddNationalityGroupToCmGuests < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :cm_guests, :nationality_group, :integer, default: 0, null: false, if_not_exists: true
4
+ end
5
+ end
@@ -0,0 +1,40 @@
1
+ ## 📌 Summary
2
+
3
+ Add **age-based pricing rules** for **discounts or surcharges** based on guest age groups
4
+ Supports both **line-item** and **per-guest** adjustments.
5
+
6
+ ---
7
+
8
+ ## ⚙️ How It Works
9
+
10
+ ### 👶 Children Discount (Per-Guest)
11
+
12
+ - **Rule:** `all` + `['child']`
13
+ - **Adjustment:** $1 per child
14
+ - **Guests:** 3 children ($6 each)
15
+ - **Calculation:** 3 × $6 = $18 → $3 total discount
16
+ - **Total:** **$15**
17
+
18
+ ### 👧 Children Surcharge (Per-Guest)
19
+
20
+ - **Rule:** `any` + `['child']`
21
+ - **Adjustment:** $0.50 per child
22
+ - **Guests:** 2 children ($6 each)
23
+ - **Calculation:** 2 × $6 = $12 → $1 total surcharge
24
+ - **Total:** **$13**
25
+
26
+ ### 🧑 Adult Discount (Line-Item)
27
+
28
+ - **Rule:** `any` + `['adult']`
29
+ - **Adjustment:** $1 discount
30
+ - **Guests:** 2 adults, 2 children ($6 each)
31
+ - **Calculation:** 4 × $6 = $24 → $1 discount
32
+ - **Total:** **$23**
33
+
34
+ ### 🧔 Adult Surcharge (Line-Item)
35
+
36
+ - **Rule:** `none` + `['adult']`
37
+ - **Adjustment:** $0.50 surcharge
38
+ - **Guests:** 3 teenagers ($6 each)
39
+ - **Calculation:** 3 × $6 = $18 → $0.50 surcharge
40
+ - **Total:** **$18.50**
@@ -0,0 +1,35 @@
1
+ ## 📌 Summary
2
+ Add **nationality-based pricing rules** for **discounts or surcharges** based on guest nationality groups.
3
+ Supports both **line-item** and **per-guest** adjustments.
4
+
5
+ ---
6
+
7
+ ## ⚙️ How It Works
8
+
9
+ ### Local Discount (Per-Guest)
10
+ - **Rule:** `any` + `['local']`
11
+ - **Adjustment:** $1 per local
12
+ - **Guests:** 2 locals ($6 each)
13
+ - **Calculation:** 2 × $6 = $12 → $2 total discount
14
+ - **Total:** **$10**
15
+
16
+ ### Local Discount (Line-Item)
17
+ - **Rule:** `any` + `['local']`
18
+ - **Adjustment:** $1 discount
19
+ - **Guests:** 2 locals, 1 foreigner ($6 each)
20
+ - **Calculation:** 3 × $6 = $18 → $1 discount
21
+ - **Total:** **$17**
22
+
23
+ ### 🌍 Foreigner Surcharge (Per-Guest)
24
+ - **Rule:** `any` + `['foreigner']`
25
+ - **Adjustment:** $2 per foreigner
26
+ - **Guests:** 1 local, 2 foreigners ($6 each)
27
+ - **Calculation:** 2 × $2 = $4 total surcharge
28
+ - **Total:** **$20**
29
+
30
+ ### 🌍 Foreigner Surcharge (Line-Item)
31
+ - **Rule:** `none` + `['foreigner']`
32
+ - **Adjustment:** $0.50 surcharge
33
+ - **Guests:** 3 locals ($6 each)
34
+ - **Calculation:** 3 × $6 = $18 → $0.50 surcharge
35
+ - **Total:** **$18.50**
@@ -2,8 +2,10 @@ FactoryBot.define do
2
2
  factory :cm_check_in, class: SpreeCmCommissioner::CheckIn do
3
3
  confirmed_at { DateTime.current }
4
4
  check_in_type { "pre_check_in" }
5
- guest_id { 1 }
5
+ association :guest, factory: :cm_guest
6
6
  check_in_method { "manual" }
7
+ check_in_by { create(:user) }
8
+ checkinable { create(:cm_taxon_event) }
7
9
  verification_state { "submitted" }
8
10
  end
9
11
  end
@@ -7,5 +7,31 @@ FactoryBot.define do
7
7
  start_time { 1.hour.from_now }
8
8
  end_time { 2.hours.from_now }
9
9
  association :event, factory: :cm_taxon_event
10
+
11
+ transient do
12
+ products { nil }
13
+ variants { nil }
14
+ option_values { nil }
15
+ end
16
+
17
+ after(:build) do |check_in_session, evaluator|
18
+ if evaluator.products.present?
19
+ evaluator.products.each do |product|
20
+ check_in_session.check_in_rules.build(ruleable: product)
21
+ end
22
+ end
23
+
24
+ if evaluator.variants.present?
25
+ evaluator.variants.each do |variant|
26
+ check_in_session.check_in_rules.build(ruleable: variant)
27
+ end
28
+ end
29
+
30
+ if evaluator.option_values.present?
31
+ evaluator.option_values.each do |option_value|
32
+ check_in_session.check_in_rules.build(ruleable: option_value)
33
+ end
34
+ end
35
+ end
10
36
  end
11
37
  end
@@ -7,6 +7,8 @@ FactoryBot.define do
7
7
  gender { 1 }
8
8
  dob { '1986-03-28' }
9
9
  token { SecureRandom.hex(32) }
10
+ public_metadata { {} }
11
+ private_metadata { {} }
10
12
  end
11
13
 
12
14
  factory :cm_guest, class: SpreeCmCommissioner::Guest do
@@ -16,5 +18,7 @@ FactoryBot.define do
16
18
  gender { 1 }
17
19
  dob { '1986-03-28' }
18
20
  token { SecureRandom.hex(32) }
21
+ public_metadata { {} }
22
+ private_metadata { {} }
19
23
  end
20
24
  end
@@ -2,4 +2,8 @@ FactoryBot.define do
2
2
  factory :pricing_action, class: 'SpreeCmCommissioner::PricingAction' do
3
3
  association :pricing_rule_group, factory: :pricing_rule_group
4
4
  end
5
+
6
+ factory :pricing_action_guest_adjustments, class: 'SpreeCmCommissioner::PricingActions::CreateGuestAdjustments' do
7
+ association :pricing_rule_group, factory: :pricing_rule_group
8
+ end
5
9
  end
@@ -1,7 +1,7 @@
1
1
  FactoryBot.define do
2
2
  factory :pricing_rule_nationality, class: 'SpreeCmCommissioner::PricingRules::Nationality' do
3
3
  association :pricing_rule_group, factory: :pricing_rule_group
4
- rule_type { 'must' }
4
+ rule_type { 'any' }
5
5
  nationalities { ["Cambodian"] }
6
6
  end
7
7
 
@@ -14,4 +14,48 @@ FactoryBot.define do
14
14
  association :pricing_rule_group, factory: :pricing_rule_group
15
15
  min_distance { 5 }
16
16
  end
17
+
18
+ factory :pricing_rule_age_group, class: 'SpreeCmCommissioner::PricingRules::AgeGroup' do
19
+ association :pricing_rule_group, factory: :pricing_rule_group
20
+ rule_type { 'any' }
21
+ age_groups { ['adult'] }
22
+
23
+ trait :all_match do
24
+ rule_type { 'all' }
25
+ end
26
+
27
+ trait :none_match do
28
+ rule_type { 'none' }
29
+ end
30
+
31
+ trait :children do
32
+ age_groups { ['child'] }
33
+ end
34
+
35
+ trait :adults do
36
+ age_groups { ['adult'] }
37
+ end
38
+ end
39
+
40
+ factory :pricing_rule_nationality_group, class: 'SpreeCmCommissioner::PricingRules::NationalityGroup' do
41
+ association :pricing_rule_group, factory: :pricing_rule_group
42
+ rule_type { 'any' }
43
+ nationality_groups { ['local'] }
44
+
45
+ trait :all_match do
46
+ rule_type { 'all' }
47
+ end
48
+
49
+ trait :none_match do
50
+ rule_type { 'none' }
51
+ end
52
+
53
+ trait :locals do
54
+ nationality_groups { ['local'] }
55
+ end
56
+
57
+ trait :foreigners do
58
+ nationality_groups { ['foreigner'] }
59
+ end
60
+ end
17
61
  end
@@ -1,10 +1,11 @@
1
1
  FactoryBot.define do
2
- factory :cm_route, class: 'SpreeCmCommissioner::Route' do
2
+ factory :cm_route, class: SpreeCmCommissioner::Route do
3
+ association :vendor, factory: :vendor
4
+ association :tenant, factory: :cm_tenant
5
+ association :origin_place, factory: :cm_place
6
+ association :destination_place, factory: :cm_place
7
+
3
8
  sequence(:route_name) { |n| "Route #{n}" }
4
- origin_place_id { create(:cm_place).id }
5
- destination_place_id { create(:cm_place).id }
6
- trip_count { 0 }
7
- order_count { 0 }
8
- fulfilled_order_count { 0 }
9
+ sequence(:short_name) { |n| "R#{n}" }
9
10
  end
10
11
  end
@@ -0,0 +1,12 @@
1
+ FactoryBot.define do
2
+ factory :cm_route_metric, class: SpreeCmCommissioner::RouteMetric do
3
+ association :origin_place, factory: :cm_place
4
+ association :destination_place, factory: :cm_place
5
+
6
+ route_name { "#{origin_place.name} - #{destination_place.name}" }
7
+ route_type { :bus }
8
+ trip_count { 0 }
9
+ order_count { 0 }
10
+ fulfilled_order_count { 0 }
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ FactoryBot.define do
2
+ factory :route_photo, class: SpreeCmCommissioner::RoutePhoto do
3
+ attachment { Rack::Test::UploadedFile.new(SpreeMultiVendor::Engine.root.join('spec', 'fixtures', 'thinking-cat.jpg'), 'image/jpeg') }
4
+ end
5
+ end
@@ -11,7 +11,10 @@ FactoryBot.define do
11
11
  association :vehicle, factory: :cm_vehicle
12
12
  association :origin_place, factory: :cm_place
13
13
  association :destination_place, factory: :cm_place
14
- association :route, factory: :cm_route
14
+
15
+ # Don't create route by default to avoid uniqueness conflicts
16
+ # Tests that need these should create them explicitly
17
+ route { nil }
15
18
  end
16
19
 
17
20
  factory :cm_trip_with_seat_counts, parent: :cm_trip do
@@ -4,7 +4,9 @@ FactoryBot.define do
4
4
  association :stop_place, factory: :cm_place
5
5
  association :location_place, factory: :cm_place
6
6
 
7
- stop_type { :boarding }
7
+ allow_boarding { true }
8
+ allow_drop_off { true }
9
+
8
10
  stop_name { stop_place.name }
9
11
 
10
12
  arrival_time { Time.current }