spree_cm_commissioner 2.8.3.pre.pre11 → 2.8.3.pre.pre12

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/api/v2/storefront/transit/draft_orders_controller.rb +3 -1
  4. data/app/controllers/spree/api/v2/tenant/transit/draft_orders_controller.rb +3 -1
  5. data/app/interactors/spree_cm_commissioner/pin_code_sender.rb +62 -5
  6. data/app/jobs/spree_cm_commissioner/telegram_gateway/pin_code_sender_job.rb +18 -0
  7. data/app/models/concerns/spree_cm_commissioner/line_item_open_dated_trippable.rb +50 -7
  8. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +0 -1
  9. data/app/models/spree_cm_commissioner/pin_code.rb +62 -9
  10. data/app/models/spree_cm_commissioner/product_decorator.rb +6 -0
  11. data/app/models/spree_cm_commissioner/show.rb +0 -4
  12. data/app/models/spree_cm_commissioner/trip.rb +2 -12
  13. data/app/models/spree_cm_commissioner/voting_contestant.rb +0 -1
  14. data/app/queries/spree_cm_commissioner/single_leg_trips_query.rb +24 -3
  15. data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +3 -1
  16. data/app/serializers/spree/v2/tenant/show_episode_serializer.rb +1 -1
  17. data/app/serializers/spree/v2/tenant/show_serializer.rb +1 -2
  18. data/app/serializers/spree_cm_commissioner/v2/storefront/pin_code_serializer.rb +1 -1
  19. data/app/serializers/spree_cm_commissioner/v2/storefront/ticket_transfer_minimal_serializer.rb +1 -1
  20. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_query_result_serializer.rb +3 -1
  21. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +2 -1
  22. data/app/services/spree_cm_commissioner/open_dated_trips/redeem.rb +80 -36
  23. data/app/services/spree_cm_commissioner/telegram_gateway/pin_code_sender.rb +99 -0
  24. data/app/services/spree_cm_commissioner/transit_order/create.rb +48 -14
  25. data/app/services/spree_cm_commissioner/trips/add_ons/create.rb +124 -0
  26. data/app/services/spree_cm_commissioner/trips/add_ons/update_price.rb +36 -0
  27. data/app/services/spree_cm_commissioner/trips/create_multi_leg.rb +1 -2
  28. data/app/services/spree_cm_commissioner/trips/variants/create.rb +1 -2
  29. data/app/services/spree_cm_commissioner/voting_sessions/finalize.rb +28 -29
  30. data/app/services/telegram_gateway_adapter/client.rb +124 -0
  31. data/config/locales/en.yml +5 -6
  32. data/config/locales/km.yml +15 -48
  33. data/db/migrate/20260610000001_add_delivery_channel_to_cm_pin_codes.rb +6 -0
  34. data/db/migrate/20260610000001_drop_is_open_dated_from_cm_trips.rb +6 -0
  35. data/db/migrate/20260614000001_change_code_limit_in_cm_pin_codes.rb +5 -0
  36. data/db/migrate/20260615000002_add_index_to_code_on_cm_pin_codes.rb +5 -0
  37. data/lib/spree_cm_commissioner/transit/trip_form.rb +0 -13
  38. data/lib/spree_cm_commissioner/trip_query_result.rb +37 -6
  39. data/lib/spree_cm_commissioner/trip_result.rb +6 -7
  40. data/lib/spree_cm_commissioner/version.rb +1 -1
  41. metadata +11 -7
  42. data/app/serializers/spree/v2/tenant/campaign_serializer.rb +0 -13
  43. data/app/services/spree_cm_commissioner/advertisements/sorted_advertisements.rb +0 -61
  44. data/app/services/spree_cm_commissioner/advertisements/update_campaign_sorted_ads_ids.rb +0 -33
  45. data/app/services/spree_cm_commissioner/trips/create_open_dated_trip.rb +0 -98
  46. data/app/services/spree_cm_commissioner/trips/update_open_dated_trip.rb +0 -67
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc670dfc1c70b1d0307e09941e9e1bbcbcc8b8ebebcd3ff883b81350e42ead04
4
- data.tar.gz: 8c1fc869352e101588a38ff8ce00726a50788e7ea9bd936e53d74cf01246ef00
3
+ metadata.gz: dc7986faabd73e3ae1e70e3a80e14f59119301779f847a112d585d15bf52406f
4
+ data.tar.gz: 97c8e0f4d601a7ddb553e786bc23f0f8b9a70a1eae307d7cf79da8e05d70d5c9
5
5
  SHA512:
