spree_cm_commissioner 2.5.8 → 2.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree_cm_commissioner/orders_controller.rb +1 -1
  4. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +5 -0
  5. data/app/models/spree_cm_commissioner/order_decorator.rb +14 -0
  6. data/app/models/spree_cm_commissioner/pricing_action.rb +21 -14
  7. data/app/models/spree_cm_commissioner/pricing_actions/create_line_item_adjustments.rb +14 -0
  8. data/app/models/spree_cm_commissioner/pricing_actions/create_route_adjustments.rb +70 -0
  9. data/app/models/spree_cm_commissioner/pricing_model.rb +39 -0
  10. data/app/models/spree_cm_commissioner/pricing_model_handler/order.rb +72 -0
  11. data/app/models/spree_cm_commissioner/pricing_model_route.rb +16 -0
  12. data/app/models/spree_cm_commissioner/route.rb +2 -0
  13. data/app/queries/spree_cm_commissioner/check_in_sessions_metrics_query.rb +24 -0
  14. data/app/serializers/spree/v2/storefront/cart_serializer_decorator.rb +1 -0
  15. data/app/services/spree_cm_commissioner/cart/recalculate_decorator.rb +0 -4
  16. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +11 -1
  17. data/app/services/spree_cm_commissioner/orders/jwt_token/generate.rb +1 -1
  18. data/app/services/spree_cm_commissioner/transit/legs_builder_service.rb +2 -1
  19. data/app/services/spree_cm_commissioner/transit_order/create.rb +14 -8
  20. data/app/views/blazer/queries/_preset.html.erb +2 -4
  21. data/config/initializers/spree_permitted_attributes.rb +1 -0
  22. data/db/migrate/20260110073000_create_cm_pricing_model_routes.rb +37 -0
  23. data/db/migrate/20260204183545_add_term_accepted_at_to_spree_orders.rb +6 -0
  24. data/lib/spree_cm_commissioner/check_in_sessions_metric.rb +34 -0
  25. data/lib/spree_cm_commissioner/test_helper/factories/pricing_action_factory.rb +4 -0
  26. data/lib/spree_cm_commissioner/test_helper/factories/pricing_model_route_factory.rb +8 -0
  27. data/lib/spree_cm_commissioner/transit/leg.rb +4 -1
  28. data/lib/spree_cm_commissioner/version.rb +1 -1
  29. data/lib/spree_cm_commissioner.rb +1 -0
  30. metadata +10 -4
  31. data/app/services/spree_cm_commissioner/line_items/apply_pricing_models.rb +0 -27
  32. data/app/services/spree_cm_commissioner/pricing_models/apply.rb +0 -43
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52ecb9b50cd59df5354348171034968d7c6c1b7a59ba4ad1eb1a6b134d3b3a8b
4
- data.tar.gz: bc18344da1e94917ceff4341cec0bfaa82d1160941ec54a5ea8dd0b2d13860b8
3
+ metadata.gz: 1e130ccfe9b9b83a2152d4a021b2505e3a4e8544144df805541e9655fe024f40
4
+ data.tar.gz: 715e29f413240f94d3eb6a6a7bc6810fb7a443da0acac4d654dae65e1fc4070b
5
5
  SHA512:
6
- metadata.gz: 1e364c0093ef94862e7ab2d94f9e44b6ba2dee21b0951885c3c6e0feaded3e12ba572b856d33ea1fce84fc86c19ea87eb7bcf716c817ed4b5269c59b912976a4
7
- data.tar.gz: d6dc9e74e163f64d353513020ba7b8fdba7ab149774eb1dd3cf3153005ebdabea14de798968e043e6d9681aed59eed03334f36ea35d59707b20db277858bad59
6
+ metadata.gz: 15b4365a24963a3f2c427169331bf098728dcc12166d42878369a3060be530b880c8bf6678a35e90dfb2a81926615da425e6c1c334711bfe87906cd8216afe12
7
+ data.tar.gz: 374b79ae73eefe142f4358472e4800d069864c6efd83592a903203796bf6447ce1f0ac9ef369f4a78c3289e36cec47e49db111ee09335d61b011434e1ff6f7fb
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.5.8)
37
+ spree_cm_commissioner (2.5.9)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -7,7 +7,7 @@ module SpreeCmCommissioner
7
7
  @order = if params[:id].match?(/^R\d{9,}-([A-Za-z0-9_\-]+)$/)
8
8
  Spree::Order.search_by_qr_data!(params[:id])
9
9
  else
10
- SpreeCmCommissioner::Orders::Tickets::VerifyToken.call(token: params[:id]).value[:order]
10
+ SpreeCmCommissioner::Orders::JwtToken::Verify.call(token: params[:t]).value[:order]
11
11
  end
12
12
 
13
13
  @product_type = @order.products.first&.product_type || 'accommodation'
@@ -53,6 +53,7 @@ module SpreeCmCommissioner
53
53
 
54
54
  def direction = private_metadata['direction']
55
55
  def trip_id = private_metadata['trip_id']&.to_i
