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.
- checksums.yaml +4 -4
- data/.github/workflows/test_and_build_gem.yml +2 -2
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/inventory_items_controller.rb +36 -56
- data/app/controllers/spree/admin/stock_managements_controller.rb +14 -3
- data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +2 -2
- data/app/controllers/spree/api/v2/storefront/transit/draft_orders_controller.rb +4 -4
- data/app/controllers/spree/api/v2/tenant/popular_route_places_controller.rb +60 -0
- data/app/controllers/spree/api/v2/tenant/routes_controller.rb +50 -0
- data/app/controllers/spree/api/v2/tenant/transit/draft_orders_controller.rb +46 -0
- data/app/controllers/spree/transit/trips_controller.rb +3 -3
- data/app/finders/spree_cm_commissioner/places/find_with_route.rb +12 -12
- data/app/finders/spree_cm_commissioner/route_metrics/find_popular.rb +44 -0
- data/app/finders/spree_cm_commissioner/routes/find.rb +94 -0
- data/app/finders/spree_cm_commissioner/routes/find_popular.rb +19 -35
- data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +11 -4
- data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +10 -1
- data/app/interactors/spree_cm_commissioner/trip_clone_creator.rb +4 -3
- data/app/interactors/spree_cm_commissioner/trip_stops_creator.rb +2 -2
- data/app/jobs/spree_cm_commissioner/route_metrics/decrease_trip_count_job.rb +10 -0
- data/app/jobs/spree_cm_commissioner/route_metrics/increase_fulfilled_order_count_job.rb +10 -0
- data/app/jobs/spree_cm_commissioner/route_metrics/increase_order_count_job.rb +10 -0
- data/app/jobs/spree_cm_commissioner/route_metrics/increase_trip_count_job.rb +10 -0
- data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +2 -2
- data/app/models/concerns/spree_cm_commissioner/route_order_countable.rb +2 -2
- data/app/models/spree_cm_commissioner/place.rb +5 -8
- data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/route.rb +45 -5
- data/app/models/spree_cm_commissioner/route_metric.rb +21 -0
- data/app/models/spree_cm_commissioner/route_photo.rb +12 -0
- data/app/models/spree_cm_commissioner/trip.rb +8 -33
- data/app/models/spree_cm_commissioner/trip_stop.rb +16 -2
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +3 -1
- data/app/queries/spree_cm_commissioner/trip_query.rb +2 -2
- data/app/serializers/spree/v2/tenant/transit_cart_serializer.rb +11 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/route_metric_serializer.rb +13 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/route_serializer.rb +2 -1
- data/app/serializers/spree_cm_commissioner/v2/storefront/transit_line_item_serializer.rb +17 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/trip_stop_serializer.rb +1 -1
- data/app/services/spree_cm_commissioner/route_metrics/decrease_trip_count.rb +31 -0
- data/app/services/spree_cm_commissioner/{routes/increment_fulfilled_order_count.rb → route_metrics/increase_fulfilled_order_count.rb} +3 -3
- data/app/services/spree_cm_commissioner/{routes/increment_order_count.rb → route_metrics/increase_order_count.rb} +3 -3
- data/app/services/spree_cm_commissioner/route_metrics/increase_trip_count.rb +31 -0
- data/app/services/spree_cm_commissioner/{routes/base_update_order_metrics.rb → route_metrics/update_route_metrics.rb} +11 -16
- data/app/services/spree_cm_commissioner/routes/create.rb +51 -0
- data/app/services/spree_cm_commissioner/routes/update.rb +25 -0
- data/app/{interactors/spree_cm_commissioner/transit/draft_order_creator.rb → services/spree_cm_commissioner/transit_order/create.rb} +13 -16
- data/app/services/spree_cm_commissioner/trips/create_single_leg.rb +123 -0
- data/app/services/spree_cm_commissioner/trips/service_calendars/create_or_update.rb +54 -0
- data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +88 -0
- data/app/services/spree_cm_commissioner/trips/variants/create.rb +103 -0
- data/app/views/spree/admin/inventory_items/prices.html.erb +45 -0
- data/app/views/spree/admin/inventory_items/stocks.html.erb +36 -0
- data/app/views/spree/admin/stock_managements/calendar.html.erb +105 -12
- data/app/views/spree/admin/stock_managements/index.html.erb +9 -8
- data/app/views/spree/transit/trip_stops/index.html.erb +4 -2
- data/app/views/spree_cm_commissioner/guest_mailer/send_ticket_to_guest.html.erb +0 -1
- data/config/initializers/spree_permitted_attributes.rb +11 -0
- data/config/routes.rb +12 -7
- data/db/migrate/20251224033103_migrate_cm_routes_to_cm_route_metrics.rb +17 -0
- data/db/migrate/20251224033910_migrate_cm_vendor_routes_to_cm_routes.rb +30 -0
- data/db/migrate/20260105072450_migrate_cm_trip_stops_to_support_trip_connection.rb +12 -0
- data/db/migrate/20260108101406_add_allow_booking_to_cm_trips.rb +5 -0
- data/lib/spree_cm_commissioner/test_helper/factories/route_factory.rb +7 -6
- data/lib/spree_cm_commissioner/test_helper/factories/route_metric_factory.rb +12 -0
- data/lib/spree_cm_commissioner/test_helper/factories/route_photo_factory.rb +5 -0
- data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +4 -1
- data/lib/spree_cm_commissioner/test_helper/factories/trip_stop_factory.rb +3 -1
- data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +2 -0
- data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +22 -0
- data/lib/spree_cm_commissioner/transit/route_stop.rb +61 -0
- data/lib/spree_cm_commissioner/transit/route_stop_collection.rb +175 -0
- data/lib/spree_cm_commissioner/transit/trip_form.rb +81 -0
- data/lib/spree_cm_commissioner/transit/trip_stop_form.rb +65 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +4 -0
- metadata +42 -21
- data/app/jobs/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_job.rb +0 -10
- data/app/jobs/spree_cm_commissioner/transit/route_order_count_incrementer_job.rb +0 -10
- data/app/jobs/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_job.rb +0 -13
- data/app/jobs/spree_cm_commissioner/transit/route_trip_count_decrementer_job.rb +0 -10
- data/app/jobs/spree_cm_commissioner/transit/route_trip_count_incrementer_job.rb +0 -10
- data/app/models/concerns/spree_cm_commissioner/route_trip_count_callbacks.rb +0 -48
- data/app/models/spree_cm_commissioner/trip_connection.rb +0 -36
- data/app/models/spree_cm_commissioner/vendor_route.rb +0 -9
- data/app/services/spree_cm_commissioner/routes/decrement_previous_trip_count.rb +0 -30
- data/app/services/spree_cm_commissioner/routes/decrement_trip_count.rb +0 -33
- data/app/services/spree_cm_commissioner/routes/increment_trip_count.rb +0 -33
- data/app/views/spree/admin/inventory_items/show.html.erb +0 -72
- 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
|
|
3
|
-
class
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
40
|
-
SpreeCmCommissioner::
|
|
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
|
-
#
|
|
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
|
|
16
|
-
class
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
23
|
+
order = create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user)
|
|
24
|
+
success(order: order)
|
|
28
25
|
rescue StandardError => e
|
|
29
|
-
|
|
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
|
-
|
|
116
|
-
|
|
112
|
+
@saved_guests ||= []
|
|
113
|
+
@saved_guests << SpreeCmCommissioner::SavedGuest.new if @saved_guests[index].blank?
|
|
117
114
|
|
|
118
|
-
guest.saved_guest =
|
|
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 %>
|