6
- metadata.gz: 696ae98c855011b50c62a2cb60e6f31564d3841531361785595b2334adb340e6f3a189bc73bb3b636801f66664d1bf8037f95bc7ea17895769be09825546e10c
7
- data.tar.gz: 641c6beaccb2eb7cc69977d204f583af78d0f49ecd642da05569579a57e96c6e882be53a163ce20cb5d8f0550970bc7998f07b173b6070d20580e9e6ac87ae37
6
+ metadata.gz: a1bec82d194a2943c597dc804c2a3f69c7a3f09da8c9734d82e76c1af7c102748175307e1a530a7107a0011bf5d1556cefc1c2d9fd7929e1e52ea2979912ed2c
7
+ data.tar.gz: 992f9e6d0c5b58e06d726ed5967121d85530bf654410a475ce7b694f7e559dc42d6bc0e021136e550b000517b6d9a505e45db16bede3bc2ecc0b70c308c267ec
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.8.3.pre.pre11)
37
+ spree_cm_commissioner (2.8.3.pre.pre12)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -11,6 +11,7 @@ module Spree
11
11
  # - inbound_date: Date of inbound transit (optional)
12
12
  # - outbound_legs: Array of outbound leg details (required)
13
13
  # - inbound_legs: Array of inbound leg details (optional)
14
+ # - include_return: Whether to add the trip's "return included" add-on as a line item (optional)
14
15
  def create
15
16
  @outbound_legs = params[:outbound_legs].is_a?(Array) && params[:outbound_legs].any? ? build_legs(:outbound, params[:outbound_legs]) : []
16
17
  @inbound_legs = params[:inbound_legs].is_a?(Array) && params[:inbound_legs].any? ? build_legs(:inbound, params[:inbound_legs]) : []
@@ -20,7 +21,8 @@ module Spree
20
21
  inbound_date: params[:inbound_date]&.to_date,
21
22
  outbound_legs: @outbound_legs,
22
23
  inbound_legs: @inbound_legs,
23
- user: spree_current_user
24
+ user: spree_current_user,
25
+ include_return: ActiveModel::Type::Boolean.new.cast(params[:include_return])
24
26
  )
25
27
 
26
28
  if result.success?
@@ -11,6 +11,7 @@ module Spree
11
11
  # - inbound_date: Date of inbound transit (optional)
12
12
  # - outbound_legs: Array of outbound leg details (required)
13
13
  # - inbound_legs: Array of inbound leg details (optional)
14
+ # - include_return: Whether to add the trip's "return included" add-on as a line item (optional)
14
15
  def create
15
16
  @outbound_legs = params[:outbound_legs].is_a?(Array) && params[:outbound_legs].any? ? build_legs(:outbound, params[:outbound_legs]) : []
16
17
  @inbound_legs = params[:inbound_legs].is_a?(Array) && params[:inbound_legs].any? ? build_legs(:inbound, params[:inbound_legs]) : []
@@ -20,7 +21,8 @@ module Spree
20
21
  inbound_date: params[:inbound_date]&.to_date,
21
22
  outbound_legs: @outbound_legs,
22
23
  inbound_legs: @inbound_legs,
23
- user: spree_current_user
24
+ user: spree_current_user,
25
+ include_return: ActiveModel::Type::Boolean.new.cast(params[:include_return])
24
26
  )
25
27
 
26
28
  if result.success?
@@ -6,7 +6,7 @@ module SpreeCmCommissioner
6
6
  context.fail!(message: I18n.t('pincode_sender.pincode.blank')) if context.pin_code.nil?
7
7
 
8
8
  if context.pin_code.phone_number?