56
+ def connected_trip_id = private_metadata['connected_trip_id']
56
57
  def boarding_trip_stop_id = private_metadata['boarding_trip_stop_id']&.to_i
57
58
  def drop_off_trip_stop_id = private_metadata['drop_off_trip_stop_id']&.to_i
58
59
 
@@ -99,6 +100,10 @@ module SpreeCmCommissioner
99
100
  set_private_metadata_value('trip_id', value)
100
101
  end
101
102
 
103
+ def connected_trip_id=(value)
104
+ set_private_metadata_value('connected_trip_id', value)
105
+ end
106
+
102
107
  def boarding_trip_stop_id=(value)
103
108
  set_private_metadata_value('boarding_trip_stop_id', value)
104
109
  end
@@ -264,6 +264,20 @@ module SpreeCmCommissioner
264
264
  tenant&.formatted_url.presence || ENV.fetch('DEFAULT_URL_HOST')
265
265
  end
266
266
 
267
+ # Returns arrays of connected line_item IDs grouped by connected_trip_id.
268
+ # Example: [[1,2,3], [4,5], [6]]
269
+ #
270
+ # Line items with the same connected_trip_id are grouped together.
271
+ # Line items without a group_id are returned as singletons.
272
+ def connected_line_item_ids(direction: nil)
273
+ scoped = line_items
274
+ scoped = scoped.select { |li| li.direction == direction } if direction.present?
275
+
276
+ scoped.group_by { |li| li.connected_trip_id || li.id }
277
+ .values
278
+ .map { |group| group.map(&:id).sort }
279
+ end
280
+
267
281
  private
268
282
 
269
283
  def unstock_inventory_in_redis!
@@ -1,27 +1,34 @@
1
1
  module SpreeCmCommissioner
2
2
  class PricingAction < Base
3
- belongs_to :pricing_rule_group, class_name: 'SpreeCmCommissioner::PricingRuleGroup'
3
+ include Spree::CalculatedAdjustments
4
+ include Spree::AdjustmentSource
4
5
 
5
- has_one :calculator, as: :calculable, class_name: 'Spree::Calculator', dependent: :destroy
6
- accepts_nested_attributes_for :calculator
6
+ has_many :adjustments, as: :source, class_name: 'Spree::Adjustment'
7
7
 
8
- def perform(line_item)
9
- return if pricing_rule_group.pricing_rules.blank?
8
+ belongs_to :pricing_rule_group, class_name: 'SpreeCmCommissioner::PricingRuleGroup'
10
9
 
11
- amount = calculator&.compute(line_item)
10
+ delegate :pricing_rules, to: :pricing_rule_group
12
11
 
13
- line_item.adjustments.create!(
14
- amount: amount,
15
- source: self,
16
- adjustable: line_item,
17
- order: line_item.order,
18
- label: pricing_rule_group.pricing_model.name,
19
- mandatory: true
20
- )
12
+ def perform(_options = {})
13
+ raise NotImplementedError, "#{self.class}#perform must be implemented"
14
+ end
15
+
16
+ def compute_amount(adjustable)
17
+ calculator&.compute(adjustable) || 0
21
18
  end
22
19
 
23
20
  def self.available_calculator_types
24
21
  SpreeCmCommissioner::Calculators.constants.map(&:to_s)
25
22
  end
23
+
24
+ protected
25
+
26
+ def label
27
+ pricing_rule_group.name
28
+ end
29
+
30
+ def order_currency(order)
31
+ order&.currency || Spree::Config[:currency]
32
+ end
26
33
  end
27
34
  end
@@ -1,6 +1,20 @@
1
1
  module SpreeCmCommissioner
2
2
  module PricingActions
3
3
  class CreateLineItemAdjustments < SpreeCmCommissioner::PricingAction
4
+ def perform(options = {})
5
+ order = options[:order]
6
+ line_items = options[:line_items] || []
7
+
8
+ return if line_items.blank?
9
+
10
+ line_items.each do |line_item|
11
+ create_unique_adjustment(order, line_item)
12
+ end
13
+ end
14
+
15
+ def compute_amount(line_item)
16
+ compute(line_item)
17
+ end
4
18
  end
5
19
  end
6
20
  end
