spree_cm_commissioner 2.5.1 → 2.5.2.pre.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test_and_build_gem.yml +2 -2
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/spree/admin/inventory_items_controller.rb +36 -56
  5. data/app/controllers/spree/admin/stock_managements_controller.rb +14 -3
  6. data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +2 -2
  7. data/app/controllers/spree/api/v2/storefront/transit/draft_orders_controller.rb +4 -4
  8. data/app/controllers/spree/api/v2/tenant/popular_route_places_controller.rb +60 -0
  9. data/app/controllers/spree/api/v2/tenant/routes_controller.rb +50 -0
  10. data/app/controllers/spree/api/v2/tenant/transit/draft_orders_controller.rb +46 -0
  11. data/app/controllers/spree/transit/trips_controller.rb +3 -3
  12. data/app/finders/spree_cm_commissioner/places/find_with_route.rb +12 -12
  13. data/app/finders/spree_cm_commissioner/route_metrics/find_popular.rb +44 -0
  14. data/app/finders/spree_cm_commissioner/routes/find.rb +94 -0
  15. data/app/finders/spree_cm_commissioner/routes/find_popular.rb +19 -35
  16. data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +11 -4
  17. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +10 -1
  18. data/app/interactors/spree_cm_commissioner/trip_clone_creator.rb +4 -3
  19. data/app/interactors/spree_cm_commissioner/trip_stops_creator.rb +2 -2
  20. data/app/jobs/spree_cm_commissioner/route_metrics/decrease_trip_count_job.rb +10 -0
  21. data/app/jobs/spree_cm_commissioner/route_metrics/increase_fulfilled_order_count_job.rb +10 -0
  22. data/app/jobs/spree_cm_commissioner/route_metrics/increase_order_count_job.rb +10 -0
  23. data/app/jobs/spree_cm_commissioner/route_metrics/increase_trip_count_job.rb +10 -0
  24. data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +2 -2
  25. data/app/models/concerns/spree_cm_commissioner/route_order_countable.rb +2 -2
  26. data/app/models/spree_cm_commissioner/place.rb +5 -8
  27. data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
  28. data/app/models/spree_cm_commissioner/route.rb +45 -5
  29. data/app/models/spree_cm_commissioner/route_metric.rb +21 -0
  30. data/app/models/spree_cm_commissioner/route_photo.rb +12 -0
  31. data/app/models/spree_cm_commissioner/trip.rb +8 -33
  32. data/app/models/spree_cm_commissioner/trip_stop.rb +16 -2
  33. data/app/models/spree_cm_commissioner/vendor_decorator.rb +3 -1
  34. data/app/queries/spree_cm_commissioner/trip_query.rb +2 -2
  35. data/app/serializers/spree/v2/tenant/transit_cart_serializer.rb +11 -0
  36. data/app/serializers/spree_cm_commissioner/v2/storefront/route_metric_serializer.rb +13 -0
  37. data/app/serializers/spree_cm_commissioner/v2/storefront/route_serializer.rb +2 -1
  38. data/app/serializers/spree_cm_commissioner/v2/storefront/transit_line_item_serializer.rb +17 -0
  39. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_stop_serializer.rb +1 -1
  40. data/app/services/spree_cm_commissioner/route_metrics/decrease_trip_count.rb +31 -0
  41. data/app/services/spree_cm_commissioner/{routes/increment_fulfilled_order_count.rb → route_metrics/increase_fulfilled_order_count.rb} +3 -3
  42. data/app/services/spree_cm_commissioner/{routes/increment_order_count.rb → route_metrics/increase_order_count.rb} +3 -3
  43. data/app/services/spree_cm_commissioner/route_metrics/increase_trip_count.rb +31 -0
  44. data/app/services/spree_cm_commissioner/{routes/base_update_order_metrics.rb → route_metrics/update_route_metrics.rb} +11 -16
  45. data/app/services/spree_cm_commissioner/routes/create.rb +51 -0
  46. data/app/services/spree_cm_commissioner/routes/update.rb +25 -0
  47. data/app/{interactors/spree_cm_commissioner/transit/draft_order_creator.rb → services/spree_cm_commissioner/transit_order/create.rb} +13 -16
  48. data/app/services/spree_cm_commissioner/trips/create_single_leg.rb +123 -0
  49. data/app/services/spree_cm_commissioner/trips/service_calendars/create_or_update.rb +54 -0
  50. data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +88 -0
  51. data/app/services/spree_cm_commissioner/trips/variants/create.rb +103 -0
  52. data/app/views/spree/admin/inventory_items/prices.html.erb +45 -0
  53. data/app/views/spree/admin/inventory_items/stocks.html.erb +36 -0
  54. data/app/views/spree/admin/stock_managements/calendar.html.erb +105 -12
  55. data/app/views/spree/admin/stock_managements/index.html.erb +9 -8
  56. data/app/views/spree/transit/trip_stops/index.html.erb +4 -2
  57. data/app/views/spree_cm_commissioner/guest_mailer/send_ticket_to_guest.html.erb +0 -1
  58. data/config/initializers/spree_permitted_attributes.rb +11 -0
  59. data/config/routes.rb +12 -7
  60. data/db/migrate/20251224033103_migrate_cm_routes_to_cm_route_metrics.rb +17 -0
  61. data/db/migrate/20251224033910_migrate_cm_vendor_routes_to_cm_routes.rb +30 -0
  62. data/db/migrate/20260105072450_migrate_cm_trip_stops_to_support_trip_connection.rb +12 -0
  63. data/db/migrate/20260108101406_add_allow_booking_to_cm_trips.rb +5 -0
  64. data/lib/spree_cm_commissioner/test_helper/factories/route_factory.rb +7 -6
  65. data/lib/spree_cm_commissioner/test_helper/factories/route_metric_factory.rb +12 -0
  66. data/lib/spree_cm_commissioner/test_helper/factories/route_photo_factory.rb +5 -0
  67. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +4 -1
  68. data/lib/spree_cm_commissioner/test_helper/factories/trip_stop_factory.rb +3 -1
  69. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +2 -0
  70. data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +22 -0
  71. data/lib/spree_cm_commissioner/transit/route_stop.rb +61 -0
  72. data/lib/spree_cm_commissioner/transit/route_stop_collection.rb +175 -0
  73. data/lib/spree_cm_commissioner/transit/trip_form.rb +81 -0
  74. data/lib/spree_cm_commissioner/transit/trip_stop_form.rb +65 -0
  75. data/lib/spree_cm_commissioner/version.rb +1 -1
  76. data/lib/spree_cm_commissioner.rb +4 -0
  77. metadata +42 -21
  78. data/app/jobs/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_job.rb +0 -10
  79. data/app/jobs/spree_cm_commissioner/transit/route_order_count_incrementer_job.rb +0 -10
  80. data/app/jobs/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_job.rb +0 -13
  81. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_decrementer_job.rb +0 -10
  82. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_incrementer_job.rb +0 -10
  83. data/app/models/concerns/spree_cm_commissioner/route_trip_count_callbacks.rb +0 -48
  84. data/app/models/spree_cm_commissioner/trip_connection.rb +0 -36
  85. data/app/models/spree_cm_commissioner/vendor_route.rb +0 -9
  86. data/app/services/spree_cm_commissioner/routes/decrement_previous_trip_count.rb +0 -30
  87. data/app/services/spree_cm_commissioner/routes/decrement_trip_count.rb +0 -33
  88. data/app/services/spree_cm_commissioner/routes/increment_trip_count.rb +0 -33
  89. data/app/views/spree/admin/inventory_items/show.html.erb +0 -72
  90. data/lib/spree_cm_commissioner/test_helper/factories/trip_connection_factory.rb +0 -6