9
- send_sms
9
+ send_phone_otp
10
10
  else
11
11
  send_email
12
12
  end
@@ -16,18 +16,73 @@ module SpreeCmCommissioner
16
16
 
17
17
  private
18
18
 
19
- def send_sms
19
+ # Channel selection for phone-number OTPs lives here so the routing is
20
+ # easy to find and reason about. Telegram Gateway is primary; SMS is the fallback.
21
+ def send_phone_otp
22
+ if telegram_gateway_available?
23
+ send_via_telegram_gateway
24
+ else
25
+ send_via_sms
26
+ end
27
+ end
28
+
29
+ def telegram_gateway_available?
30
+ return false unless telegram_gateway_enabled?
31
+ return false if telegram_gateway_api_key.blank?
32
+
33
+ ability = telegram_gateway_adapter.check_send_ability(context.pin_code.contact)
34
+ context.telegram_gateway_request_id = ability.request_id if ability.ok?
35
+ ability.ok?
36
+ rescue TelegramGatewayAdapter::Error, Faraday::Error
37
+ false
38
+ end
39
+
40
+ def send_via_telegram_gateway
20
41
  from_number = sms_from_number
21
42
  return if from_number.blank?
22
43
 
23
- options = {
44
+ # Save the chosen channel + the Telegram request_id synchronously so
45
+ # (a) the API response already reflects the channel for the mobile UI,
46
+ # (b) verification can reach Telegram even if the user submits the OTP
47
+ # before the async send job has run. For Telegram-delivered codes
48
+ # the `code` column stores Telegram's request_id (Telegram generates
49
+ # and holds the actual OTP).
50
+ context.pin_code.update!(
51
+ delivery_channel: :telegram_gateway,
52
+ code: context.telegram_gateway_request_id
53
+ )
54
+
55
+ SpreeCmCommissioner::TelegramGateway::PinCodeSenderJob.perform_later(
56
+ pin_code_id: context.pin_code.id,
57
+ from: from_number,
58
+ request_id: context.telegram_gateway_request_id,
59
+ tenant_id: context.pin_code&.application&.tenant_id
60
+ )
61
+ end
62
+
63
+ def send_via_sms
64
+ from_number = sms_from_number
65
+ return if from_number.blank?
66
+
67
+ SpreeCmCommissioner::SmsPinCodeJob.perform_later(
68
+ pin_code_id: context.pin_code.id,
24
69
  from: from_number,
25
70
  to: context.pin_code.contact,
26
71
  body: I18n.t('pincode_sender.sms.body', code: context.pin_code.code, readable_type: context.pin_code.readable_type),
27
72
  tenant_id: context.pin_code&.application&.tenant_id
28
- }
73
+ )
74
+ end
75
+
76
+ def telegram_gateway_enabled?
77
+ ENV['TELEGRAM_GATEWAY_ENABLED'] == 'yes'
78
+ end
29
79
 
30
- SpreeCmCommissioner::SmsPinCodeJob.perform_later(options)
80
+ def telegram_gateway_api_key
81
+ @telegram_gateway_api_key ||= ENV.fetch('TELEGRAM_GATEWAY_API_KEY', nil)
82
+ end
83
+
84
+ def telegram_gateway_adapter
85
+ @telegram_gateway_adapter ||= TelegramGatewayAdapter::Client.new
31
86
  end
32
87
 
33
88
  def sms_from_number
@@ -38,6 +93,8 @@ module SpreeCmCommissioner
38
93
  end
39
94
 
40
95
  def send_email