@@ -0,0 +1,70 @@
1
+ module SpreeCmCommissioner
2
+ module PricingActions
3
+ class CreateRouteAdjustments < SpreeCmCommissioner::PricingAction
4
+ def perform(options = {})
5
+ order = options[:order]
6
+ line_items = options[:line_items] || []
7
+
8
+ return if pricing_rules.blank?
9
+ return if line_items.blank?
10
+
11
+ eligible_guests = collect_unique_eligible_guests(line_items)
12
+ return if eligible_guests.empty?
13
+
14
+ total_amount = compute_total_amount(order, eligible_guests)
15
+ return if total_amount.zero?
16
+
17
+ create_route_adjustment(order, total_amount)
18
+ end
19
+
20
+ private
21
+
22
+ def collect_unique_eligible_guests(line_items)
23
+ seen = {}
24
+
25
+ line_items.each_with_object([]) do |line_item, result|
26
+ line_item.guests.select { |guest| guest_eligible?(guest) }.each_with_index do |guest, index|
27
+ key = guest.saved_guest_id.present? ? [guest.saved_guest_id, index] : guest.id
28
+ next if seen[key]
29
+
30
+ seen[key] = true
31
+ result << guest
32
+ end
33
+ end
34
+ end
35
+
36
+ def guest_eligible?(guest)
37
+ pricing_rules.all? do |rule|
38
+ !rule.respond_to?(:guest_eligible?) || rule.guest_eligible?(guest)
39
+ end
40
+ end
41
+
42
+ def compute_total_amount(order, guests)
43
+ guests.sum { |guest| compute_per_guest_amount(order, guest) }
44
+ end
45
+
46
+ def compute_per_guest_amount(order, guest)
47
+ item = build_per_guest_item(order, guest)
48
+ compute_amount(item)
49
+ end
50
+
51
+ def build_per_guest_item(order, guest)
52
+ Struct.new(:amount, :currency, :quantity).new(
53
+ guest.line_item.amount_per_guest,
54
+ order_currency(order),
55
+ 1
56
+ )
57
+ end
58
+
59
+ def create_route_adjustment(order, amount)
60
+ adjustments.create!(
61
+ adjustable: order,
62
+ amount: amount,
63
+ order: order,
64
+ label: label,
65
+ mandatory: true
66
+ )
67
+ end
68
+ end
69
+ end
70
+ end
@@ -6,6 +6,8 @@ module SpreeCmCommissioner
6
6
 
7
7
  has_many :pricing_model_variants, class_name: 'SpreeCmCommissioner::PricingModelVariant', dependent: :destroy
8
8
  has_many :variants, through: :pricing_model_variants, class_name: 'Spree::Variant'
9
+ has_many :pricing_model_routes, class_name: 'SpreeCmCommissioner::PricingModelRoute', dependent: :destroy
10
+ has_many :routes, through: :pricing_model_routes, class_name: 'SpreeCmCommissioner::Route'
9
11
  has_many :pricing_rule_groups, class_name: 'SpreeCmCommissioner::PricingRuleGroup', dependent: :destroy
10
12
  has_many :pricing_rules, through: :pricing_rule_groups, class_name: 'SpreeCmCommissioner::PricingRule'
11
13
 
@@ -14,5 +16,42 @@ module SpreeCmCommissioner
14
16
  validates :name, presence: true, uniqueness: { scope: :vendor_id }
15
17
 
16
18
  enum status: { draft: 0, active: 1, archived: 2 }
19
+
20
+ # Activates pricing model for an order (similar to Spree::Promotion#activate)
21
+ def activate(order:, line_items:)
22
+ pricing_rule_groups.each do |group|
23
+ activate_group(group, order, line_items)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def activate_group(group, order, line_items)
30
+ eligible_rules = group.pricing_rules.select { |r| any_line_item_eligible?(r, line_items) }
31
+
32
+ return if eligible_rules.empty?
33
+
34
+ if group_eligible?(group, eligible_rules)
35
+
36
+ group.pricing_action&.perform(order: order, line_items: line_items)
37
+ else
38
+ Rails.logger.info("PricingRuleGroup #{group.id} not eligible for order #{order.id}")
39
+ end
40
+ end
41
+
42
+ def any_line_item_eligible?(rule, line_items)
43
+ line_items.any? { |li| rule.eligible?(li) }
44
+ end
45
+
46
+ def group_eligible?(group, eligible_rules)
47
+ case group.match_type
48
+ when 'all'
49
+ eligible_rules.size == group.pricing_rules.size
50
+ when 'any'
51
+ eligible_rules.any?
52
+ else
53
+ false
54
+ end
55
+ end
17
56
  end
18
57
  end