@@ -0,0 +1,31 @@
1
+ module SpreeCmCommissioner
2
+ module RouteMetrics
3
+ class IncreaseTripCount
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(trip:)
7
+ return failure(nil, 'Trip not found') unless trip
8
+
9
+ route_metric = find_or_create_route_metric(trip)
10
+
11
+ route_metric.with_lock do
12
+ route_metric.update!(trip_count: route_metric.trip_count + 1)
13
+ end
14
+
15
+ success(trip: trip)
16
+ rescue StandardError => e
17
+ failure(nil, e.message)
18
+ end
19
+
20
+ private
21
+
22
+ def find_or_create_route_metric(trip)
23
+ SpreeCmCommissioner::RouteMetric.find_or_create_by!(
24
+ origin_place_id: trip.origin_place_id,
25
+ destination_place_id: trip.destination_place_id,
26
+ route_type: trip.route_type
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,6 +1,6 @@
1
1
  module SpreeCmCommissioner
2
- module Routes
3
- class BaseUpdateOrderMetrics
2
+ module RouteMetrics
3
+ class UpdateRouteMetrics
4
4
  attr_reader :order, :attribute
5
5
 
6
6
  SUPPORTED_ATTRIBUTES = %i[order_count fulfilled_order_count].freeze
@@ -31,26 +31,21 @@ module SpreeCmCommissioner
31
31
  trip = product&.trip
32
32
  return unless trip
33
33
 
34
- route = find_or_create_route_for_trip(trip)
35
- ensure_trip_route(trip, route)
36
- increment_route_by(route, line_item.quantity)
34
+ route_metric = find_or_create_route_metric_for_trip(trip)
35
+ increment_route_by(route_metric, line_item.quantity)
36
+
37
+ route = trip.route
38
+ increment_route_by(route, line_item.quantity) if route
37
39
  end
38
40
 
39
- def find_or_create_route_for_trip(trip)
40
- SpreeCmCommissioner::Route.find_or_create_by(
41
+ def find_or_create_route_metric_for_trip(trip)
42
+ SpreeCmCommissioner::RouteMetric.find_or_create_by(
41
43
  origin_place_id: trip.origin_place_id,
42
- destination_place_id: trip.destination_place_id
44
+ destination_place_id: trip.destination_place_id,
45
+ route_type: trip.route_type
43
46
  )
44
47
  end
45
48
 
46
- def ensure_trip_route(trip, route)
47
- trip_route_id = trip.respond_to?(:route_id) ? trip.route_id : nil
48
-
49
- return unless trip.respond_to?(:update_column) && trip_route_id != route.id
50
-
51
- trip.update_column(:route_id, route.id) # rubocop:disable Rails/SkipsModelValidations
52
- end
53
-
54
49
  def increment_route_by(route, qty)
55
50
  route.with_lock do
56
51
  current_value = route.public_send(attribute) || 0
@@ -0,0 +1,51 @@
1
+ module SpreeCmCommissioner
2
+ module Routes
3
+ class Create
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ # @param vendor [Spree::Vendor]
7
+ # @param route_params [Hash]
8
+ # @param route_stops [SpreeCmCommissioner::Transit::RouteStopCollection]
9
+ def call(vendor:, route_params:, route_stops:)
10
+ # vendor_id, tenant_id, origin_place_id, destination_place_id are excluded and set automatically.
11
+ attrs = ::Spree::PermittedAttributes.route_attributes
12
+ .index_with { |attr_key| route_params[attr_key] }
13
+ .except(:vendor_id, :tenant_id, :route_stops, :origin_place_id, :destination_place_id)
14
+ .compact
15
+
16
+ return failure(nil, 'Route stops are required') if route_stops.length < 2
17
+
18
+ origin_place = find_place_for(route_stops, route_stops.first)
19
+ destination_place = find_place_for(route_stops, route_stops.last)
20
+
21
+ return failure(nil, 'Origin or destination place could not be determined') if origin_place.nil? || destination_place.nil?
22
+ return failure(nil, 'Route with origin & destination already exists') if vendor.routes.exists?(
23
+ origin_place_id: origin_place.id,
24
+ destination_place_id: destination_place.id
25
+ )
26
+
27
+ attrs = attrs.merge(
28
+ tenant_id: vendor.tenant_id,
29
+ route_name: attrs[:route_name] || "#{origin_place.name} -> #{destination_place.name}",
30
+ origin_place_id: origin_place.id,
31
+ destination_place_id: destination_place.id,
32
+ route_stops: route_stops
33
+ )
34
+
35
+ route = vendor.routes.new(attrs)
36
+ if route.save
37
+ success(route: route)
38
+ else
39
+ failure(route.errors)
40
+ end
41
+ end
42
+
43
+ def find_place_for(route_stops, route_stop)
44
+ return nil if route_stop.nil?
45
+
46
+ @locations ||= SpreeCmCommissioner::VendorPlace.where(id: route_stops.map(&:location_id).uniq).index_by(&:id)
47
+ @locations[route_stop.location_id]&.place
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ module SpreeCmCommissioner
2
+ module Routes
3
+ class Update
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(vendor:, route:, route_params:)
7
+ # vendor_id, tenant_id, origin_place_id, destination_place_id are excluded and set automatically.
8
+ # route_stops is not updateable.
9
+ attrs = ::Spree::PermittedAttributes.route_attributes
10
+ .index_with { |attr_key| route_params[attr_key] }
11
+ .except(:vendor_id, :tenant_id, :route_stops, :origin_place_id, :destination_place_id)
12
+ .compact
13
+
14
+ route = vendor.routes.find_by(id: route.id)
15
+ return failure(nil, :route_not_found) if route.nil?
16
+
17
+ if route.update(attrs)
18
+ success(route: route)
19
+ else
20
+ failure(route.errors)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,4 +1,4 @@
1
- # DraftOrderCreator creates a new order for a entire journey, including outbound and (if present) inbound directions.
1
+ # SpreeCmCommissioner::TransitOrder::Create creates a new order for a entire journey, including outbound and (if present) inbound directions.
2
2
  #
3
3
  # Attributes:
4
4
  # - outbound_date: [Date] The date of outbound transit
@@ -12,25 +12,22 @@
12
12
  #
13
13
  # Note: inbound_legs and inbound_date are optional; the order may represent a one-way (outbound only) journey.
14
14
  module SpreeCmCommissioner
15
- module Transit
16
- class DraftOrderCreator < BaseInteractor
17
- delegate :outbound_date,
18
- :inbound_date,
19
- :outbound_legs,
20
- :inbound_legs,
21
- :user, to: :context
15
+ module TransitOrder
16
+ class Create
17
+ prepend ::Spree::ServiceModule::Base
22
18
 
23
- def call
24
- return context.fail!(message: 'Outbound legs are missing') if outbound_legs.blank?
19
+ def call(outbound_date:, inbound_date:, outbound_legs:, inbound_legs: [], user: nil)
20
+ return failure(nil, 'Outbound legs are missing') if outbound_legs.blank?
25
21
 
26
22
  begin
27
- context.order = create_order!
23
+ order = create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user)
24
+ success(order: order)
28
25
  rescue StandardError => e
29
- context.fail!(message: e.message)
26
+ failure(nil, e.message)
30
27
  end
31
28
  end
32
29
 
33
- def create_order!
30
+ def create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user)
34
31
  order = Spree::Order.new(state: 'cart', use_billing: true, user: user)