96
+ context.pin_code.update!(delivery_channel: :email)
97
+
41
98
  SpreeCmCommissioner::PinCodeMailer.send_pin_code(
42
99
  context.pin_code.id,
43
100
  context.pin_code.readable_type,
@@ -0,0 +1,18 @@
1
+ module SpreeCmCommissioner
2
+ module TelegramGateway
3
+ # options = { pin_code_id:, from:, request_id:, tenant_id: }
4
+ class PinCodeSenderJob < SpreeCmCommissioner::SmsJob
5
+ def perform(options = {})
6
+ pin_code = SpreeCmCommissioner::PinCode.find_by(id: options[:pin_code_id])
7
+ return if pin_code.nil?
8
+
9
+ SpreeCmCommissioner::TelegramGateway::PinCodeSender.call(
10
+ pin_code: pin_code,
11
+ from: options[:from],
12
+ request_id: options[:request_id],
13
+ tenant_id: options[:tenant_id]
14
+ )
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,13 +1,28 @@
1
1
  module SpreeCmCommissioner
2
+ # Makes an open-return entitlement line item redeemable.
3
+ #
4
+ # New design: the open return is no longer a fake open-dated *trip* line item. It is a plain
5
+ # ecommerce add-on (a Spree::Product linked to the outbound trip's product via an
6
+ # `open_dated_pair` ProductRelation) sold as a regular ecommerce line item at checkout.
7
+ #
8
+ # Redemption swaps that ecommerce line item in place into a concrete transit ticket on a chosen
9
+ # return trip (see SpreeCmCommissioner::OpenDatedTrips::Redeem). Once swapped, the line item is a
10
+ # `transit` item.
2
11
  module LineItemOpenDatedTrippable
3
12
  extend ActiveSupport::Concern
4
13
 
5
14
  included do
6
15
  include SpreeCmCommissioner::StoreMetadata
7
16
 
8
- # Open dated trip metadata
9
- store_public_metadata :is_open_dated, :boolean, default: false
10
- store_public_metadata :original_trip_id, :integer # Original template trip ID BEFORE redemption
17
+ store_private_metadata :open_dated_product_id, :integer
18
+
19
+ # The endpoints (Place ids) the *return* trip must run between — already reversed from the
20
+ # outbound journey at purchase time (return origin = outbound destination, and vice versa).
21
+ # Storing the return direction means every consumer reads it directly: redemption is a plain
22
+ # id match and the search needs no flip. Endpoints rather than a trip keep this
23
+ # leg-count-independent: a multi-leg outbound still has exactly one origin and one destination.
24
+ store_private_metadata :open_dated_return_origin_place_id, :integer
25
+ store_private_metadata :open_dated_return_destination_place_id, :integer
11
26
 
12
27
  # Alias existing columns for better semantic meaning
13
28
  alias_attribute :valid_until, :to_date # Expiration date
@@ -19,19 +34,47 @@ module SpreeCmCommissioner
19
34
  accepter
20
35
  end
21
36
 
22
- # Check if ticket has been redeemed
37
+ def open_dated?
38
+ open_dated_return_origin_place_id.present? && open_dated_return_destination_place_id.present?
39
+ end
40
+
41
+ def open_dated_product
42
+ return nil if open_dated_product_id.blank?
43
+
44
+ @open_dated_product ||= Spree::Product.find_by(id: open_dated_product_id)
45
+ end
46
+
47
+ # Origin place the return trip must depart from (= outbound destination).
48
+ def open_dated_return_origin_place
49
+ return nil if open_dated_return_origin_place_id.blank?
50
+
51
+ @open_dated_return_origin_place ||= SpreeCmCommissioner::Place.find_by(id: open_dated_return_origin_place_id)
52
+ end
53
+
54
+ # Destination place the return trip must arrive at (= outbound origin).
55
+ def open_dated_return_destination_place
56
+ return nil if open_dated_return_destination_place_id.blank?
57
+
58
+ @open_dated_return_destination_place ||= SpreeCmCommissioner::Place.find_by(id: open_dated_return_destination_place_id)
59
+ end
60
+
61
+ # Human-readable return journey, e.g. "Siem Reap → Phnom Penh". Nil when endpoints are missing.
62
+ def open_dated_route_label
63
+ return nil if open_dated_return_origin_place.blank? || open_dated_return_destination_place.blank?
64
+
65
+ "#{open_dated_return_origin_place.full_path_name} → #{open_dated_return_destination_place.full_path_name}"
66
+ end
67
+
23
68
  def redeemed?
24
69
  redeemed_at.present?
25
70
  end
26
71
 
27
- # Check if ticket is expired
28
72
  def expired?
29
73
  valid_until.present? && valid_until.to_date < Date.current
30
74
  end
31
75
 
32
- # Check if ticket can be redeemed
33
76
  def can_redeem?
34
- is_open_dated? && !redeemed? && !expired?
77
+ open_dated? && !redeemed? && !expired?
35
78
  end
36
79
  end
37
80
  end
@@ -138,7 +138,6 @@ module SpreeCmCommissioner
138
138
 
139
139
  trip = SpreeCmCommissioner::Trip.find_by(id: trip_id)
140
140
  return unless trip # Skip if trip doesn't exist (will be caught by other validations)
141
- return if trip.open_dated? # Skip validation for open-dated trips
142
141
 
143
142
  # For scheduled trips, require valid trip stops
144
143
  errors.add(:boarding_trip_stop_id, 'must be greater than 0') if boarding_trip_stop_id.present? && boarding_trip_stop_id <= 0
@@ -3,8 +3,9 @@ module SpreeCmCommissioner
3
3
  has_secure_token
4
4
 
5
5
  enum contact_type: { 'phone_number' => 0, 'email' => 1, 'telegram' => 2 }
6
+ enum delivery_channel: { sms: 0, telegram_gateway: 1, email: 2 }, _prefix: :delivered_via
6
7
 
7
- validates :code, length: { maximum: 6 }
8
+ validates :code, length: { maximum: 32 }
8
9
  validates :contact, presence: true
9
10
  validates :contact, email: true, if: :email?
10
11
  validates :type, presence: true
@@ -18,6 +19,15 @@ module SpreeCmCommissioner
18
19
  PIN_CODE_MISMATCHED = 'not_match'.freeze
19
20
  PIN_CODE_OK = 'ok'.freeze
20
21
 
22
+ # Maps Telegram Gateway verification_status values to our PIN_CODE_* results.
23
+ # Anything not in this map → soft fallback to local comparison.
24
+ TELEGRAM_VERIFICATION_RESULTS = {
25
+ 'code_valid' => PIN_CODE_OK,
26
+ 'code_invalid' => PIN_CODE_MISMATCHED,
27
+ 'code_max_attempts_exceeded' => PIN_CODE_ATTEMPT_REACHED,
28
+ 'expired' => PIN_CODE_EXPIRED
29
+ }.freeze
30
+
21
31
  belongs_to :application, class_name: 'Spree::OauthApplication', optional: true
22
32
 
23
33
  def check?(code)
@@ -26,14 +36,10 @@ module SpreeCmCommissioner
26
36
 
27
37
  increment_attempt
28
38
 
29
- if self.code == code
30
- set_expire
31
- save
32
- PIN_CODE_OK
33
- else
34
- save
35
- PIN_CODE_MISMATCHED
36
- end
39
+ result = verify_code(code)
40
+ set_expire if result == PIN_CODE_OK
41
+ save
42
+ result
37
43
  end
38
44
 
39
45
  def set_expire
@@ -143,5 +149,52 @@ module SpreeCmCommissioner
143
149
  self.code = Random.new.bytes(8).bytes.join[0, 6]
144
150
  self.expires_in = expires_in_seconds
145
151
  end
152
+
153
+ # SMS codes verify internally (Plasgate has no verify API).
154
+ # Telegram-delivered codes verify with Telegram — no internal fallback,
155
+ # because `code` holds Telegram's request_id, not a comparable OTP.
156
+ # If Telegram is unreachable, we return PIN_CODE_MISMATCHED; the user
157
+ # retries from the app.
158
+ def verify_code(submitted_code)
159
+ return verify_internally(submitted_code) unless delivered_via_telegram_gateway?
160
+
161
+ verify_with_telegram(submitted_code) || PIN_CODE_MISMATCHED
162
+ end
163
+
164
+ def verify_internally(submitted_code)
165
+ code == submitted_code ? PIN_CODE_OK : PIN_CODE_MISMATCHED
166
+ end
167
+
168
+ # For Telegram-delivered PinCodes, `code` column stores Telegram's
169
+ # request_id (not the OTP itself — Telegram generated and delivered the OTP).
170
+ # Returns a PIN_CODE_* result, or nil if Telegram couldn't help us decide
171
+ # (network error, unmapped status).
172
+ def verify_with_telegram(submitted_code)
173
+ result = TelegramGatewayAdapter::Client.new.check_verification_status(code, submitted_code)
174
+ TELEGRAM_VERIFICATION_RESULTS[result.verification_status]
175
+ rescue TelegramGatewayAdapter::Error, Faraday::Error => e
176
+ log_verification_error(e)
177
+ nil
178
+ end
179
+
180
+ def log_verification_error(error)
181
+ CmAppLogger.error(
182
+ label: 'PinCode#verify_with_telegram failed',
183
+ data: {
184
+ pin_code_id: id,
185
+ request_id: code,
186
+ error_class: error.class.name,
187
+ error_message: error.message
188
+ }
189
+ )
190
+
191
+ return unless ENV['PIN_CODE_DEBUG_NOTIFIY_TELEGRAM_ENABLE'] == 'yes'
192
+
193
+ SpreeCmCommissioner::TelegramDebugPinCodeSenderJob.perform_later(
194
+ pin_code_id: id,
195
+ tenant_id: application&.tenant_id,
196
+ error_message: "Verification failed: #{error.class.name} — #{error.message}"
197
+ )
198
+ end
146
199
  end
147
200
  end
@@ -55,6 +55,12 @@ module SpreeCmCommissioner
55
55
  base.has_many :product_relations, class_name: 'SpreeCmCommissioner::ProductRelation', dependent: :destroy
56
56
  base.has_many :related_products, through: :product_relations
57
57
 
58
+ # Inverse side of product_relations: relations where this product is the related_product
59
+ # (e.g. trip add-ons point here).
60
+ base.has_many :inverse_product_relations, class_name: 'SpreeCmCommissioner::ProductRelation',
61
+ foreign_key: :related_product_id,
62
+ dependent: :destroy
63
+
58
64
  base.belongs_to :event, class_name: 'Spree::Taxon', optional: true
59
65
 
60
66
  base.has_many :preview_roles, class_name: 'SpreeCmCommissioner::PreviewRole', as: :previewable
@@ -81,10 +81,6 @@ module SpreeCmCommissioner
81
81
  :block_vpn,
82
82
  :max_votes_per_contestant_per_user
83
83
 
84
- def parent_slug
85
- parent&.slug
86
- end
87
-
88
84
  def show?
89
85
  depth == 1
90
86
  end
@@ -65,9 +65,8 @@ module SpreeCmCommissioner
65
65
  has_many :inventory_items, through: :variants
66
66
  has_many :blocks, through: :variants
67
67
 
68
- validates :departure_time, presence: true, unless: :open_dated?
69
- validates :vehicle_type, presence: true, unless: :open_dated?
70
- validates :duration, numericality: { greater_than: 0 }, unless: :open_dated?
68
+ validates :departure_time, presence: true
69
+ validates :duration, numericality: { greater_than: 0 }
71
70
 
72
71
  validate :direct_trips_cannot_have_board_to_trips, if: -> { direct? }
73
72
  validate :multi_leg_trip_stops_must_have_board_to_trip, if: -> { multi_leg? }
@@ -120,15 +119,6 @@ module SpreeCmCommissioner
120
119
  errors.add(:board_to_trips, 'direct trips cannot have board_to_trips')
121
120
  end
122
121
 
123
- def open_dated?
124
- is_open_dated == true
125
- end
126
-
127
- # Find the open dated version of this trip's product
128
- def open_dated_pair
129
- open_dated_product
130
- end
131
-
132
122
  def display_name
133
123
  product&.name
134
124
  end
@@ -8,7 +8,6 @@ module SpreeCmCommissioner
8
8
  belongs_to :advanced_to, polymorphic: true, optional: true
9
9
  belongs_to :advanced_from, polymorphic: true, optional: true
10
10
  has_many :votes, class_name: 'SpreeCmCommissioner::Vote', foreign_key: :contestant_id, dependent: :restrict_with_error
11
- has_one :leading_stat, class_name: 'SpreeCmCommissioner::VotingSessionStat', foreign_key: :leading_contestant_id, dependent: :nullify
12
11
  has_many :show_contestant_images, through: :show_contestant
13
12
  has_many :show_contestant_videos, through: :show_contestant
14
13
 
@@ -14,7 +14,7 @@
14
14
  # results = query.call # => [TripQueryResult, ...]
15
15
  #
16
16
  module SpreeCmCommissioner
17
- class SingleLegTripsQuery
17
+ class SingleLegTripsQuery # rubocop:disable Metrics/ClassLength
18
18
  attr_reader :origin_id, :destination_id, :date,
19
19
  :vendor_id, :tenant_id,
20
20
  :number_of_guests, :route_type, :options
@@ -61,8 +61,7 @@ module SpreeCmCommissioner
61
61
  .eager_load(
62
62
  vendor: :logo,
63
63
  vehicle_type: :option_values,
64
- route: {},
65
- open_dated_product: %i[trip variants]
64
+ route: {}
66
65
  )
67
66
  .select(selected_columns_sql)
68
67
  .joins(
@@ -79,6 +78,7 @@ module SpreeCmCommissioner
79
78
  LEFT JOIN spree_prices AS date_prices ON date_prices.inventory_item_id = date_iv.id AND date_prices.deleted_at IS NULL
80
79
  LEFT JOIN (#{stops_subquery_sql}) all_stops ON all_stops.trip_id = cm_trips.id
81
80
  #{return_metric_joins_sql}
81
+ #{open_dated_product_joins_sql}
82
82
  SQL
83
83
  )
84
84
  .where(<<~SQL.squish, origin: origin_id, destination: destination_id)
@@ -111,6 +111,9 @@ module SpreeCmCommissioner
111
111
  COALESCE(date_prices.compare_at_amount, prices.compare_at_amount) AS compare_at_price,
112
112
  COALESCE(date_prices.currency, prices.currency) AS currency,
113
113
  all_stops.stops AS stops,
114
+ open_dated_rel.product_id AS open_dated_product_id,
115
+ open_dated_prices.amount AS open_dated_price,
116
+ open_dated_prices.compare_at_amount AS open_dated_compare_at_price,
114
117
  #{return_columns_sql}
115
118
  SQL
116
119
  end
@@ -142,6 +145,24 @@ module SpreeCmCommissioner
142
145
  SQL
143
146
  end
144
147
 
148
+ # Joins the "open dated" pair product (if any) for the trip's product, and its master price,
149
+ # so we can expose the return add-on's price/compare_at_price without loading the full
150
+ # Spree::Product/Variant/Price records.
151
+ def open_dated_product_joins_sql
152
+ <<~SQL.squish
153
+ LEFT JOIN cm_product_relations AS open_dated_rel
154
+ ON open_dated_rel.related_product_id = cm_trips.product_id
155
+ AND open_dated_rel.relation_type = #{SpreeCmCommissioner::ProductRelation.relation_types[:open_dated_pair]}
156
+ LEFT JOIN spree_variants AS open_dated_master
157
+ ON open_dated_master.product_id = open_dated_rel.product_id
158
+ AND open_dated_master.is_master = true
159
+ LEFT JOIN spree_prices AS open_dated_prices
160
+ ON open_dated_prices.variant_id = open_dated_master.id
161
+ AND open_dated_prices.inventory_item_id IS NULL
162
+ AND open_dated_prices.deleted_at IS NULL
163
+ SQL
164
+ end
165
+
145
166
  # Subquery to aggregate all stops for each trip into a JSON array, ordered by sequence.
146
167
  # Each stop includes place coordinates (lat/lon) joined from cm_places.
147
168
  # Returns one row per trip_id with a `stops` JSON column consumed by selected_columns_sql.
@@ -14,7 +14,9 @@ module Spree
14
14
 
15
15
  base.attributes :need_confirmation, :product_type, :kyc, :kyc_fields, :allowed_upload_later, :allow_anonymous_booking,
16
16
  :use_video_as_default, :allow_transfer, :allow_gift_transfer
17
- base.attributes :reveal_description, :discontinue_on, :public_metadata, :purchasable_on, :preview
17
+
18
+ base.attributes :reveal_description, :discontinue_on, :public_metadata, :purchasable_on, :preview,
19
+ :enable_inventory_hold
18
20
 
19
21
  # Expose only the `event_id` here instead of the full event object.
20
22
  # This lets the client fetch event details separately (usually already cached),
@@ -2,7 +2,7 @@ module Spree
2
2
  module V2
3
3
  module Tenant
4
4
  class ShowEpisodeSerializer < BaseSerializer
5
- attributes :episode_number, :name, :description, :scheduled_at, :ends_at
5
+ attributes :episode_number, :name, :scheduled_at, :ends_at
6
6
 
7
7
  attribute :status do |episode|
8
8
  if episode.current?
@@ -5,11 +5,10 @@ module Spree
5
5
  set_type :show
6
6
 
7
7
  attributes :slug, :description, :permalink, :from_date, :to_date, :preview, :created_at, :updated_at,
8
- :free_vote_limit, :free_vote_limit_type, :show_type, :parent_slug
8
+ :free_vote_limit, :free_vote_limit_type, :show_type
9
9
 
10
10
  attribute :name, &:display_name
11
11
 
12
- has_many :seasons, serializer: Spree::V2::Tenant::ShowSerializer
13
12
  has_many :show_people, serializer: Spree::V2::Tenant::ShowPersonSerializer
14
13
  has_many :episodes, serializer: Spree::V2::Tenant::ShowEpisodeSerializer
15
14
  has_many :voting_sessions, serializer: Spree::V2::Tenant::VotingSessionSerializer
@@ -4,7 +4,7 @@ module SpreeCmCommissioner
4
4
  class PinCodeSerializer < BaseSerializer
5
5
  set_type :pin_code
6
6
 
7
- attribute :token, :type, :contact_type, :contact
7
+ attribute :token, :type, :contact_type, :contact, :delivery_channel
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module SpreeCmCommissioner
4
4
  class TicketTransferMinimalSerializer < BaseSerializer
5
5
  set_type :ticket_transfer
6
6
 
7
- attributes :state, :price, :platform_fee, :seller_net, :total, :currency, :token,
7
+ attributes :number, :state, :price, :platform_fee, :seller_net, :total, :currency, :token,
8
8
  :transfer_type, :settlement_status, :completed_at, :expires_at, :created_at, :updated_at
9
9
 
10
10
  belongs_to :from_user, serializer: ::Spree::V2::Storefront::UserSerializer
@@ -5,9 +5,11 @@ module SpreeCmCommissioner
5
5
  set_type :trip_query_result
6
6
 
7
7
  attributes :quantity_available, :max_capacity, :price, :compare_at_price, :currency, :display_price, :display_compare_at_price,
8
- :has_return_trip_globally, :has_return_trip_in_vendor
8
+ :has_return_trip_globally, :has_return_trip_in_vendor, :include_return_price, :include_return_compare_at_price,
9
+ :display_include_return_price, :display_include_return_compare_at_price
9
10
 
10
11
  attribute :direct, &:direct?
12
+ attribute :include_return, &:include_return?
11
13
  has_many :trips, serializer: ::SpreeCmCommissioner::V2::Storefront::TripResultSerializer
12
14
  end
13
15
  end
@@ -17,7 +17,8 @@ module SpreeCmCommissioner
17
17
 
18
18
  attributes :origin_place, :destination_place, :stops, :price, :compare_at_price, :currency, :quantity_available, :max_capacity,
19
19
  :allow_seat_selection, :departure_time, :route_type, :distance, :offset_days,
20
- :featured_until, :display_price, :display_compare_at_price
20
+ :featured_until, :display_price, :display_compare_at_price,
21
+ :open_dated_product_id, :open_dated_price, :open_dated_compare_at_price
21
22
 
22
23
  # Deprecated for backwards compatibility
23
24
  attribute :compare_at_amount, &:compare_at_price