@@ -0,0 +1,72 @@
1
+ module SpreeCmCommissioner
2
+ module PricingModelHandler
3
+ # Handles pricing model activation for orders
4
+ class Order
5
+ attr_reader :order
6
+
7
+ def initialize(order)
8
+ @order = order
9
+ end
10
+
11
+ def activate
12
+ return unless order&.persisted?
13
+
14
+ order.adjustments.pricing_action.delete_all
15
+
16
+ order.connected_line_item_ids.each do |group_ids|
17
+ line_items_in_group = order.line_items.where(id: group_ids)
18
+ activate_for_line_items(line_items_in_group)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def activate_for_line_items(line_items)
25
+ return if line_items.blank?
26
+
27
+ pricing_models = find_pricing_models(line_items).compact
28
+ return if pricing_models.blank?
29
+
30
+ pricing_models.each do |pricing_model|
31
+ pricing_model.activate(order: order, line_items: line_items)
32
+ end
33
+ end
34
+
35
+ def find_pricing_models(line_items)
36
+ route_pricing_models(line_items) + variant_pricing_models(line_items)
37
+ end
38
+
39
+ def route_pricing_models(line_items)
40
+ transit_line_items = line_items.select { |li| li.trip_id.present? }
41
+ return [] if transit_line_items.blank?
42
+
43
+ origin_place_id, destination_place_id = determine_origin_destination(transit_line_items)
44
+
45
+ SpreeCmCommissioner::PricingModelRoute
46
+ .active
47
+ .for_origin_destination(origin_place_id, destination_place_id)
48
+ .includes(:pricing_model)
49
+ .map(&:pricing_model)
50
+ end
51
+
52
+ def variant_pricing_models(line_items)
53
+ line_items.flat_map { |li| li.variant&.pricing_models&.active&.to_a || [] }
54
+ end
55
+
56
+ def determine_origin_destination(line_items)
57
+ if line_items.size > 1
58
+ first_line_item = line_items.min_by(&:id)
59
+ last_line_item = line_items.max_by(&:id)
60
+
61
+ first_trip = SpreeCmCommissioner::Trip.find(first_line_item.trip_id)
62
+ last_trip = SpreeCmCommissioner::Trip.find(last_line_item.trip_id)
63
+
64
+ [first_trip.origin_place_id, last_trip.destination_place_id]
65
+ else
66
+ trip = SpreeCmCommissioner::Trip.find(line_items.first.trip_id)
67
+ [trip.origin_place_id, trip.destination_place_id]
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,16 @@
1
+ module SpreeCmCommissioner
2
+ class PricingModelRoute < Base
3
+ belongs_to :route, class_name: 'SpreeCmCommissioner::Route'
4
+ belongs_to :pricing_model, class_name: 'SpreeCmCommissioner::PricingModel'
5
+ belongs_to :origin_place, class_name: 'SpreeCmCommissioner::Place'
6
+ belongs_to :destination_place, class_name: 'SpreeCmCommissioner::Place'
7
+
8
+ scope :for_origin_destination, lambda { |origin_place_id, destination_place_id|
9
+ where(origin_place_id: origin_place_id, destination_place_id: destination_place_id)
10
+ }
11
+
12
+ scope :active, lambda {
13
+ joins(:pricing_model).where(pricing_model: { status: :active })
14
+ }
15
+ end
16
+ end
@@ -14,6 +14,8 @@ module SpreeCmCommissioner
14
14
  has_many :route_photos, class_name: 'SpreeCmCommissioner::RoutePhoto', as: :viewable, dependent: :destroy
15
15
 
16
16
  has_many :trips, inverse_of: :route
17
+ has_many :pricing_model_routes, class_name: 'SpreeCmCommissioner::PricingModelRoute', dependent: :destroy
18
+ has_many :pricing_models, through: :pricing_model_routes, class_name: 'SpreeCmCommissioner::PricingModel'
17
19
 
18
20
  validates :route_name, presence: true
19
21
 
@@ -0,0 +1,24 @@
1
+ module SpreeCmCommissioner
2
+ class CheckInSessionsMetricsQuery
3
+ def initialize(event:, session:)
4
+ @event = event
5
+ @session = session
6
+ end
7
+
8
+ def call
9
+ guests = @session.check_in_rules.any? ? eligible_guests_for_session : @event.guests
10
+ guests_count = guests.count
11
+ check_ins_count = guests.left_joins(:check_in).where.not(cm_check_ins: { id: nil })
12
+ .where(cm_check_ins: { check_in_session_id: [@session.id, nil] })
13
+ .distinct.count(:id)
14
+
15
+ CheckInSessionsMetric.new(check_ins_count: check_ins_count, guests_count: guests_count)
16
+ end
17
+
18
+ private
19
+
20
+ def eligible_guests_for_session
21
+ @event.guests.where('cm_guests.public_metadata @> ?::jsonb', { eligible_check_in_session_ids: [@session.id] }.to_json)
22
+ end
23
+ end
24
+ end
@@ -4,6 +4,7 @@ module Spree
4
4
  module CartSerializerDecorator
5
5
  def self.prepended(base)
6
6
  base.attributes :phone_number, :intel_phone_number, :country_code, :request_state,
7
+ :term_accepted_at,
7
8
  :channel, :hold_expires_at
8
9
 
9
10
  base.attribute :qr_data do |order|
@@ -24,10 +24,6 @@ module SpreeCmCommissioner
24
24
  else
25
25
  order.ensure_updated_shipments
26
26
  end
27
- # SPREE: Original Spree::Cart::Recalculate code ends here
28
-
29
- # CUSTOM: Apply pricing models (intercity taxi, line item pricing) to the line item
30
- SpreeCmCommissioner::LineItems::ApplyPricingModels.call(order: order, line_item: line_item)
31
27
 
32
28
  # SPREE: Original Spree::Cart::Recalculate code continues here
33
29
  ::Spree::PromotionHandler::Cart.new(order, line_item).activate
@@ -36,6 +36,8 @@ class SpreeCmCommissioner::Integrations::StadiumXV1
36
36
 
37
37
  match_mapping = Spree::Taxon.find_or_initialize_integration_mapping(integration_id: @integration.id, external_id: external_match._id)
38
38
  match_taxon = match_mapping.internal
39
+ match_name = "#{external_match.home_name} vs #{external_match.away_name}"
40
+ match_permalink = generate_match_permalink(match_name)
39
41
 
40
42
  # Track match sync and create event taxon directly under events/
