spree_cm_commissioner 2.5.14.pre.pre6 → 2.5.14
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/.env.example +4 -2
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/import_existing_orders_controller.rb +3 -1
- data/app/controllers/spree/admin/import_new_orders_controller.rb +3 -1
- data/app/controllers/spree/api/v2/operator/check_ins_controller.rb +15 -2
- data/app/models/spree_cm_commissioner/export.rb +7 -2
- data/app/models/spree_cm_commissioner/product_decorator.rb +5 -0
- data/app/models/spree_cm_commissioner/trip.rb +23 -3
- data/app/queries/spree_cm_commissioner/trip_query.rb +3 -2
- data/app/services/spree_cm_commissioner/check_ins/create_bulk.rb +14 -3
- data/app/services/spree_cm_commissioner/organizer/export_guest_csv_service.rb +36 -0
- data/app/services/spree_cm_commissioner/trips/create_open_dated_trip.rb +99 -0
- data/app/services/spree_cm_commissioner/trips/update_open_dated_trip.rb +67 -0
- data/app/services/spree_cm_commissioner/trips/variants/create.rb +12 -6
- data/config/locales/en.yml +1 -1
- data/db/migrate/20260128090001_add_is_open_dated_to_cm_trips.rb +6 -0
- data/db/migrate/20260207100000_add_index_created_at_to_cm_imports.rb +5 -0
- data/db/migrate/20260217162827_add_index_to_cm_guests_on_event_id_and_bib_prefix_and_bib_number.rb +8 -0
- data/lib/spree_cm_commissioner/transit/trip_form.rb +13 -0
- data/lib/spree_cm_commissioner/trip_query_result.rb +9 -0
- data/lib/spree_cm_commissioner/trip_result.rb +7 -2
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +9 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 574ed1a0c32a052dce78787ecfa40603a68f033ff8a113f3ae7a91e415656fe6
|
|
4
|
+
data.tar.gz: 3640a360d2df37c723529f091136e1491c2a0cf61515993d6ebd502e01b3996f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 893ac42ed0c4c306260bd8b578fed2619d7ee65daf13d009228f81ae2047a98e164983b89b0345344d384cffe1f88989c20b717685e40a38c5abf8a159609e77
|
|
7
|
+
data.tar.gz: 5d145d664bbba939d59f2a5797ff96494993c911b2dba2159a23c8accafce6bc0e9719b7e62697f1bebd2f2ef7151544031a47f9993eb8311c745ef84c76283d
|
data/.env.example
CHANGED
|
@@ -44,7 +44,9 @@ CACHE_DEFAULT_MAX_AGE=300 # Default for unlisted controllers - 5 minutes
|
|
|
44
44
|
# Batch size for processing maintenance tasks in app/jobs/spree_cm_commissioner/maintenance_tasks/orchestrate_job.rb
|
|
45
45
|
MAINTENANCE_TASKS_BATCH_SIZE=100
|
|
46
46
|
|
|
47
|
+
# Export related:
|
|
48
|
+
OPERATOR_GUEST_JSON_GZIP_CACHE_HOURS=24
|
|
49
|
+
EXPORT_PRESIGNED_URL_EXPIRATION_MINUTES=15
|
|
50
|
+
|
|
47
51
|
# import order batch size
|
|
48
52
|
IMPORT_ORDERS_BATCH_SIZE=50
|
|
49
|
-
# How long event crews/organizers can regenerate guest exports. See: app/services/spree_cm_commissioner/operator_guest_json_gzips/create.rb
|
|
50
|
-
OPERATOR_GUEST_JSON_GZIP_CACHE_HOURS=24
|
data/Gemfile.lock
CHANGED
|
@@ -3,7 +3,9 @@ module Spree
|
|
|
3
3
|
class ImportExistingOrdersController < BaseImportOrdersController
|
|
4
4
|
# override
|
|
5
5
|
def collection
|
|
6
|
-
@collection ||= model_class.existing_order
|
|
6
|
+
@collection ||= model_class.existing_order
|
|
7
|
+
.order(created_at: :desc)
|
|
8
|
+
.page(params[:page])
|
|
7
9
|
.per(params[:per_page])
|
|
8
10
|
end
|
|
9
11
|
|
|
@@ -3,7 +3,9 @@ module Spree
|
|
|
3
3
|
class ImportNewOrdersController < BaseImportOrdersController
|
|
4
4
|
# override
|
|
5
5
|
def collection
|
|
6
|
-
@collection ||= model_class.new_order
|
|
6
|
+
@collection ||= model_class.new_order
|
|
7
|
+
.order(created_at: :desc)
|
|
8
|
+
.page(params[:page])
|
|
7
9
|
.per(params[:per_page])
|
|
8
10
|
end
|
|
9
11
|
|
|
@@ -5,10 +5,23 @@ module Spree
|
|
|
5
5
|
class CheckInsController < ::Spree::Api::V2::ResourceController
|
|
6
6
|
before_action :require_spree_current_user, only: %i[index create]
|
|
7
7
|
|
|
8
|
+
# Check-in history data requires fresh/recent data for operator dashboards
|
|
9
|
+
# Short cache duration ensures operators see near real-time check-in activity
|
|
10
|
+
CACHE_EXPIRES_IN = 1.minute
|
|
11
|
+
|
|
8
12
|
def collection
|
|
9
|
-
@collection
|
|
13
|
+
@collection ||= SpreeCmCommissioner::CheckIn
|
|
14
|
+
.where(checkinable_type: 'Spree::Taxon', checkinable_id: params[:taxon_id])
|
|
15
|
+
.page(params[:page])
|
|
16
|
+
.per(params[:per_page])
|
|
17
|
+
end
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
# override
|
|
20
|
+
def collection_cache_opts
|
|
21
|
+
{
|
|
22
|
+
namespace: Spree::Api::Config[:api_v2_collection_cache_namespace],
|
|
23
|
+
expires_in: CACHE_EXPIRES_IN
|
|
24
|
+
}
|
|
12
25
|
end
|
|
13
26
|
|
|
14
27
|
def create
|
|
@@ -35,18 +35,23 @@ module SpreeCmCommissioner
|
|
|
35
35
|
exported_file.filename.to_s
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def exported_file_url
|
|
38
|
+
def exported_file_url
|
|
39
39
|
# Controllers should set ActiveStorage::Current.url_options in a before_action
|
|
40
40
|
# See GuestJsonGzipsController for an example
|
|
41
41
|
return nil if ActiveStorage::Current.url_options.nil?
|
|
42
42
|
return nil if exported_file.blank?
|
|
43
43
|
|
|
44
44
|
exported_file.url(
|
|
45
|
-
expires_in:
|
|
45
|
+
expires_in: presigned_url_expires_in,
|
|
46
46
|
disposition: 'attachment'
|
|
47
47
|
)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# subclasses can override this to set different expiration time base on use case.
|
|
51
|
+
def presigned_url_expires_in
|
|
52
|
+
ENV.fetch('EXPORT_PRESIGNED_URL_EXPIRATION_MINUTES', '15').to_i.minutes
|
|
53
|
+
end
|
|
54
|
+
|
|
50
55
|
def enqueue_export
|
|
51
56
|
SpreeCmCommissioner::ExportJob.perform_later(export_id: id)
|
|
52
57
|
end
|
|
@@ -9,6 +9,9 @@ module SpreeCmCommissioner
|
|
|
9
9
|
base.include SpreeCmCommissioner::ServiceType
|
|
10
10
|
base.include SpreeCmCommissioner::ServiceRecommendations
|
|
11
11
|
base.include SpreeCmCommissioner::Integrations::IntegrationMappable
|
|
12
|
+
base.include SpreeCmCommissioner::StoreMetadata
|
|
13
|
+
|
|
14
|
+
base.delegate :is_open_dated, :is_open_dated?, to: :trip, allow_nil: true
|
|
12
15
|
|
|
13
16
|
base.has_many :variant_kind_option_types, -> { where(kind: :variant).order(:position) },
|
|
14
17
|
through: :product_option_types, source: :option_type
|
|
@@ -79,6 +82,8 @@ module SpreeCmCommissioner
|
|
|
79
82
|
|
|
80
83
|
base.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status vendor_id short_name]
|
|
81
84
|
|
|
85
|
+
base.store_public_metadata :open_dated_validity_days, :integer, default: 90
|
|
86
|
+
|
|
82
87
|
base.after_update :update_variants_vendor_id, if: :saved_change_to_vendor_id?
|
|
83
88
|
base.after_update :sync_event_id_to_children, if: :saved_change_to_event_id?
|
|
84
89
|
|
|
@@ -22,7 +22,13 @@ module SpreeCmCommissioner
|
|
|
22
22
|
|
|
23
23
|
belongs_to :service_origin, class_name: 'Spree::Taxon', optional: true
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
has_one :open_dated_relation, lambda {
|
|
26
|
+
where(relation_type: :open_dated_pair)
|
|
27
|
+
}, class_name: 'SpreeCmCommissioner::ProductRelation',
|
|
28
|
+
foreign_key: :related_product_id, primary_key: :product_id, dependent: :destroy
|
|
29
|
+
has_one :open_dated_product, through: :open_dated_relation, source: :product
|
|
30
|
+
|
|
31
|
+
belongs_to :vehicle_type, class_name: 'SpreeCmCommissioner::VehicleType', optional: true
|
|
26
32
|
belongs_to :vehicle, class_name: 'SpreeCmCommissioner::Vehicle', optional: true
|
|
27
33
|
has_one :seat_layout, as: :layoutable, class_name: 'SpreeCmCommissioner::SeatLayout', dependent: :destroy
|
|
28
34
|
|
|
@@ -41,8 +47,9 @@ module SpreeCmCommissioner
|
|
|
41
47
|
has_many :inventory_items, through: :variants
|
|
42
48
|
has_many :blocks, through: :variants
|
|
43
49
|
|
|
44
|
-
validates :departure_time, presence: true
|
|
45
|
-
validates :
|
|
50
|
+
validates :departure_time, presence: true, unless: :open_dated?
|
|
51
|
+
validates :vehicle_type, presence: true, unless: :open_dated?
|
|
52
|
+
validates :duration, numericality: { greater_than: 0 }, unless: :open_dated?
|
|
46
53
|
validate :origin_and_destination_cannot_be_the_same
|
|
47
54
|
|
|
48
55
|
accepts_nested_attributes_for :trip_stops, :product, :seat_layout, allow_destroy: true
|
|
@@ -77,6 +84,19 @@ module SpreeCmCommissioner
|
|
|
77
84
|
errors.add(:base, 'Origin and destination cannot be the same')
|
|
78
85
|
end
|
|
79
86
|
|
|
87
|
+
def open_dated?
|
|
88
|
+
is_open_dated == true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Find the open dated version of this trip's product
|
|
92
|
+
def open_dated_pair
|
|
93
|
+
open_dated_product
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def display_name
|
|
97
|
+
product&.name
|
|
98
|
+
end
|
|
99
|
+
|
|
80
100
|
private
|
|
81
101
|
|
|
82
102
|
def duplicate_seat_layout_from_vehicle
|
|
@@ -71,7 +71,7 @@ module SpreeCmCommissioner
|
|
|
71
71
|
prices.amount AS amount, prices.compare_at_amount AS compare_at_amount,
|
|
72
72
|
prices.currency AS currency
|
|
73
73
|
SQL
|
|
74
|
-
.includes(vendor: :logo, vehicle_type: :option_values)
|
|
74
|
+
.includes(vendor: :logo, vehicle_type: :option_values, route: {}, open_dated_product: %i[trip variants])
|
|
75
75
|
|
|
76
76
|
scope = scope.joins(:vendor) if tenant_id.present?
|
|
77
77
|
|
|
@@ -129,7 +129,8 @@ module SpreeCmCommissioner
|
|
|
129
129
|
drop_off: build_drop_off_info(trip),
|
|
130
130
|
quantity_available: trip&.quantity_available,
|
|
131
131
|
max_capacity: trip&.max_capacity,
|
|
132
|
-
amenities: (trip.vehicle_type&.option_values || [])
|
|
132
|
+
amenities: (trip.vehicle_type&.option_values || []),
|
|
133
|
+
open_dated_product: trip&.open_dated_product
|
|
133
134
|
}
|
|
134
135
|
SpreeCmCommissioner::TripResult.new(trip_result_options)
|
|
135
136
|
end
|
|
@@ -8,12 +8,23 @@ module SpreeCmCommissioner
|
|
|
8
8
|
|
|
9
9
|
confirmed_at ||= Time.current
|
|
10
10
|
guest_ids = check_ins_attributes.pluck(:guest_id)
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
indexed_guests = SpreeCmCommissioner::Guest
|
|
13
|
+
.includes(:event, line_item: :order)
|
|
14
|
+
.where(id: guest_ids)
|
|
15
|
+
.index_by(&:id)
|
|
12
16
|
|
|
13
17
|
check_ins = ActiveRecord::Base.transaction do
|
|
14
18
|
check_ins_attributes.map do |attrs|
|
|
15
19
|
guest = indexed_guests[attrs[:guest_id].to_i]
|
|
16
|
-
raise ActiveRecord::RecordNotFound, "Couldn't find Guest
|
|
20
|
+
raise ActiveRecord::RecordNotFound, "Couldn't find Guest to check in" if guest.blank?
|
|
21
|
+
|
|
22
|
+
order = guest.line_item.order
|
|
23
|
+
if order.canceled?
|
|
24
|
+
error = ActiveRecord::RecordInvalid.new(guest)
|
|
25
|
+
error.record.errors.add(:base, 'Cannot check in Guest from cancelled order')
|
|
26
|
+
raise error
|
|
27
|
+
end
|
|
17
28
|
|
|
18
29
|
check_in = check_in!(guest, attrs, check_in_by, confirmed_at)
|
|
19
30
|
update_guest!(guest, attrs[:guest_attributes]) if attrs[:guest_attributes].present?
|
|
@@ -25,7 +36,7 @@ module SpreeCmCommissioner
|
|
|
25
36
|
success(check_ins: check_ins)
|
|
26
37
|
rescue ActiveRecord::RecordInvalid => e
|
|
27
38
|
failure(:invalid_record, e.record.errors.full_messages.join(', '))
|
|
28
|
-
rescue ActiveRecord::RecordNotFound
|
|
39
|
+
rescue ActiveRecord::RecordNotFound
|
|
29
40
|
failure(:record_not_found, e.message)
|
|
30
41
|
end
|
|
31
42
|
|
|
@@ -29,6 +29,8 @@ module SpreeCmCommissioner
|
|
|
29
29
|
def fetch_value(guest, column)
|
|
30
30
|
if guest_field?(column)
|
|
31
31
|
fetch_guest_value(guest, column)
|
|
32
|
+
elsif dynamic_field?(column)
|
|
33
|
+
fetch_dynamic_field_value(guest, column)
|
|
32
34
|
else
|
|
33
35
|
fetch_option_value(guest, column)
|
|
34
36
|
end
|
|
@@ -38,6 +40,39 @@ module SpreeCmCommissioner
|
|
|
38
40
|
SpreeCmCommissioner::KycBitwise::ORDERED_BIT_FIELDS.include?(column.to_sym)
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
def dynamic_field?(column)
|
|
44
|
+
return false unless dynamic_fields_exist?
|
|
45
|
+
|
|
46
|
+
dynamic_field_labels.include?(column)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def dynamic_field_labels
|
|
50
|
+
return [] unless dynamic_fields_exist?
|
|
51
|
+
|
|
52
|
+
@dynamic_field_labels ||= load_dynamic_field_labels
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def dynamic_fields_exist?
|
|
56
|
+
return @has_dynamic_fields unless @has_dynamic_fields.nil?
|
|
57
|
+
|
|
58
|
+
@has_dynamic_fields = event.event_products.joins(:product_dynamic_fields).exists?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def load_dynamic_field_labels
|
|
62
|
+
event.event_products.joins(product_dynamic_fields: :dynamic_field).distinct.pluck('label')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def fetch_dynamic_field_value(guest, column)
|
|
66
|
+
guest_dynamic_field = guest.guest_dynamic_fields.find { |gdf| gdf.dynamic_field&.label == column }
|
|
67
|
+
return '' unless guest_dynamic_field
|
|
68
|
+
|
|
69
|
+
if guest_dynamic_field.dynamic_field_option.present?
|
|
70
|
+
guest_dynamic_field.dynamic_field_option.value
|
|
71
|
+
else
|
|
72
|
+
guest_dynamic_field.value || ''
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
41
76
|
def fetch_guest_value(guest, column)
|
|
42
77
|
column_mappings(guest)[column] || ''
|
|
43
78
|
end
|
|
@@ -82,6 +117,7 @@ module SpreeCmCommissioner
|
|
|
82
117
|
:occupation,
|
|
83
118
|
:nationality,
|
|
84
119
|
:id_card,
|
|
120
|
+
guest_dynamic_fields: %i[dynamic_field dynamic_field_option],
|
|
85
121
|
line_item: { variant: { option_values: :option_type } }
|
|
86
122
|
).complete
|
|
87
123
|
scope = scope.where('cm_guests.created_at >= ?', formatted_date_time(filters[:from_date])) if filters[:from_date].present?
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Trips
|
|
3
|
+
# Service class responsible for creating Open Dated trips in the booking system.
|
|
4
|
+
# Open Dated trips are flexible, date-agnostic products that can be redeemed on any
|
|
5
|
+
# scheduled trip within the same route. They don't have fixed departure times or calendars.
|
|
6
|
+
#
|
|
7
|
+
# Key characteristics of Open Dated trips:
|
|
8
|
+
# - is_open_dated: true
|
|
9
|
+
# - departure_time: nil (no fixed schedule)
|
|
10
|
+
# - No service calendar (not tied to specific dates)
|
|
11
|
+
# - Special "Open Dated" variant option
|
|
12
|
+
# - Can be linked to multiple fixed-date trips via ProductRelations (open_dated_pair)
|
|
13
|
+
# - No specific vehicle type (can be used on any vehicle in the route)
|
|
14
|
+
#
|
|
15
|
+
# Required trip_form attributes for Open Dated trips:
|
|
16
|
+
# - route_id: The route this open dated trip is for
|
|
17
|
+
# - price: Ticket price
|
|
18
|
+
# - is_open_dated: Must be true
|
|
19
|
+
class CreateOpenDatedTrip
|
|
20
|
+
prepend Spree::ServiceModule::Base
|
|
21
|
+
include Transit::TripHelper
|
|
22
|
+
|
|
23
|
+
# Main service method that validates the trip form and orchestrates OT creation.
|
|
24
|
+
# Creates variant, trip, and inventory within a database transaction.
|
|
25
|
+
# Note: Open dated trips do NOT have calendars since they're not date-specific.
|
|
26
|
+
#
|
|
27
|
+
# @param vendor [Spree::Vendor] The vendor creating the open dated trip
|
|
28
|
+
# @param trip_form [OpenStruct/Object] Form object with: route_id, price, is_open_dated
|
|
29
|
+
# @return [Spree::ServiceModule::Result] Success with trip/variant or failure with error
|
|
30
|
+
def call(vendor:, trip_form:)
|
|
31
|
+
return failure(nil, trip_form.errors.full_messages.to_sentence) unless trip_form.valid_open_dated_data?
|
|
32
|
+
|
|
33
|
+
ApplicationRecord.transaction do
|
|
34
|
+
route = vendor.routes.find(trip_form.route_id)
|
|
35
|
+
variant = create_variant!(vendor, trip_form)
|
|
36
|
+
trip = create_trip!(vendor, trip_form, variant, route)
|
|
37
|
+
generate_inventory!(variant)
|
|
38
|
+
|
|
39
|
+
success(trip: trip, variant: variant, route: route)
|
|
40
|
+
end
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
CmAppLogger.error(
|
|
43
|
+
label: 'SpreeCmCommissioner::Trips::CreateOpenDatedTrip#call',
|
|
44
|
+
data: {
|
|
45
|
+
error_class: e.class.name,
|
|
46
|
+
error_message: e.message,
|
|
47
|
+
vendor_id: vendor&.id,
|
|
48
|
+
route_id: trip_form&.route_id
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
failure(nil, e.message)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Creates a product variant for the open dated trip using shared Variants::Create
|
|
57
|
+
# with "Open Dated" option value and fixed capacity of 100.
|
|
58
|
+
def create_variant!(vendor, trip_form)
|
|
59
|
+
result = SpreeCmCommissioner::Trips::Variants::Create.call(
|
|
60
|
+
vendor: vendor,
|
|
61
|
+
trip_form: trip_form,
|
|
62
|
+
price: trip_form.price || 0.0,
|
|
63
|
+
capacity: 100,
|
|
64
|
+
option_value_name: 'open dated'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
raise StandardError, result.error unless result.success?
|
|
68
|
+
|
|
69
|
+
result.value[:variant]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Creates the Open Dated trip record.
|
|
73
|
+
# Key difference from regular trips: departure_time is explicitly set to nil
|
|
74
|
+
# and is_open_dated is true, making this a flexible, date-agnostic product.
|
|
75
|
+
#
|
|
76
|
+
# Gets origin and destination from the Route model instead of trip_stops.
|
|
77
|
+
|
|
78
|
+
def create_trip!(vendor, trip_form, variant, route)
|
|
79
|
+
vendor.trips.create!(
|
|
80
|
+
product: variant.product,
|
|
81
|
+
origin_place_id: route.origin_place_id,
|
|
82
|
+
destination_place_id: route.destination_place_id,
|
|
83
|
+
route: route,
|
|
84
|
+
departure_time: nil, # Open dated trips have no fixed departure time
|
|
85
|
+
duration: 0, # Open dated trips have no fixed duration
|
|
86
|
+
allow_booking: trip_form.allow_booking.nil? ? true : trip_form.allow_booking,
|
|
87
|
+
allow_seat_selection: false, # Open dated trips don't allow seat selection
|
|
88
|
+
is_open_dated: true, # This is the key flag for open dated trips
|
|
89
|
+
route_type: trip_form.route_type || route.route_type
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Triggers asynchronous generation of permanent inventory items for the variant.
|
|
94
|
+
def generate_inventory!(variant)
|
|
95
|
+
SpreeCmCommissioner::Stock::PermanentInventoryItemsGeneratorJob.perform_later(variant_ids: [variant[:id]])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Trips
|
|
3
|
+
# Service class responsible for updating an existing Open Dated trip.
|
|
4
|
+
# Handles updates to trip, product, and variant only.
|
|
5
|
+
# Ignored trip stops and service calendar since Open Dated trips don't have them.
|
|
6
|
+
class UpdateOpenDatedTrip
|
|
7
|
+
prepend Spree::ServiceModule::Base
|
|
8
|
+
|
|
9
|
+
# Main service method that orchestrates trip updates within a database transaction.
|
|
10
|
+
# Updates trip/product and variant if price provided.
|
|
11
|
+
# Returns success with updated objects or failure with error message.
|
|
12
|
+
def call(trip:, trip_form:)
|
|
13
|
+
return failure(nil, 'Trip is not open dated') unless trip.open_dated?
|
|
14
|
+
|
|
15
|
+
ApplicationRecord.transaction do
|
|
16
|
+
update_trip_and_product(trip, trip_form)
|
|
17
|
+
update_variant(trip, trip_form) if trip_form.price.present?
|
|
18
|
+
|
|
19
|
+
success(trip: trip, variant: trip.product.variants.first)
|
|
20
|
+
end
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
CmAppLogger.error(
|
|
23
|
+
label: 'SpreeCmCommissioner::Trips::UpdateOpenDatedTrip#call',
|
|
24
|
+
data: {
|
|
25
|
+
error_class: e.class.name,
|
|
26
|
+
error_message: e.message,
|
|
27
|
+
trip_id: trip&.id
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
failure(nil, e.message)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Updates the trip record and associated product with attributes from the form.
|
|
36
|
+
def update_trip_and_product(trip, trip_form)
|
|
37
|
+
trip.update!(trip_attrs(trip_form))
|
|
38
|
+
trip.product.update!(product_attrs(trip_form))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Updates the variant's price if a new price is provided in the form.
|
|
42
|
+
def update_variant(trip, trip_form)
|
|
43
|
+
variant = trip.product.variants.where.not(id: trip.product.master.id).first
|
|
44
|
+
return unless variant
|
|
45
|
+
|
|
46
|
+
variant.update!(price: trip_form.price)
|
|
47
|
+
trip.product.update!(price: trip_form.price)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def trip_attrs(form)
|
|
51
|
+
{
|
|
52
|
+
allow_booking: form.allow_booking,
|
|
53
|
+
allow_seat_selection: false, # Always false for Open Dated trips
|
|
54
|
+
is_open_dated: true # Always true for Open Dated trips
|
|
55
|
+
}.compact
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def product_attrs(form)
|
|
59
|
+
{
|
|
60
|
+
name: form.name,
|
|
61
|
+
short_name: form.short_name,
|
|
62
|
+
open_dated_validity_days: form.open_dated_validity_days
|
|
63
|
+
}.compact
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -8,7 +8,9 @@ module SpreeCmCommissioner
|
|
|
8
8
|
|
|
9
9
|
# Main method that validates inputs and creates product, options, variant, and stock in transaction.
|
|
10
10
|
# Returns success with product and variant or failure with error message.
|
|
11
|
-
|
|
11
|
+
# @param option_value_name [String, nil] Seat-type option value name (e.g. 'open dated').
|
|
12
|
+
# Defaults to 'Normal' when not passed. Pass explicitly to create variants with custom seat types.
|
|
13
|
+
def call(vendor:, trip_form:, price:, capacity:, option_value_name: nil)
|
|
12
14
|
return failure(nil, 'vendor must be present') if vendor.blank?
|
|
13
15
|
return failure(nil, 'trip_form must be present') if trip_form.blank?
|
|
14
16
|
return failure(nil, 'price must be present') if price.blank?
|
|
@@ -16,9 +18,11 @@ module SpreeCmCommissioner
|
|
|
16
18
|
return failure(nil, 'capacity must be greater than 0') if capacity <= 0
|
|
17
19
|
return failure(nil, 'name must be present') if trip_form.name.blank?
|
|
18
20
|
|
|
21
|
+
option_value_name = option_value_name.presence || 'Normal'
|
|
22
|
+
|
|
19
23
|
ApplicationRecord.transaction do
|
|
20
24
|
product = create_product!(vendor, trip_form)
|
|
21
|
-
option_values = setup_option_values!(product)
|
|
25
|
+
option_values = setup_option_values!(product, option_value_name)
|
|
22
26
|
variant = create_variant!(product, price, option_values)
|
|
23
27
|
|
|
24
28
|
create_stock_item!(variant, capacity)
|
|
@@ -51,7 +55,8 @@ module SpreeCmCommissioner
|
|
|
51
55
|
price: trip_form.price,
|
|
52
56
|
shipping_category: Spree::ShippingCategory.find_or_create_by!(name: 'Default'),
|
|
53
57
|
option_types: [Spree::OptionType.find_by(name: 'seat-type')].compact,
|
|
54
|
-
stores: [Spree::Store.default].compact
|
|
58
|
+
stores: [Spree::Store.default].compact,
|
|
59
|
+
open_dated_validity_days: trip_form.open_dated_validity_days
|
|
55
60
|
)
|
|
56
61
|
end
|
|
57
62
|
|
|
@@ -60,9 +65,10 @@ module SpreeCmCommissioner
|
|
|
60
65
|
vendor.branches.where(id: trip_form.trip_stops.map(&:stop_id)).index_by(&:id)
|
|
61
66
|
end
|
|
62
67
|
|
|
63
|
-
# Sets up or finds the seat-type option type and creates
|
|
68
|
+
# Sets up or finds the seat-type option type and finds/creates the option value by name.
|
|
64
69
|
# Associates the option type with the product if not already present.
|
|
65
|
-
|
|
70
|
+
# @param option_value_name [String] e.g. 'Normal' or 'open dated'; defaults to 'Normal' when not passed to call.
|
|
71
|
+
def setup_option_values!(product, option_value_name = 'Normal')
|
|
66
72
|
option_type = Spree::OptionType.find_or_create_by!(
|
|
67
73
|
name: 'seat-type',
|
|
68
74
|
presentation: 'Seat Type'
|
|
@@ -70,7 +76,7 @@ module SpreeCmCommissioner
|
|
|
70
76
|
|
|
71
77
|
product.option_types << option_type unless product.option_types.include?(option_type)
|
|
72
78
|
|
|
73
|
-
option_value = option_type.option_values.find_or_create_by_name!(option_type,
|
|
79
|
+
option_value = option_type.option_values.find_or_create_by_name!(option_type, option_value_name)
|
|
74
80
|
|
|
75
81
|
[option_value]
|
|
76
82
|
end
|
data/config/locales/en.yml
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
class AddIsOpenDatedToCmTrips < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :cm_trips, :is_open_dated, :boolean, default: false unless column_exists?(:cm_trips, :is_open_dated)
|
|
4
|
+
add_index :cm_trips, :is_open_dated unless index_exists?(:cm_trips, :is_open_dated)
|
|
5
|
+
end
|
|
6
|
+
end
|
data/db/migrate/20260217162827_add_index_to_cm_guests_on_event_id_and_bib_prefix_and_bib_number.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class AddIndexToCmGuestsOnEventIdAndBibPrefixAndBibNumber < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
# Add composite index for optimal bib number lookup performance
|
|
4
|
+
add_index :cm_guests, [:event_id, :bib_prefix, :bib_number],
|
|
5
|
+
name: 'index_cm_guests_on_event_bib_prefix_number',
|
|
6
|
+
if_not_exists: true
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -9,7 +9,9 @@ module SpreeCmCommissioner::Transit
|
|
|
9
9
|
:total_vehicles,
|
|
10
10
|
:allow_booking,
|
|
11
11
|
:allow_seat_selection,
|
|
12
|
+
:is_open_dated,
|
|
12
13
|
:route_type,
|
|
14
|
+
:open_dated_validity_days,
|
|
13
15
|
:trip_stops, # TripStopForm[]
|
|
14
16
|
:service_calendar # monday, tuesday, wednesday, thursday, friday, saturday, sunday, start_date, end_date
|
|
15
17
|
|
|
@@ -65,6 +67,17 @@ module SpreeCmCommissioner::Transit
|
|
|
65
67
|
errors.empty?
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
def valid_open_dated_data?
|
|
71
|
+
errors.clear
|
|
72
|
+
|
|
73
|
+
errors.add(:route_id, 'must be present') if route_id.blank?
|
|
74
|
+
errors.add(:price, 'must be present') if price.blank?
|
|
75
|
+
errors.add(:is_open_dated, 'must be true') unless is_open_dated
|
|
76
|
+
errors.add(:open_dated_validity_days, 'must be present') if open_dated_validity_days.blank?
|
|
77
|
+
|
|
78
|
+
errors.empty?
|
|
79
|
+
end
|
|
80
|
+
|
|
68
81
|
# Groups trip_stops into legs by location changes
|
|
69
82
|
# Separates legs when location_id changes (at branch stops)
|
|
70
83
|
# Break stops (stop_type = :stop) are included but don't trigger separation
|
|
@@ -31,6 +31,11 @@ module SpreeCmCommissioner
|
|
|
31
31
|
@trips.map(&:price).compact.sum
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def total_open_return_price
|
|
35
|
+
open_return_price = open_dated_product&.variants&.first&.price.to_d
|
|
36
|
+
total_price + open_return_price
|
|
37
|
+
end
|
|
38
|
+
|
|
34
39
|
def total_duration
|
|
35
40
|
@trips.map(&:duration).compact.sum
|
|
36
41
|
end
|
|
@@ -60,5 +65,9 @@ module SpreeCmCommissioner
|
|
|
60
65
|
def vehicle_types
|
|
61
66
|
@trips.map(&:vehicle_type).uniq
|
|
62
67
|
end
|
|
68
|
+
|
|
69
|
+
def open_dated_product
|
|
70
|
+
@trips.first&.open_dated_product
|
|
71
|
+
end
|
|
63
72
|
end
|
|
64
73
|
end
|
|
@@ -4,7 +4,7 @@ module SpreeCmCommissioner
|
|
|
4
4
|
:departure_time, :duration, :route_type,
|
|
5
5
|
:vehicle_type_id, :vehicle_type, :vendor_id, :vendor,
|
|
6
6
|
:quantity_available, :max_capacity, :boarding, :drop_off,
|
|
7
|
-
:product_id, :amenities, :price, :compare_at_amount, :currency, :distance
|
|
7
|
+
:product_id, :amenities, :price, :compare_at_amount, :currency, :distance, :open_dated_product
|
|
8
8
|
|
|
9
9
|
def initialize(options = {})
|
|
10
10
|
options.each do |key, value|
|
|
@@ -51,11 +51,16 @@ module SpreeCmCommissioner
|
|
|
51
51
|
price,
|
|
52
52
|
quantity_available,
|
|
53
53
|
max_capacity,
|
|
54
|
-
departure_time&.to_i
|
|
54
|
+
departure_time&.to_i,
|
|
55
|
+
open_dated_product&.id
|
|
55
56
|
].compact.join('-')
|
|
56
57
|
|
|
57
58
|
version_hash = Digest::MD5.hexdigest(version_parts)
|
|
58
59
|
"trip_results/#{id}-#{version_hash}"
|
|
59
60
|
end
|
|
61
|
+
|
|
62
|
+
def open_dated_pair_id
|
|
63
|
+
open_dated_product&.id
|
|
64
|
+
end
|
|
60
65
|
end
|
|
61
66
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_cm_commissioner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.5.14
|
|
4
|
+
version: 2.5.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: spree
|
|
@@ -2097,9 +2097,11 @@ files:
|
|
|
2097
2097
|
- app/services/spree_cm_commissioner/transit/legs_builder_service.rb
|
|
2098
2098
|
- app/services/spree_cm_commissioner/transit_order/create.rb
|
|
2099
2099
|
- app/services/spree_cm_commissioner/trips/clone.rb
|
|
2100
|
+
- app/services/spree_cm_commissioner/trips/create_open_dated_trip.rb
|
|
2100
2101
|
- app/services/spree_cm_commissioner/trips/create_single_leg.rb
|
|
2101
2102
|
- app/services/spree_cm_commissioner/trips/search.rb
|
|
2102
2103
|
- app/services/spree_cm_commissioner/trips/service_calendars/create_or_update.rb
|
|
2104
|
+
- app/services/spree_cm_commissioner/trips/update_open_dated_trip.rb
|
|
2103
2105
|
- app/services/spree_cm_commissioner/trips/update_single_leg.rb
|
|
2104
2106
|
- app/services/spree_cm_commissioner/trips/variants/create.rb
|
|
2105
2107
|
- app/services/spree_cm_commissioner/update_guest_service.rb
|
|
@@ -3020,6 +3022,7 @@ files:
|
|
|
3020
3022
|
- db/migrate/20260121024645_add_nationality_group_to_cm_guests.rb
|
|
3021
3023
|
- db/migrate/20260126110528_seed_user_initial_usernames.rb
|
|
3022
3024
|
- db/migrate/20260128043540_add_counter_cache_to_spree_users.rb
|
|
3025
|
+
- db/migrate/20260128090001_add_is_open_dated_to_cm_trips.rb
|
|
3023
3026
|
- db/migrate/20260128093305_add_exportable_to_cm_exports.rb
|
|
3024
3027
|
- db/migrate/20260128093313_add_metadata_to_cm_exports.rb
|
|
3025
3028
|
- db/migrate/20260129082338_remove_preferences_file_name_columns_from_cm_exports.rb
|
|
@@ -3030,6 +3033,8 @@ files:
|
|
|
3030
3033
|
- db/migrate/20260204100002_add_service_origin_to_cm_trips.rb
|
|
3031
3034
|
- db/migrate/20260204183545_add_term_accepted_at_to_spree_orders.rb
|
|
3032
3035
|
- db/migrate/20260207040752_clear_cm_exports.rb
|
|
3036
|
+
- db/migrate/20260207100000_add_index_created_at_to_cm_imports.rb
|
|
3037
|
+
- db/migrate/20260217162827_add_index_to_cm_guests_on_event_id_and_bib_prefix_and_bib_number.rb
|
|
3033
3038
|
- db/migrate/20260218100000_create_cm_maintenance_tasks.rb
|
|
3034
3039
|
- docker-compose.yml
|
|
3035
3040
|
- docs/api/scoped-access-token-endpoints.md
|
|
@@ -3215,9 +3220,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
3215
3220
|
version: '2.7'
|
|
3216
3221
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
3217
3222
|
requirements:
|
|
3218
|
-
- - "
|
|
3223
|
+
- - ">="
|
|
3219
3224
|
- !ruby/object:Gem::Version
|
|
3220
|
-
version:
|
|
3225
|
+
version: '0'
|
|
3221
3226
|
requirements:
|
|
3222
3227
|
- none
|
|
3223
3228
|
rubygems_version: 3.4.1
|