35
32
 
36
33
  outbound_line_items = build_line_items_for_legs!(order: order, legs: outbound_legs, initial_date: outbound_date)
@@ -112,10 +109,10 @@ module SpreeCmCommissioner
112
109
 
113
110
  def insert_saved_guests_per_line_items_leg(line_items)
114
111
  line_items.flat_map(&:guests).each_with_index do |guest, index|
115
- context.saved_guests ||= []
116
- context.saved_guests << SpreeCmCommissioner::SavedGuest.new if context.saved_guests[index].blank?
112
+ @saved_guests ||= []
113
+ @saved_guests << SpreeCmCommissioner::SavedGuest.new if @saved_guests[index].blank?
117
114
 
118
- guest.saved_guest = context.saved_guests[index]
115
+ guest.saved_guest = @saved_guests[index]
119
116
  end
120
117
 
121
118
  line_items
@@ -0,0 +1,123 @@
1
+ module SpreeCmCommissioner
2
+ module Trips
3
+ # Service class responsible for creating a single-leg trip in the booking system.
4
+ # Orchestrates the creation of product variant, trip record, stops, calendar, and inventory.
5
+ class CreateSingleLeg
6
+ prepend Spree::ServiceModule::Base
7
+
8
+ # Main service method that validates the trip form and orchestrates creation.
9
+ # Creates variant, trip, calendar, and inventory within a database transaction.
10
+ # Returns success with created objects or failure with error message on exceptions.
11
+ def call(vendor:, trip_form:)
12
+ return failure(nil, trip_form.errors.full_messages.to_sentence) unless trip_form.valid_data?
13
+
14
+ ApplicationRecord.transaction do
15
+ variant = create_variant!(vendor, trip_form)
16
+ trip = create_trip!(vendor, trip_form, variant)
17
+ calendar = setup_calendar!(variant.product, trip_form.service_calendar)
18
+ generate_inventory!(variant)
19
+
20
+ success(trip: trip, variant: variant, calendar: calendar)
21
+ end
22
+ rescue StandardError => e
23
+ CmAppLogger.error(
24
+ label: 'SpreeCmCommissioner::Trips::CreateSingleLeg#call',
25
+ data: {
26
+ error_class: e.class.name,
27
+ error_message: e.message
28
+ }
29
+ )
30
+ failure(nil, e.message)
31
+ end
32
+
33
+ private
34
+
35
+ # Creates a product variant for the trip using the vehicle type's seat capacity.
36
+ def create_variant!(vendor, trip_form)
37
+ first_stop = trip_form.trip_stops.first
38
+ vehicle_type = SpreeCmCommissioner::VehicleType.find(first_stop.vehicle_type_id)
39
+ capacity = vehicle_type.number_of_seats
40
+
41
+ result = SpreeCmCommissioner::Trips::Variants::Create.call(
42
+ vendor: vendor,
43
+ trip_form: trip_form,
44
+ price: trip_form.price || 0.0,
45
+ capacity: capacity
46
+ )
47
+
48
+ raise result.error unless result.success?
49
+
50
+ result.value[:variant]
51
+ end
52
+
53
+ # Creates the main trip record with details extracted from the trip form.
54
+ # Associates the trip with vendor, product, vehicle, route, and time.
55
+ def create_trip!(vendor, trip_form, variant)
56
+ first_stop = trip_form.trip_stops.first
57
+ last_stop = trip_form.trip_stops.last
58
+
59
+ locations = vendor.locations.where(id: trip_form.trip_stops.map(&:location_id)).index_by(&:id)
60
+ stops = vendor.stops.where(id: trip_form.trip_stops.map(&:stop_id)).index_by(&:id)
61
+
62
+ trip = vendor.trips.create!(
63
+ product: variant.product,
64
+ vehicle_type: SpreeCmCommissioner::VehicleType.find_by(id: first_stop.vehicle_type_id),
65
+ vehicle: SpreeCmCommissioner::Vehicle.find_by(id: first_stop.vehicle_id),
66
+ origin_place: locations[first_stop.location_id].place,
67
+ destination_place: locations[last_stop.location_id].place,
68
+ route: vendor.routes.find(trip_form.route_id),
69
+ departure_time: first_stop.departure_time,
70
+ duration: trip_form.total_duration_seconds,
71
+ allow_booking: trip_form.allow_booking,
72
+ allow_seat_selection: trip_form.allow_seat_selection,
73
+ route_type: trip_form.route_type || vendor.routes.find(trip_form.route_id)&.route_type
74
+ )
75
+
76
+ create_trip_stops(trip, trip_form, locations, stops)
77
+ trip
78
+ end
79
+
80
+ # Creates individual trip stops based on the trip form stops.
81
+ # Each stop includes arrival/departure times and boarding/drop-off permissions.
82
+ def create_trip_stops(trip, trip_form, locations, stops)
83
+ previous_departure = nil
84
+
85
+ trip_form.trip_stops.each do |ts|
86
+ arrival_time = previous_departure || ts.departure_time
87
+
88
+ trip.trip_stops.create!(
89
+ stop_place: stops[ts.stop_id].place,
90
+ location_place: locations[ts.location_id].place,
91
+ allow_boarding: ts.allow_boarding?,
92
+ allow_drop_off: ts.allow_drop_off,
93
+ arrival_time: arrival_time,
94
+ departure_time: ts.departure_time
95
+ )
96
+
97
+ previous_departure = ts.departure_time
98
+ end
99
+ end
100
+
101
+ # Sets up the service calendar for the trip, defining operational dates, weekdays, and exceptions.
102
+ def setup_calendar!(product, calendar_form)
103
+ res = SpreeCmCommissioner::Trips::ServiceCalendars::CreateOrUpdate.call(
104
+ calendarable: product,
105
+ name: calendar_form.name,
106
+ start_date: calendar_form.start_date,
107
+ end_date: calendar_form.end_date,
108
+ weekdays: calendar_form.weekdays,
109
+ exception_rules: calendar_form.exception_rules || []
110
+ )
111
+
112
+ raise res.error unless res.success?
113
+
114
+ res.value[:calendar]
115
+ end
116
+
117
+ # Triggers asynchronous generation of permanent inventory items for the variant.
118
+ def generate_inventory!(variant)
119
+ SpreeCmCommissioner::Stock::PermanentInventoryItemsGeneratorJob.perform_later(variant_ids: [variant[:id]])
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,54 @@
1
+ module SpreeCmCommissioner
2
+ module Trips
3
+ module ServiceCalendars
4
+ # Service to create or update a service calendar for a calendarable entity.
5
+ class CreateOrUpdate
6
+ prepend ::Spree::ServiceModule::Base
7
+
8
+ def call(calendarable:, name:, start_date:, end_date:, weekdays:, exception_rules: []) # rubocop:disable Metrics/ParameterLists
9
+ return failure(nil, 'calendarable must be present') if calendarable.blank?
10
+ return failure(nil, 'name must be present') if name.blank?
11
+ return failure(nil, 'start_date must be present') if start_date.blank?
12
+ return failure(nil, 'end_date must be present') if end_date.blank?
13
+ return failure(nil, 'weekdays must be present') if weekdays.blank?
14
+
15
+ ApplicationRecord.transaction do
16
+ calendar = create_or_update_calendar(calendarable, name, start_date, end_date, weekdays, exception_rules)
17
+ success(calendar: calendar)
18
+ end
19
+ rescue StandardError => e
20
+ CmAppLogger.error(
21
+ label: 'SpreeCmCommissioner::Trips::ServiceCalendars::CreateOrUpdate#call',
22
+ data: {
23
+ error_class: e.class.name,
24
+ error_message: e.message
25
+ }
26
+ )
27
+ failure(nil, 'Failed to create service calendar')
28
+ end
29
+
30
+ private
31
+
32
+ # Create or update calendar
33
+ def create_or_update_calendar(calendarable, name, start_date, end_date, weekdays, exception_rules) # rubocop:disable Metrics/ParameterLists
34
+ calendar = calendarable.service_calendar || calendarable.build_service_calendar
35
+
36
+ calendar.assign_attributes(
37
+ name: name,
38
+ start_date: start_date,
39
+ end_date: end_date,
40
+ exception_rules: exception_rules
41
+ )
42
+
43
+ # Set weekday attributes
44
+ weekdays.each do |day, enabled|
45
+ calendar.send("#{day}=", enabled)
46
+ end
47
+
48
+ calendar.save!
49
+ calendar
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -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
+ end
20
+ rescue StandardError => e
21
+ CmAppLogger.error(
22
+ label: 'SpreeCmCommissioner::Trips::UpdateSingleLeg#call',
23
+ data: {
24
+ error_class: e.class.name,
25
+ error_message: e.message
26
+ }
27
+ )
28
+ failure(nil, 'Failed to update trip')
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
+
56
+ updates[:allow_boarding] = ts_form.allow_boarding? unless ts_form.allow_boarding.nil?
57
+ updates[:allow_drop_off] = ts_form.allow_drop_off unless ts_form.allow_drop_off.nil?
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
@@ -0,0 +1,45 @@
1
+ <%= turbo_frame_tag "inventory_item" do %>
2
+ <div>
3
+ <div class="p-3 mb-3 overflow-hidden border border-2 rounded bg-primary bg-opacity-10 border-primary position-relative">
4
+ <div class="text-white position-absolute top-50 translate-middle-y" style="right: -20px; opacity: 0.1;">
5
+ <%= svg_icon name: "money.svg", width: '120', height: '120' %>
6
+ </div>
7
+ <h5 class="mb-2 text-white modal-title position-relative">
8
+ <%= svg_icon name: "money.svg", width: '24', height: '24' %>
9
+ <span class="ml-2">Bulk Update Prices</span>
10
+ </h5>
11
+ <p class="mb-0 text-white position-relative"><%= @inventory_items.count %> items selected</p>
12
+ </div>
13
+
14
+ <%= form_with url: bulk_update_prices_admin_product_inventory_items_path(@product), method: :patch, local: true, html: { class: 'bulk-inventory-items-form' } do |form| %>
15
+ <%= form.hidden_field :inventory_ids, value: params[:inventory_ids] %>
16
+
17
+ <table class="table">
18
+ <tbody>
19
+ <% supported_currencies_for_all_stores.each do |currency| %>
20
+ <tr data-hook="bulk_prices_row">
21
+ <td><%= form.label "price_#{currency}", currency, class: 'form-label' %></td>
22
+ <td>
23
+ <%= form.hidden_field "prices[#{currency}][currency]", value: currency %>
24
+ <%= form.text_field "prices[#{currency}][price]",
25
+ placeholder: "Price",
26
+ class: 'form-control' %>
27
+ </td>
28
+ <td>
29
+ <%= form.text_field "prices[#{currency}][compare_at_price]",
30
+ placeholder: "Compare at price",
31
+ class: 'form-control' %>
32
+ </td>
33
+ </tr>
34
+ <% end %>
35
+ </tbody>
36
+ </table>
37
+
38
+ <div class="flex-row mt-2 d-flex justify-content-end">
39
+ <div class="form-actions" data-hook="buttons">
40
+ <%= button Spree.t('actions.save'), 'save.svg', 'submit', { class: 'btn-primary', data: { disable_with: "#{ Spree.t(:saving) }...", turbo_frame: "_top" } } %>
41
+ </div>
42
+ </div>
43
+ <% end %>
44
+ </div>
45
+ <% end %>
@@ -0,0 +1,36 @@
1
+ <%= turbo_frame_tag "inventory_item" do %>
2
+ <div>
3
+ <div class="p-3 mb-3 overflow-hidden border border-2 rounded bg-success bg-opacity-10 border-success position-relative">
4
+ <div class="text-white position-absolute top-50 translate-middle-y" style="right: -20px; opacity: 0.1;">
5
+ <%= svg_icon name: "box-seam.svg", width: '120', height: '120' %>
6
+ </div>
7
+ <h5 class="mb-2 text-white modal-title position-relative">
8
+ <%= svg_icon name: "box-seam.svg", width: '24', height: '24' %>
9
+ <span class="ml-2">Bulk Update Stocks</span>
10
+ </h5>
11
+ <p class="mb-0 text-white position-relative"><%= @inventory_items.count %> items selected</p>
12
+ </div>
13
+
14
+ <%= form_with url: bulk_update_stocks_admin_product_inventory_items_path(@product), method: :patch, local: true, html: { class: 'bulk-inventory-items-form' } do |form| %>
15
+ <%= form.hidden_field :inventory_ids, value: params[:inventory_ids] %>
16
+
17
+ <div class="form-group">
18
+ <%= form.label :quantity, "Available Quantity", class: 'form-label' %>
19
+ <%= form.number_field :quantity,
20
+ placeholder: "Enter quantity",
21
+ class: 'form-control',
22
+ required: true,
23
+ min: 0 %>
24
+ <small class="form-text text-muted">
25
+ This quantity will be set for all <%= @inventory_items.count %> selected items and synced to Redis.
26
+ </small>
27
+ </div>
28
+
29
+ <div class="flex-row mt-2 d-flex justify-content-end">
30
+ <div class="form-actions" data-hook="buttons">
31
+ <%= button Spree.t('actions.save'), 'save.svg', 'submit', { class: 'btn-success', data: { disable_with: "#{ Spree.t(:saving) }...", turbo_frame: "_top" } } %>
32
+ </div>
33
+ </div>
34
+ <% end %>
35
+ </div>
36
+ <% end %>