41
43
  @sync_result.track(:match, match_taxon) do |tracker|
@@ -43,7 +45,8 @@ class SpreeCmCommissioner::Integrations::StadiumXV1
43
45
  vendor: @integration.vendor,
44
46
  parent: events_root_taxon,
45
47
  taxonomy: events_taxonomy,
46
- name: "#{external_match.home_name} vs #{external_match.away_name}",
48
+ name: match_name,
49
+ permalink: match_permalink,
47
50
  kind: :event,
48
51
  from_date: external_match.match_datetime,
49
52
  to_date: external_match.match_datetime + 120.minutes,
@@ -108,6 +111,13 @@ class SpreeCmCommissioner::Integrations::StadiumXV1
108
111
  def events_taxonomy
109
112
  @events_taxonomy ||= Spree::Taxonomy.events
110
113
  end
114
+
115
+ def generate_match_permalink(match_name)
116
+ loop do
117
+ base_permalink = "#{events_root_taxon.permalink}/#{match_name.to_s.parameterize}-#{SecureRandom.hex(4)}"
118
+ break base_permalink unless Spree::Taxon.exists?(permalink: base_permalink)
119
+ end
120
+ end
111
121
  end
112
122
  end
113
123
  end
@@ -5,7 +5,7 @@ module SpreeCmCommissioner
5
5
 
6
6
  def call(order:)
7
7
  nonce = generate_nonce
8
- token = "#{order.number}?jwt_token=#{nonce}-#{generate_qr_jwt(order: order, nonce: nonce)}"
8
+ token = "#{order.number}?t=#{nonce}-#{generate_qr_jwt(order: order, nonce: nonce)}"
9
9
 
10
10
  success(token: token)
11
11
  end
@@ -13,6 +13,7 @@ module SpreeCmCommissioner
13
13
  SpreeCmCommissioner::Transit::Leg.new(
14
14
  direction: direction,
15
15
  trip_id: leg['trip_id'].to_i,
16
+ main_trip_id: leg['main_trip_id']&.to_i,
16
17
  boarding_trip_stop_id: leg['boarding_trip_stop_id'].to_i,
17
18
  drop_off_trip_stop_id: leg['drop_off_trip_stop_id'].to_i,
18
19
  seat_selections: Array(leg['seat_selections']).map do |seat|
@@ -35,7 +36,7 @@ module SpreeCmCommissioner
35
36
  else
36
37
  legs_params.map do |leg|
37
38
  leg.permit(
38
- :trip_id, :boarding_trip_stop_id, :drop_off_trip_stop_id,
39
+ :trip_id, :main_trip_id, :boarding_trip_stop_id, :drop_off_trip_stop_id,
39
40
  seat_selections: [:variant_id, :quantity, { block_ids: [] }]
40
41
  )
41
42
  end
@@ -53,8 +53,11 @@ module SpreeCmCommissioner
53
53
  all_line_items = []
54
54
  current_leg_date = initial_date
55
55
 
56
+ # Use main_trip_id for connected trips (when legs have multiple trips)
57
+ connected_trip_id = legs.size > 1 ? legs.first.main_trip_id&.to_s : nil
58
+
56
59
  legs.each do |leg|
57
- leg_line_items = build_line_items_for!(leg, order, current_leg_date)
60
+ leg_line_items = build_line_items_for!(leg, order, current_leg_date, connected_trip_id)
58
61
  leg_line_items = insert_saved_guests_per_line_items_leg(leg_line_items)
59
62
 
60
63
  all_line_items.concat(leg_line_items)
@@ -64,7 +67,7 @@ module SpreeCmCommissioner
64
67
  all_line_items
65
68
  end
66
69
 
67
- def build_line_items_for!(leg, order, date)
70
+ def build_line_items_for!(leg, order, date, connected_trip_id = nil)
68
71
  trip = SpreeCmCommissioner::Trip.find(leg.trip_id)
69
72
  trip_stops = trip.trip_stops.where(id: [leg.boarding_trip_stop_id, leg.drop_off_trip_stop_id]).index_by(&:id)
70
73
 
@@ -75,18 +78,21 @@ module SpreeCmCommissioner
75
78
  )
76
79
 
77
80
  leg.seat_selections.group_by(&:variant_id).map do |variant_id, seat_selections|
81
+ metadata = {
82
+ direction: leg.direction,
83
+ trip_id: leg.trip_id.to_s,
84
+ boarding_trip_stop_id: leg.boarding_trip_stop_id.to_s,
85
+ drop_off_trip_stop_id: leg.drop_off_trip_stop_id.to_s
86
+ }
87
+ metadata[:connected_trip_id] = connected_trip_id if connected_trip_id.present?
88
+
78
89
  line_item = order.line_items.new(
79
90
  product_type: :transit,
80
91
  from_date: from_date,
81
92
  to_date: to_date,
82
93
  variant_id: variant_id,
83
94
  quantity: seat_selections.sum(&:quantity),
84
- private_metadata: {
85
- direction: leg.direction,
86
- trip_id: leg.trip_id.to_s,
87
- boarding_trip_stop_id: leg.boarding_trip_stop_id.to_s,
88
- drop_off_trip_stop_id: leg.drop_off_trip_stop_id.to_s
89
- }
95
+ private_metadata: metadata
90
96
  )
