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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40530545badfcae7bcf0a9ea607b78503c757c18669fa49fc8b6cf206f6df984
4
- data.tar.gz: 279f9a33c706c9ab20628168a35283a1e4d2c5dd5013ff3cfc88bd29a0d99c9d
3
+ metadata.gz: 574ed1a0c32a052dce78787ecfa40603a68f033ff8a113f3ae7a91e415656fe6
4
+ data.tar.gz: 3640a360d2df37c723529f091136e1491c2a0cf61515993d6ebd502e01b3996f
5
5
  SHA512:
6
- metadata.gz: 556876458e952e2e910b5387515fcf1bff9a35bdfc041f66d9b6ad08e8bae795b12ffa572d4cf93a4bc012f6da9807d8f2e48e37ab6c489202636adf6ec8b66e
7
- data.tar.gz: 23f197a4d3344d26832aa8cf0421fedbcf19c2345d7647bd3c154b54029ef3e6bea633e1b98cc8184edfd516d1e0b6bb7bcfa565edad86ed2b01ea5b3cb5ddc5
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
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.5.14.pre.pre6)
37
+ spree_cm_commissioner (2.5.14)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -3,7 +3,9 @@ module Spree
3
3
  class ImportExistingOrdersController < BaseImportOrdersController
4
4
  # override
5
5
  def collection
6
- @collection ||= model_class.existing_order.page(params[:page])
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.page(params[:page])
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 = SpreeCmCommissioner::CheckIn.where(checkinable: params[:taxon_id])
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
- @collection = @collection.page(params[:page]).per(params[:per_page])
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(expires_in: 15.minutes)
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: 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
- belongs_to :vehicle_type, class_name: 'SpreeCmCommissioner::VehicleType', optional: false
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 :duration, numericality: { greater_than: 0 }
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
- indexed_guests = SpreeCmCommissioner::Guest.where(id: guest_ids).index_by(&:id)
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 with 'id'=#{attrs[:guest_id]}" if guest.blank?
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 => e
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
- def call(vendor:, trip_form:, price:, capacity:)
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 normal option value.
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
- def setup_option_values!(product)
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, 'Normal')
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
@@ -209,7 +209,7 @@ en:
209
209
  riel: "៛"
210
210
 
211
211
  spree:
212
- is_open_dated: "Is Open Dated?"
212
+ is_open_dated: "Is Open Dated Trip?"
213
213
  description: Description
214
214
  registered_by:
215
215
  system_registered: "System Registered"
@@ -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
@@ -0,0 +1,5 @@
1
+ class AddIndexCreatedAtToCmImports < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_index :cm_imports, :created_at, if_not_exists: true
4
+ end
5
+ end
@@ -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
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.5.14-pre6'.freeze
2
+ VERSION = '2.5.14'.freeze
3
3
 
4
4
  module_function
5
5
 
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.pre.pre6
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-20 00:00:00.000000000 Z
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: 1.3.1
3225
+ version: '0'
3221
3226
  requirements:
3222
3227
  - none
3223
3228
  rubygems_version: 3.4.1