91
97
 
92
98
  build_guests_for!(line_item, seat_selections)
@@ -4,8 +4,6 @@
4
4
  <%= f.label :preset, Spree.t(:preset) %>
5
5
  </div>
6
6
  <small style="margin-bottom: 6px; display: inline-block;">
7
- Mark as <strong>preset</strong> to auto-assign this report to new organizers or vendors.
7
+ Preset reports are generated automatically for all new organizers and vendors once created.
8
8
  </small>
9
- <% end %>
10
-
11
-
9
+ <% end %>
@@ -63,6 +63,7 @@ module Spree
63
63
  :channel,
64
64
  :phone_number,
65
65
  :country_code,
66
+ :term_accepted_at,
66
67
  {
67
68
  saved_guests_attributes: %i[
68
69
  id
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to create cm_route_pricing_models table
4
+ # This table links pricing models to routes with specific origin/destination pairs
5
+ #
6
+ # Example: A route PP → KP → KPC → SR can have different pricing models for each O-D pair:
7
+ # - PP → SR: $1 extra charge
8
+ # - PP → KP: $2 extra charge
9
+ # - KP → KPC: $3 extra charge
10
+ # - KPC → SR: $4 extra charge
11
+ #
12
+ # When a user books PP → SR, only the PP → SR pricing model ($1) applies,
13
+ # not the sum of all segment charges
14
+
15
+ class CreateCmPricingModelRoutes < ActiveRecord::Migration[7.0]
16
+ def change
17
+ create_table :cm_pricing_model_routes, if_not_exists: true do |t|
18
+ t.references :route, null: false, foreign_key: { to_table: :cm_routes }
19
+ t.references :pricing_model, null: false, foreign_key: { to_table: :cm_pricing_models }
20
+ t.references :origin_place, null: false, foreign_key: { to_table: :cm_places }
21
+ t.references :destination_place, null: false, foreign_key: { to_table: :cm_places }
22
+
23
+ t.timestamps
24
+ end
25
+
26
+ # Ensure unique combination of route + pricing_model + origin + destination
27
+ add_index :cm_pricing_model_routes,
28
+ %i[route_id pricing_model_id origin_place_id destination_place_id],
29
+ unique: true,
30
+ name: 'idx_pricing_model_routes_unique'
31
+
32
+ # Index for efficient lookup by route and origin/destination
33
+ add_index :cm_pricing_model_routes,
34
+ %i[route_id origin_place_id destination_place_id],
35
+ name: 'idx_pricing_model_routes_on_route_origin_dest'
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ class AddTermAcceptedAtToSpreeOrders < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :spree_orders, :term_accepted_at, :datetime, null: true, if_not_exists: true
4
+ add_index :spree_orders, :term_accepted_at, if_not_exists: true
5
+ end
6
+ end
@@ -0,0 +1,34 @@
1
+ module SpreeCmCommissioner
2
+ class CheckInSessionsMetric
3
+ attr_reader :check_ins_count, :guests_count, :percent, :other_value
4
+
5
+ def initialize(check_ins_count:, guests_count:)
6
+ @check_ins_count = check_ins_count
7
+ @guests_count = guests_count
8
+ @percent = calculate_attendance_percent
9
+ @other_value = 100 - @percent
10
+ end
11
+
12
+ def to_h
13
+ {
14
+ check_ins_count: check_ins_count,
15
+ guests_count: guests_count,
16
+ no_show: no_show,
17
+ percent: percent,
18
+ other_value: other_value
19
+ }
20
+ end
21
+
22
+ def no_show
23
+ guests_count - check_ins_count
24
+ end
25
+
26
+ private
27
+
28
+ def calculate_attendance_percent
29
+ return 0 if guests_count.zero?
30
+
31
+ ((check_ins_count.to_f / guests_count) * 100).round(2)
32
+ end
33
+ end
34
+ end
@@ -6,4 +6,8 @@ FactoryBot.define do
6
6
  factory :pricing_action_guest_adjustments, class: 'SpreeCmCommissioner::PricingActions::CreateGuestAdjustments' do
7
7
  association :pricing_rule_group, factory: :pricing_rule_group
8
8
  end
9
+
10
+ factory :pricing_action_route_adjustment, class: 'SpreeCmCommissioner::PricingActions::CreateRouteAdjustments' do
11
+ association :pricing_rule_group, factory: :pricing_rule_group
12
+ end
9
13
  end
@@ -0,0 +1,8 @@
1
+ FactoryBot.define do
2
+ factory :cm_pricing_model_route, class: 'SpreeCmCommissioner::PricingModelRoute' do
3
+ association :pricing_model, factory: :cm_pricing_model
4
+ association :route, factory: :cm_route
5
+ association :origin_place, factory: :cm_place
6
+ association :destination_place, factory: :cm_place
7
+ end
8
+ end
@@ -1,10 +1,11 @@
1
1
  module SpreeCmCommissioner::Transit
2
2
  class Leg
3
- attr_accessor :direction, :trip_id, :boarding_trip_stop_id, :drop_off_trip_stop_id, :seat_selections
3
+ attr_accessor :direction, :trip_id, :main_trip_id, :boarding_trip_stop_id, :drop_off_trip_stop_id, :seat_selections
4
4
 
5
5
  def initialize(options = {})
6
6
  @direction = options[:direction]
7
7
  @trip_id = options[:trip_id]
8
+ @main_trip_id = options[:main_trip_id]
8
9
  @boarding_trip_stop_id = options[:boarding_trip_stop_id]
9
10
  @drop_off_trip_stop_id = options[:drop_off_trip_stop_id]
10
11
  @seat_selections = options[:seat_selections] || []
@@ -14,6 +15,7 @@ module SpreeCmCommissioner::Transit
14
15
  new(
15
16
  direction: hash[:direction], # outbound / inbound
16
17
  trip_id: hash[:trip_id],
18
+ main_trip_id: hash[:main_trip_id],
17
19
  boarding_trip_stop_id: hash[:boarding_trip_stop_id],
18
20
  drop_off_trip_stop_id: hash[:drop_off_trip_stop_id],
19
21
  seat_selections: (hash[:seat_selections] || []).map { |seat_selection| SeatSelection.from_hash(seat_selection) }
@@ -28,6 +30,7 @@ module SpreeCmCommissioner::Transit
28
30
  {
29
31
  direction: @direction,
30
32
  trip_id: @trip_id,
33
+ main_trip_id: @main_trip_id,
31
34
  boarding_trip_stop_id: @boarding_trip_stop_id,
32
35
  drop_off_trip_stop_id: @drop_off_trip_stop_id,
33
36
  seat_selections: @seat_selections.map(&:to_h)
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.5.8'.freeze
2
+ VERSION = '2.5.9'.freeze
3
3
 
4
4
  module_function
5
5
 
@@ -30,6 +30,7 @@ require 'spree_cm_commissioner/transit/trip_stop_form'
30
30
  require 'spree_cm_commissioner/transit/service_calendar_form'
31
31
  require 'spree_cm_commissioner/intercity_taxi/map_place'
32
32
  require 'spree_cm_commissioner/distance'
33
+ require 'spree_cm_commissioner/check_in_sessions_metric'
33
34
 
34
35
  require 'activerecord_multi_tenant'
35
36
  require 'google/cloud/recaptcha_enterprise'
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.8
4
+ version: 2.5.9
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-03 00:00:00.000000000 Z
11
+ date: 2026-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree
@@ -1515,7 +1515,10 @@ files:
1515
1515
  - app/models/spree_cm_commissioner/pricing_action.rb
1516
1516
  - app/models/spree_cm_commissioner/pricing_actions/create_guest_adjustments.rb
1517
1517
  - app/models/spree_cm_commissioner/pricing_actions/create_line_item_adjustments.rb
1518
+ - app/models/spree_cm_commissioner/pricing_actions/create_route_adjustments.rb
1518
1519
  - app/models/spree_cm_commissioner/pricing_model.rb
1520
+ - app/models/spree_cm_commissioner/pricing_model_handler/order.rb
1521
+ - app/models/spree_cm_commissioner/pricing_model_route.rb
1519
1522
  - app/models/spree_cm_commissioner/pricing_model_variant.rb
1520
1523
  - app/models/spree_cm_commissioner/pricing_rule.rb
1521
1524
  - app/models/spree_cm_commissioner/pricing_rule_group.rb
@@ -1776,6 +1779,7 @@ files:
1776
1779
  - app/overrides/spree/layouts/admin/map_head.html.erb.deface
1777
1780
  - app/presenters/spree/variants/visible_options_presenter.rb
1778
1781
  - app/queries/spree_cm_commissioner/accommodation_query.rb
1782
+ - app/queries/spree_cm_commissioner/check_in_sessions_metrics_query.rb
1779
1783
  - app/queries/spree_cm_commissioner/dashboard_crew_event_query.rb
1780
1784
  - app/queries/spree_cm_commissioner/event_chart_queries.rb
1781
1785
  - app/queries/spree_cm_commissioner/event_ticket_aggregator_query.rb
@@ -2043,7 +2047,6 @@ files:
2043
2047
  - app/services/spree_cm_commissioner/intercity_taxi_order/calculate_distance.rb
2044
2048
  - app/services/spree_cm_commissioner/intercity_taxi_order/create.rb
2045
2049
  - app/services/spree_cm_commissioner/intercity_taxi_order/update.rb
2046
- - app/services/spree_cm_commissioner/line_items/apply_pricing_models.rb
2047
2050
  - app/services/spree_cm_commissioner/line_items/sync_event_date.rb
2048
2051
  - app/services/spree_cm_commissioner/metafields/product_metadata_service.rb
2049
2052
  - app/services/spree_cm_commissioner/order_params_checker.rb
@@ -2056,7 +2059,6 @@ files:
2056
2059
  - app/services/spree_cm_commissioner/organizer/export_invite_guest_csv_service.rb
2057
2060
  - app/services/spree_cm_commissioner/payment_method_type_mapper.rb
2058
2061
  - app/services/spree_cm_commissioner/penalty_calculator.rb
2059
- - app/services/spree_cm_commissioner/pricing_models/apply.rb
2060
2062
  - app/services/spree_cm_commissioner/pricing_models/create_with_rule_groups.rb
2061
2063
  - app/services/spree_cm_commissioner/pricing_models/update_with_rule_groups.rb
2062
2064
  - app/services/spree_cm_commissioner/pricing_rules/build_params.rb
@@ -2998,9 +3000,11 @@ files:
2998
3000
  - db/migrate/20260105072450_migrate_cm_trip_stops_to_support_trip_connection.rb
2999
3001
  - db/migrate/20260106093359_add_contact_phone_to_cm_vendor_places.rb
3000
3002
  - db/migrate/20260108101406_add_allow_booking_to_cm_trips.rb
3003
+ - db/migrate/20260110073000_create_cm_pricing_model_routes.rb
3001
3004
  - db/migrate/20260121024645_add_nationality_group_to_cm_guests.rb
3002
3005
  - db/migrate/20260126110528_seed_user_initial_usernames.rb
3003
3006
  - db/migrate/20260128043540_add_counter_cache_to_spree_users.rb
3007
+ - db/migrate/20260204183545_add_term_accepted_at_to_spree_orders.rb
3004
3008
  - docker-compose.yml
3005
3009
  - docs/api/scoped-access-token-endpoints.md
3006
3010
  - docs/option_types/attr_types.md
@@ -3025,6 +3029,7 @@ files:
3025
3029
  - lib/spree_cm_commissioner.rb
3026
3030
  - lib/spree_cm_commissioner/cached_inventory_item.rb
3027
3031
  - lib/spree_cm_commissioner/calendar_event.rb
3032
+ - lib/spree_cm_commissioner/check_in_sessions_metric.rb
3028
3033
  - lib/spree_cm_commissioner/distance.rb
3029
3034
  - lib/spree_cm_commissioner/engine.rb
3030
3035
  - lib/spree_cm_commissioner/factories.rb
@@ -3081,6 +3086,7 @@ files:
3081
3086
  - lib/spree_cm_commissioner/test_helper/factories/place_factory.rb
3082
3087
  - lib/spree_cm_commissioner/test_helper/factories/pricing_action_factory.rb
3083
3088
  - lib/spree_cm_commissioner/test_helper/factories/pricing_model_factory.rb
3089
+ - lib/spree_cm_commissioner/test_helper/factories/pricing_model_route_factory.rb
3084
3090
  - lib/spree_cm_commissioner/test_helper/factories/pricing_model_variant_factory.rb
3085
3091
  - lib/spree_cm_commissioner/test_helper/factories/pricing_rule_factory.rb
3086
3092
  - lib/spree_cm_commissioner/test_helper/factories/pricing_rule_group_factory.rb
@@ -1,27 +0,0 @@
1
- module SpreeCmCommissioner
2
- module LineItems
3
- class ApplyPricingModels
4
- prepend ::Spree::ServiceModule::Base
5
-
6
- def call(order:, line_item:)
7
- return success(nil) if line_item.blank?
8
-
9
- return success(line_item) unless order&.persisted?
10
- return success(line_item) if line_item.variant.blank?
11
-
12
- active_pricing_models = line_item.variant.pricing_models.active
13
- return success(line_item) unless active_pricing_models.exists?
14
-
15
- line_item.adjustments.pricing_action.delete_all
16
-
17
- active_pricing_models.each do |pricing_model|
18
- SpreeCmCommissioner::PricingModels::Apply.new(
19
- line_item: line_item,
20
- pricing_model: pricing_model
21
- ).call
22
- end
23
- success(line_item)
24
- end
25
- end
26
- end
27
- end
@@ -1,43 +0,0 @@
1
- module SpreeCmCommissioner
2
- module PricingModels
3
- class Apply
4
- def initialize(line_item:, pricing_model:)
5
- @line_item = line_item
6
- @pricing_model = pricing_model
7
- end
8
-
9
- def call
10
- pricing_model.pricing_rule_groups.each do |group|
11
- apply_group(group, line_item)
12
- end
13
- end
14
-
15
- private
16
-
17
- attr_reader :line_item, :pricing_model
18
-
19
- def apply_group(group, line_item)
20
- eligible_rules = group.pricing_rules.select { |r| r.eligible?(line_item) }
21
-
22
- return if eligible_rules.empty?
23
-
24
- if group_eligible?(group, eligible_rules)
25
- group.pricing_action&.perform(line_item)
26
- else
27
- Rails.logger.info("Group #{group.id} not eligible for line_item #{line_item.id}")
28
- end
29
- end
30
-
31
- def group_eligible?(group, eligible_rules)
32
- case group.match_type
33
- when 'all'
34
- eligible_rules.size == group.pricing_rules.size
35
- when 'any'
36
- eligible_rules.any?
37
- else
38
- false
39
- end
40
- end
41
- end
42
- end
43
- end