spree_cm_commissioner 2.1.3 → 2.1.4.pre.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/api/v2/storefront/seat_layouts_controller.rb +36 -0
  4. data/app/{models/concerns → interactors}/spree_cm_commissioner/guest_seat_updater.rb +1 -1
  5. data/app/interactors/spree_cm_commissioner/trip_clone_creator.rb +1 -0
  6. data/app/interactors/spree_cm_commissioner/variant_block_updater.rb +1 -0
  7. data/app/mailers/spree_cm_commissioner/guest_mailer.rb +18 -0
  8. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +14 -0
  9. data/app/models/spree_cm_commissioner/block.rb +3 -5
  10. data/app/models/spree_cm_commissioner/check_in.rb +7 -1
  11. data/app/models/spree_cm_commissioner/check_in_rule.rb +6 -0
  12. data/app/models/spree_cm_commissioner/check_in_session.rb +16 -0
  13. data/app/models/spree_cm_commissioner/guest.rb +26 -1
  14. data/app/models/spree_cm_commissioner/import.rb +3 -1
  15. data/app/models/spree_cm_commissioner/line_item_decorator.rb +2 -1
  16. data/app/models/spree_cm_commissioner/seat_layout.rb +17 -2
  17. data/app/models/spree_cm_commissioner/seat_section.rb +1 -1
  18. data/app/models/spree_cm_commissioner/stock/line_item_availability_checker.rb +2 -2
  19. data/app/models/spree_cm_commissioner/taxon_decorator.rb +13 -0
  20. data/app/models/spree_cm_commissioner/trip.rb +37 -5
  21. data/app/models/spree_cm_commissioner/user_decorator.rb +3 -0
  22. data/app/models/spree_cm_commissioner/variant_decorator.rb +4 -3
  23. data/app/models/spree_cm_commissioner/vehicle.rb +11 -0
  24. data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +5 -0
  25. data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +3 -0
  26. data/app/serializers/spree/v2/storefront/user_serializer_decorator.rb +1 -1
  27. data/app/serializers/spree_cm_commissioner/v2/storefront/block_serializer.rb +1 -1
  28. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_serializer.rb +1 -0
  29. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_variant_serializer.rb +0 -2
  30. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_vehicle_serializer.rb +0 -2
  31. data/app/services/spree_cm_commissioner/imports/create_order_service.rb +27 -12
  32. data/app/views/spree/admin/shared/_vectors_option_type_tabs.html.erb +8 -2
  33. data/app/views/spree/admin/vectors/icons/index.html.erb +11 -7
  34. data/app/views/spree_cm_commissioner/guest_mailer/_greeting.html.erb +14 -0
  35. data/app/views/spree_cm_commissioner/guest_mailer/_mailer_stylesheets.html.erb +213 -0
  36. data/app/views/spree_cm_commissioner/guest_mailer/send_ticket_to_guest.html.erb +15 -0
  37. data/app/views/spree_cm_commissioner/layouts/guest_mailer.html.erb +35 -0
  38. data/config/locales/en.yml +2 -0
  39. data/config/locales/km.yml +2 -0
  40. data/config/routes.rb +1 -2
  41. data/db/migrate/20250827022503_create_spree_cm_commissioner_sessions.rb +14 -0
  42. data/db/migrate/20250827022757_create_spree_cm_commissioner_check_in_rules.rb +13 -0
  43. data/db/migrate/20250827032901_add_check_in_session_to_check_ins.rb +5 -0
  44. data/db/migrate/20250827083828_add_unique_index_on_guest_id_check_in_session_id_to_cm_check_ins.rb +11 -0
  45. data/db/migrate/20250903085931_add_importable_to_cm_imports.rb +5 -0
  46. data/db/migrate/20250911100738_drop_cm_variant_blocks.rb +5 -0
  47. data/db/migrate/20250911100835_add_variant_id_to_cm_blocks.rb +5 -0
  48. data/db/migrate/20250911174607_add_metadata_columns_to_cm_vehicles.rb +11 -0
  49. data/db/migrate/20250911174653_add_metadata_columns_to_cm_trips.rb +11 -0
  50. data/lib/spree_cm_commissioner/test_helper/factories/check_in_session_factory.rb +11 -0
  51. data/lib/spree_cm_commissioner/version.rb +1 -1
  52. metadata +23 -8
  53. data/app/models/spree_cm_commissioner/variant_block.rb +0 -17
  54. data/app/serializers/spree_cm_commissioner/v2/storefront/variant_block_serializer.rb +0 -9
  55. data/lib/spree_cm_commissioner/test_helper/factories/variant_block_factory.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fcd8e2ebc1971a88d96c69508d9097178ff724540850ad868b7576ba067cfaa8
4
- data.tar.gz: fc379da6272fdcad2c4c0baa626889ad62fd5c9b612a1097673b5803118ea368
3
+ metadata.gz: 53fa91ddf6ab2cbf7d0fecc25664116e5e8689f212e0db8a1319d6f37ba58f69
4
+ data.tar.gz: 3b64416fc5ecb2b21578f1a736cbffe887f264e51433d42fb28c009b72b2259c
5
5
  SHA512:
6
- metadata.gz: d83ea3dc7cc3ae0a5f75dc93e37ac9f21dc6721c0c942807ab936340c28d8de02a0d563e52a94cc1caecf3da32436972c2a48c6e599a4bcd8d42f4dd96eeac67
7
- data.tar.gz: e858334e4627f09e87470e9f3a01f36902a709612991971f5defdffd6202888e2fbfbebc162aabd46841cc3ec8105a952a4bbabcc9c412a33d9f44108bdf066d
6
+ metadata.gz: '029fc4d782f0b7e61af6675999f36905e345d4406f55098728959c05cebd26b32562c29d63af8b472dc2d98f50657b627e6ba1d8880ef81968d1611f115d8c2b'
7
+ data.tar.gz: ca652af048f6c0a6d2efd16c9a1b59d14eccbb2d7861a8403ffa5822bb32cdc8501c92277bcd894ca58e568ea04df4fd6367cd36992816e580a28e0459b4dcd0
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.1.3)
37
+ spree_cm_commissioner (2.1.4.pre.pre1)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -0,0 +1,36 @@
1
+ # This endpoint is only used for events (via `event.seat_layout_id` to call this API).
2
+ # For transit, seat layouts are included directly in the Trip API (no separate call needed).
3
+ #
4
+ # In both cases, seat availability must always be checked using:
5
+ # (on another API) /api/v2/storefront/inventory_items?includes[]=reserved_blocks
6
+ module Spree
7
+ module Api
8
+ module V2
9
+ module Storefront
10
+ class SeatLayoutsController < Spree::Api::V2::ResourceController
11
+ # override
12
+ def resource
13
+ SpreeCmCommissioner::SeatLayout.includes(
14
+ :top_level_blocks,
15
+ { seat_sections: :blocks }
16
+ ).find(params[:id])
17
+ end
18
+
19
+ # override
20
+ def default_resource_includes
21
+ [
22
+ 'top_level_blocks',
23
+ 'seat_sections',
24
+ 'seat_sections.blocks'
25
+ ]
26
+ end
27
+
28
+ # override
29
+ def resource_serializer
30
+ SpreeCmCommissioner::V2::Storefront::SeatLayoutSerializer
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -39,7 +39,7 @@ module SpreeCmCommissioner
39
39
  def block_belongs_to_variant?(block_id, variant_id)
40
40
  return false unless block_id.present? && variant_id.present?
41
41
 
42
- SpreeCmCommissioner::VariantBlock.exists?(variant_id: variant_id, block_id: block_id)
42
+ SpreeCmCommissioner::Block.exists?(variant_id: variant_id, id: block_id)
43
43
  end
44
44
  end
45
45
  end
@@ -1,3 +1,4 @@
1
+ # TODO: fix this to remove variant block related code as we already drop the table.
1
2
  module SpreeCmCommissioner
2
3
  class TripCloneCreator < BaseInteractor
3
4
  delegate :vendor, :params, :original_trip, :places, :current_store, :current_vendor, to: :context
@@ -1,3 +1,4 @@
1
+ # TODO: remove this class & its UI on our dashboard.
1
2
  module SpreeCmCommissioner
2
3
  class VariantBlockUpdater < BaseInteractor
3
4
  delegate :variant, :data, :stock_location, :current_store, to: :context
@@ -0,0 +1,18 @@
1
+ module SpreeCmCommissioner
2
+ class GuestMailer < Spree::BaseMailer
3
+ layout 'spree_cm_commissioner/layouts/guest_mailer'
4
+
5
+ def send_ticket_to_guest(guest_id, email)
6
+ @guest = SpreeCmCommissioner::Guest.find_by(id: guest_id)
7
+ @line_item = @guest.line_item
8
+ @order = @line_item.order
9
+ @product_type = @line_item.product_type || 'transit'
10
+ @trip = SpreeCmCommissioner::Trip.find_by(id: @line_item.trip_id) if @line_item.trip_id.present?
11
+ @email = email
12
+ @current_store = @order.store
13
+ subject = "#{@current_store&.name} Booking Confirmation ##{@order.number}"
14
+
15
+ mail(from: from_address, to: @email, subject: subject)
16
+ end
17
+ end
18
+ end
@@ -18,6 +18,7 @@ module SpreeCmCommissioner
18
18
 
19
19
  state_machine.around_transition to: :complete, do: :unstock_inventory!
20
20
 
21
+ state_machine.after_transition to: :complete, do: :mark_user_as_has_incomplete_guest_info, if: :has_incomplete_guest_info?
21
22
  state_machine.after_transition to: :complete, do: :send_transaction_email_to_user, if: :user_has_email?
22
23
  state_machine.after_transition to: :resumed, do: :precalculate_conversion
23
24
  state_machine.around_transition to: :resumed, do: :unstock_inventory!
@@ -73,6 +74,19 @@ module SpreeCmCommissioner
73
74
  end
74
75
  end
75
76
 
77
+ def has_incomplete_guest_info? # rubocop:disable Naming/PredicateName
78
+ line_items.any? do |line_item|
79
+ line_item.guests.any? do |guest|
80
+ !guest.post_registration_completed?
81
+ end
82
+ end
83
+ end
84
+
85
+ def mark_user_as_has_incomplete_guest_info
86
+ user&.public_metadata&.[]=('has_incomplete_guest_info', true)
87
+ user&.save!
88
+ end
89
+
76
90
  def unstock_inventory!
77
91
  ActiveRecord::Base.transaction do
78
92
  yield # Equal to block.call
@@ -5,17 +5,15 @@ module SpreeCmCommissioner
5
5
  sleeping_seat: 2,
6
6
  driver: 3,
7
7
  bathroom: 4,
8
+ text: 5,
8
9
  other: 0
9
10
  }
10
11
 
11
12
  belongs_to :seat_section, class_name: 'SpreeCmCommissioner::SeatSection', optional: true
12
13
  belongs_to :seat_layout, class_name: 'SpreeCmCommissioner::SeatLayout', optional: false
14
+ belongs_to :variant, class_name: 'Spree::Variant', optional: true
13
15
 
14
16
  validates :label, presence: true, if: :label_required?
15
-
16
- has_many :variant_blocks, class_name: 'SpreeCmCommissioner::VariantBlock', dependent: :destroy
17
- has_many :variants, class_name: 'Spree::Variant', through: :variant_blocks
18
-
19
17
  validates :width, presence: true, numericality: { greater_than: 0 }
20
18
  validates :height, presence: true, numericality: { greater_than: 0 }
21
19
  validates :x, presence: true, numericality: true
@@ -25,7 +23,7 @@ module SpreeCmCommissioner
25
23
  before_validation :assign_layout_from_section, if: -> { seat_layout.nil? && seat_section.present? }
26
24
 
27
25
  def label_required?
28
- seatable? || block_type.to_s == 'other'
26
+ seatable? || %w[other text].include?(block_type.to_s)
29
27
  end
30
28
 
31
29
  def self.seatable?(block_type)
@@ -12,6 +12,7 @@ module SpreeCmCommissioner
12
12
  has_one :line_item, class_name: 'Spree::LineItem', through: :guest
13
13
 
14
14
  belongs_to :checkinable, polymorphic: true
15
+ belongs_to :check_in_session, class_name: 'SpreeCmCommissioner::CheckInSession', optional: true
15
16
  before_validation :set_entry_type, on: :create
16
17
 
17
18
  accepts_nested_attributes_for :guest, allow_destroy: true
@@ -20,6 +21,11 @@ module SpreeCmCommissioner
20
21
  self.entry_type = guest.entry_type
21
22
  end
22
23
 
23
- validates :guest_id, uniqueness: true
24
+ validates :guest_id,
25
+ uniqueness: {
26
+ scope: :check_in_session_id,
27
+ message: I18n.t('spree.check_in.already_checked_in')
28
+ },
29
+ if: -> { check_in_session_id.present? }
24
30
  end
25
31
  end
@@ -0,0 +1,6 @@
1
+ module SpreeCmCommissioner
2
+ class CheckInRule < SpreeCmCommissioner::Base
3
+ belongs_to :check_in_session, class_name: 'SpreeCmCommissioner::CheckInSession'
4
+ belongs_to :ruleable, polymorphic: true
5
+ end
6
+ end
@@ -0,0 +1,16 @@
1
+ module SpreeCmCommissioner
2
+ class CheckInSession < SpreeCmCommissioner::Base
3
+ belongs_to :event, class_name: 'Spree::Taxon', optional: false
4
+ has_many :check_in_rules, class_name: 'SpreeCmCommissioner::CheckInRule', dependent: :destroy
5
+
6
+ accepts_nested_attributes_for :check_in_rules, allow_destroy: true
7
+
8
+ before_create :set_position
9
+
10
+ private
11
+
12
+ def set_position
13
+ self.position ||= event.check_in_sessions.count + 1
14
+ end
15
+ end
16
+ end
@@ -255,7 +255,7 @@ module SpreeCmCommissioner
255
255
  def validate_block
256
256
  return if block_id.blank?
257
257
  return if line_item.blank? || line_item.variant.blank?
258
- return if line_item.variant.variant_blocks.exists?(block_id: block_id)
258
+ return if line_item.variant.blocks.exists?(id: block_id)
259
259
 
260
260
  errors.add(:block, "does not exist in variant #{line_item.variant_id}")
261
261
  end
@@ -356,6 +356,31 @@ module SpreeCmCommissioner
356
356
  line_item&.product&.dynamic_fields&.during_check_in&.any?
357
357
  end
358
358
 
359
+ def cm_check_in_rules
360
+ SpreeCmCommissioner::CheckInRule.where(ruleable_id: line_item.product_id)
361
+ end
362
+
363
+ def eligible_check_in_sessions
364
+ check_in_session_ids = []
365
+
366
+ cm_check_in_rules.includes(:check_in_session).find_each do |rule|
367
+ case rule.ruleable_type
368
+ when 'Spree::Product'
369
+ check_in_session_ids << rule.check_in_session_id if line_item.product_id == rule.ruleable_id
370
+ when 'Spree::Variant'
371
+ check_in_session_ids << rule.check_in_session_id if line_item.variant_id == rule.ruleable_id
372
+ when 'Spree::OptionValue'
373
+ check_in_session_ids << rule.check_in_session_id if line_item.variant.option_value_ids.include?(rule.ruleable_id)
374
+ end
375
+ end
376
+
377
+ SpreeCmCommissioner::CheckInSession.where(id: check_in_session_ids.uniq)
378
+ end
379
+
380
+ def can_check_in?(session)
381
+ eligible_check_in_sessions.include?(session)
382
+ end
383
+
359
384
  private
360
385
 
361
386
  def product_dynamic_fields
@@ -2,8 +2,10 @@ module SpreeCmCommissioner
2
2
  class Import < Base
3
3
  extend FriendlyId
4
4
 
5
- enum status: { :queue => 0, :progress => 1, :done => 2, :failed => 3 }
5
+ enum :status, { :queue => 0, :progress => 1, :done => 2, :failed => 3 }
6
6
  has_one_attached :imported_file
7
7
  friendly_id :name, use: :slugged
8
+
9
+ belongs_to :importable, polymorphic: true
8
10
  end
9
11
  end
@@ -22,10 +22,11 @@ module SpreeCmCommissioner
22
22
  base.has_many :pending_guests, pending_guests_query, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
23
23
  base.has_many :product_completion_steps, class_name: 'SpreeCmCommissioner::ProductCompletionStep', through: :product
24
24
 
25
+ base.before_validation -> { self.product_type = variant.product_type || product.product_type }, if: -> { product_type.nil? }
26
+
25
27
  base.before_save :update_vendor_id
26
28
 
27
29
  base.before_create :add_due_date, if: :subscription?
28
- base.before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
29
30
 
30
31
  base.validate :ensure_not_exceed_max_quantity_per_order, if: -> { variant&.max_quantity_per_order.present? }
31
32
 
@@ -2,8 +2,9 @@ module SpreeCmCommissioner
2
2
  class SeatLayout < Base
3
3
  has_many :seat_sections, class_name: 'SpreeCmCommissioner::SeatSection', dependent: :destroy
4
4
 
5
+ has_many :blocks, class_name: 'SpreeCmCommissioner::Block'
5
6
  has_many :top_level_blocks, -> { where(seat_section_id: nil) }, class_name: 'SpreeCmCommissioner::Block', dependent: :destroy
6
- has_many :section_blocks, -> { where.not(seat_section_id: nil) }, class_name: 'SpreeCmCommissioner::Block'
7
+ has_many :section_blocks, -> { where.not(seat_section_id: nil) }, class_name: 'SpreeCmCommissioner::Block' # destroy is handled by seat_section
7
8
 
8
9
  belongs_to :layoutable, polymorphic: true
9
10
 
@@ -15,6 +16,20 @@ module SpreeCmCommissioner
15
16
 
16
17
  validates :name, presence: true
17
18
 
18
- accepts_nested_attributes_for :seat_sections, :top_level_blocks
19
+ accepts_nested_attributes_for :seat_sections, :top_level_blocks, allow_destroy: true
20
+
21
+ after_commit :sync_seat_layout_id_to_layoutable!, if: -> { layoutable.present? }
22
+
23
+ private
24
+
25
+ # layoutable (taxon, vehicle) must have seat_layout_id attribute,
26
+ # either as a column or as a key in a JSONB column (e.g., metadata)
27
+ def sync_seat_layout_id_to_layoutable!
28
+ if destroyed?
29
+ layoutable.update!(seat_layout_id: nil)
30
+ else
31
+ layoutable.update!(seat_layout_id: id)
32
+ end
33
+ end
19
34
  end
20
35
  end
@@ -11,6 +11,6 @@ module SpreeCmCommissioner
11
11
  validates :y, presence: true, numericality: true
12
12
  validates :rotation, presence: true, numericality: { greater_than_or_equal_to: -360, less_than_or_equal_to: 360 }
13
13
 
14
- accepts_nested_attributes_for :blocks
14
+ accepts_nested_attributes_for :blocks, allow_destroy: true
15
15
  end
16
16
  end
@@ -14,8 +14,8 @@ module SpreeCmCommissioner
14
14
 
15
15
  def options
16
16
  {
17
- from_date: line_item.from_date,
18
- to_date: line_item.to_date
17
+ from_date: line_item.date_range.first,
18
+ to_date: line_item.date_range.last
19
19
  }
20
20
  end
21
21
  end
@@ -8,6 +8,14 @@ module SpreeCmCommissioner
8
8
  base.preference :background_color, :string
9
9
  base.preference :foreground_color, :string
10
10
 
11
+ # seat_layout_id:
12
+ # This model has no seat_layout column (polymorphic association), so we store the seat_layout ID in public_metadata.
13
+ # This lets us check if a seat layout exists without triggering a database query.
14
+ # The ID is automatically updated whenever the seat_layout is saved.
15
+ base.store :public_metadata, accessors: %i[
16
+ seat_layout_id
17
+ ], coder: JSON
18
+
11
19
  base.has_many :taxon_vendors, class_name: 'SpreeCmCommissioner::TaxonVendor'
12
20
  base.has_many :vendors, through: :taxon_vendors
13
21
 
@@ -23,6 +31,7 @@ module SpreeCmCommissioner
23
31
  base.has_many :check_ins, as: :checkinable, class_name: 'SpreeCmCommissioner::CheckIn', dependent: :nullify
24
32
  base.has_many :customer_taxons, class_name: 'SpreeCmCommissioner::CustomerTaxon'
25
33
 
34
+ base.has_one :seat_layout, as: :layoutable, class_name: 'SpreeCmCommissioner::SeatLayout', dependent: :destroy
26
35
  base.has_one :category_icon, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::TaxonCategoryIcon'
27
36
 
28
37
  base.has_one :web_banner, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::TaxonWebBanner'
@@ -64,6 +73,7 @@ module SpreeCmCommissioner
64
73
  base.has_many :line_items, through: :products
65
74
  base.has_many :event_line_items, class_name: 'Spree::LineItem', foreign_key: :event_id
66
75
  base.has_many :event_products, class_name: 'Spree::Product', foreign_key: :event_id
76
+ base.has_many :event_variants, through: :event_products, class_name: 'Spree::Variant', source: :variants
67
77
 
68
78
  base.has_many :event_blazer_queries, as: :queryable, class_name: 'SpreeCmCommissioner::BlazerQueryable'
69
79
  base.has_many :blazer_queries, through: :event_blazer_queries, source: :blazer_query, class_name: 'Blazer::Query'
@@ -75,6 +85,9 @@ module SpreeCmCommissioner
75
85
 
76
86
  base.has_many :taxon_places, class_name: 'SpreeCmCommissioner::TaxonPlace', dependent: :destroy
77
87
  base.has_many :places, through: :taxon_places
88
+ base.has_many :check_in_sessions, class_name: 'SpreeCmCommissioner::CheckInSession', foreign_key: :event_id, dependent: :destroy
89
+
90
+ base.has_many :import_orders, as: :importable, class_name: 'SpreeCmCommissioner::Imports::ImportOrder', dependent: :destroy
78
91
 
79
92
  def base.active_homepage_events
80
93
  joins(:homepage_section_relatables)
@@ -1,15 +1,25 @@
1
1
  module SpreeCmCommissioner
2
2
  class Trip < Base
3
+ include ::Spree::Metadata
3
4
  include SpreeCmCommissioner::RouteType
4
5
 
5
6
  attr_accessor :hours, :minutes, :seconds
6
7
 
8
+ # seat_layout_id:
9
+ # This model has no seat_layout column (polymorphic association), so we store the seat_layout ID in public_metadata.
10
+ # This lets us check if a seat layout exists without triggering a database query.
11
+ # The ID is automatically updated whenever the seat_layout is saved.
12
+ store :public_metadata, accessors: %i[
13
+ seat_layout_id
14
+ ], coder: JSON
15
+
7
16
  before_validation :convert_duration_to_seconds
8
17
 
9
18
  belongs_to :vendor, class_name: 'Spree::Vendor', inverse_of: :trips, optional: false
10
19
  belongs_to :product, class_name: 'Spree::Product', inverse_of: :trip, optional: false
11
20
 
12
21
  belongs_to :vehicle, class_name: 'SpreeCmCommissioner::Vehicle', optional: false
22
+ has_one :seat_layout, as: :layoutable, class_name: 'SpreeCmCommissioner::SeatLayout', dependent: :destroy
13
23
 
14
24
  belongs_to :origin_place, class_name: 'SpreeCmCommissioner::Place', optional: false
15
25
  belongs_to :destination_place, class_name: 'SpreeCmCommissioner::Place', optional: false
@@ -23,14 +33,16 @@ module SpreeCmCommissioner
23
33
 
24
34
  has_many :variants, through: :product
25
35
  has_many :inventory_items, through: :variants
26
- has_many :variant_blocks, through: :variants
27
- has_many :blocks, through: :variant_blocks
36
+ has_many :blocks, through: :variants
28
37
 
29
38
  validates :departure_time, presence: true
30
39
  validates :duration, numericality: { greater_than: 0 }
31
40
  validate :origin_and_destination_cannot_be_the_same
32
41
 
33
- accepts_nested_attributes_for :trip_stops, :product, allow_destroy: true
42
+ # before create, duplicate seat layout from vehicle if present
43
+ before_validation :duplicate_seat_layout_from_vehicle, if: -> { seat_layout.nil? && vehicle.present? && vehicle.seat_layout.present? }
44
+
45
+ accepts_nested_attributes_for :trip_stops, :product, :seat_layout, allow_destroy: true
34
46
 
35
47
  self.whitelisted_ransackable_associations = %w[product vehicle]
36
48
  self.whitelisted_ransackable_attributes = %w[origin_place_id destination_place_id]
@@ -56,12 +68,32 @@ module SpreeCmCommissioner
56
68
  departure_time + duration.seconds
57
69
  end
58
70
 
59
- private
60
-
61
71
  def origin_and_destination_cannot_be_the_same
62
72
  return unless origin_place_id == destination_place_id
63
73
 
64
74
  errors.add(:base, 'Origin and destination cannot be the same')
65
75
  end
76
+
77
+ private
78
+
79
+ def duplicate_seat_layout_from_vehicle
80
+ original = vehicle.seat_layout
81
+
82
+ self.seat_layout_attributes =
83
+ original.attributes.except('id', 'created_at', 'updated_at', 'layoutable_id', 'layoutable_type')
84
+ .merge(
85
+ seat_sections_attributes: original.seat_sections.map do |section|
86
+ section.attributes.except('id', 'created_at', 'updated_at', 'seat_layout_id')
87
+ .merge(
88
+ blocks_attributes: section.blocks.map do |block|
89
+ block.attributes.except('id', 'created_at', 'updated_at', 'seat_layout_id', 'seat_section_id')
90
+ end
91
+ )
92
+ end,
93
+ top_level_blocks_attributes: original.top_level_blocks.map do |block|
94
+ block.attributes.except('id', 'created_at', 'updated_at', 'seat_layout_id', 'seat_section_id')
95
+ end
96
+ )
97
+ end
66
98
  end
67
99
  end
@@ -42,6 +42,9 @@ module SpreeCmCommissioner
42
42
  base.attr_accessor :assigned_roles
43
43
  base.validates_password_strength :password, if: -> { requires_strong_password? && password.present? }
44
44
 
45
+ # Store has_incomplete_guest_info in public_metadata for easy frontend access
46
+ base.store :public_metadata, accessors: [:has_incomplete_guest_info], coder: JSON
47
+
45
48
  define_user_places(base)
46
49
 
47
50
  def base.end_users
@@ -12,6 +12,8 @@ module SpreeCmCommissioner
12
12
 
13
13
  base.belongs_to :vendor, class_name: 'Spree::Vendor'
14
14
  base.has_one :event, class_name: 'Spree::Taxon', through: :product
15
+ base.has_one :trip, class_name: 'SpreeCmCommissioner::Trip', through: :product
16
+ base.has_one :vehicle, class_name: 'SpreeCmCommissioner::Vehicle', through: :trip
15
17
 
16
18
  base.has_many :taxons, class_name: 'Spree::Taxon', through: :product
17
19
  base.has_many :visible_option_values, lambda {
@@ -21,8 +23,7 @@ module SpreeCmCommissioner
21
23
  base.has_many :video_on_demands, class_name: 'SpreeCmCommissioner::VideoOnDemand', dependent: :destroy
22
24
  base.has_many :complete_line_items, -> { complete }, class_name: 'Spree::LineItem'
23
25
 
24
- base.has_many :variant_blocks, class_name: 'SpreeCmCommissioner::VariantBlock', dependent: :destroy
25
- base.has_many :blocks, class_name: 'SpreeCmCommissioner::Block', through: :variant_blocks
26
+ base.has_many :blocks, class_name: 'SpreeCmCommissioner::Block', dependent: :nullify
26
27
 
27
28
  base.has_many :variant_guest_card_class, class_name: 'SpreeCmCommissioner::VariantGuestCardClass'
28
29
  base.has_many :guest_card_classes, class_name: 'SpreeCmCommissioner::GuestCardClass', through: :variant_guest_card_class
@@ -35,7 +36,7 @@ module SpreeCmCommissioner
35
36
 
36
37
  base.accepts_nested_attributes_for :option_values
37
38
 
38
- base.before_save -> { self.product_type = product.product_type }, if: -> { product_type.nil? }
39
+ base.before_validation -> { self.product_type = product.product_type }, if: -> { product_type.nil? }
39
40
  end
40
41
 
41
42
  def delivery_required?
@@ -1,7 +1,16 @@
1
1
  module SpreeCmCommissioner
2
2
  class Vehicle < Base
3
+ include ::Spree::Metadata
3
4
  include SpreeCmCommissioner::VehicleType
4
5
 
6
+ # seat_layout_id:
7
+ # This model has no seat_layout column (polymorphic association), so we store the seat_layout ID in public_metadata.
8
+ # This lets us check if a seat layout exists without triggering a database query.
9
+ # The ID is automatically updated whenever the seat_layout is saved.
10
+ store :public_metadata, accessors: %i[
11
+ seat_layout_id
12
+ ], coder: JSON
13
+
5
14
  belongs_to :vendor, class_name: 'Spree::Vendor'
6
15
 
7
16
  has_one :primary_photo, -> { order(position: :asc) }, class_name: 'SpreeCmCommissioner::VehiclePhoto', as: :viewable, dependent: :destroy
@@ -14,6 +23,8 @@ module SpreeCmCommissioner
14
23
  has_many :option_values, through: :option_value_vehicles, class_name: 'Spree::OptionValue'
15
24
  has_many :amenities, through: :option_value_vehicles, source: :option_value
16
25
 
26
+ has_many :trips, class_name: 'SpreeCmCommissioner::Trip', dependent: :nullify
27
+
17
28
  validates :code, uniqueness: { scope: :vendor_id }, presence: true
18
29
  validates :license_plate, uniqueness: true, allow_blank: true
19
30
 
@@ -15,6 +15,11 @@ module Spree
15
15
  base.attributes :need_confirmation, :product_type, :kyc, :kyc_fields, :allowed_upload_later, :allow_anonymous_booking, :use_video_as_default
16
16
  base.attributes :reveal_description, :discontinue_on, :public_metadata, :purchasable_on
17
17
 
18
+ # Expose only the `event_id` here instead of the full event object.
19
+ # This lets the client fetch event details separately (usually already cached),
20
+ # avoids extra DB queries from a `has_one :event` association, and keeps this response lightweight.
21
+ base.attributes :event_id
22
+
18
23
  base.attribute :purchasable_on_app do |product|
19
24
  product.purchasable_on == 'app' || product.purchasable_on == 'both'
20
25
  end
@@ -17,6 +17,9 @@ module Spree
17
17
  :background_color, :foreground_color, :show_badge_status,
18
18
  :purchasable_on, :vendor_id, :available_on, :hide_video_banner
19
19
 
20
+ # To get full seat layout details, call the seat_layouts API with this ID.
21
+ base.attributes :seat_layout_id
22
+
20
23
  base.attribute :purchasable_on_app do |taxon|
21
24
  taxon.purchasable_on == 'app' || taxon.purchasable_on == 'both'
22
25
  end
@@ -5,7 +5,7 @@ module Spree
5
5
  def self.prepended(base)
6
6
  base.attributes :first_name, :last_name, :gender, :phone_number, :intel_phone_number,
7
7
  :country_code, :otp_enabled, :otp_email, :otp_phone_number,
8
- :confirm_pin_code_enabled, :tenant_id
8
+ :confirm_pin_code_enabled, :tenant_id, :has_incomplete_guest_info
9
9
 
10
10
  base.has_one :profile, serializer: ::Spree::V2::Storefront::UserProfileSerializer
11
11
  base.has_many :device_tokens, serializer: Spree::V2::Storefront::UserDeviceTokenSerializer
@@ -2,7 +2,7 @@ module SpreeCmCommissioner
2
2
  module V2
3
3
  module Storefront
4
4
  class BlockSerializer < BaseSerializer
5
- attributes :label, :width, :height, :x, :y, :rotation, :block_type
5
+ attributes :label, :width, :height, :x, :y, :rotation, :block_type, :variant_id
6
6
 
7
7
  attribute :seatable, &:seatable?
8
8
  end
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
5
5
  attributes :departure_time, :arrival_time, :duration_in_hms, :route_type
6
6
 
7
7
  has_one :vehicle, serializer: ::SpreeCmCommissioner::V2::Storefront::TripVehicleSerializer
8
+ has_one :seat_layout, serializer: SpreeCmCommissioner::V2::Storefront::SeatLayoutSerializer
8
9
 
9
10
  has_one :origin_place, serializer: ::SpreeCmCommissioner::V2::Storefront::TripPlaceSerializer
10
11
  has_one :destination_place, serializer: ::SpreeCmCommissioner::V2::Storefront::TripPlaceSerializer
@@ -4,8 +4,6 @@ module SpreeCmCommissioner
4
4
  class TripVariantSerializer < BaseSerializer
5
5
  attributes :price, :display_price, :compare_at_price, :display_compare_at_price, :currency,
6
6
  :number_of_guests, :seat_type
7
-
8
- has_many :variant_blocks, serializer: ::SpreeCmCommissioner::V2::Storefront::VariantBlockSerializer
9
7
  end
10
8
  end
11
9
  end
@@ -8,8 +8,6 @@ module SpreeCmCommissioner
8
8
 
9
9
  has_many :vehicle_photos, serializer: ::SpreeCmCommissioner::V2::Storefront::AssetSerializer
10
10
  has_many :amenities, serializer: ::SpreeCmCommissioner::V2::Storefront::AmenitySerializer
11
-
12
- has_one :seat_layout, serializer: SpreeCmCommissioner::V2::Storefront::SeatLayoutSerializer
13
11
  end
14
12
  end
15
13
  end
@@ -31,18 +31,33 @@ module SpreeCmCommissioner
31
31
 
32
32
  def build_order_params(order_data)
33
33
  guest_attributes = order_data.slice(*SpreeCmCommissioner::Guest.csv_importable_columns)
34
- {
35
- channel: construct_channel(order_data[:order_channel]),
36
- email: order_data[:email],
37
- phone_number: order_data[:phone_number],
38
- line_items_attributes: [
39
- {
40
- quantity: order_data[:quantity].to_i,
41
- sku: order_data[:variant_sku],
42
- guests_attributes: Array.new(order_data[:quantity].to_i, guest_attributes)
43
- }
44
- ]
45
- }
34
+ if order_data[:variant_id].present?
35
+ {
36
+ channel: construct_channel('google_form'),
37
+ email: order_data[:email],
38
+ phone_number: order_data[:phone_number],
39
+ line_items_attributes: [
40
+ {
41
+ quantity: 1,
42
+ variant_id: order_data[:variant_id].to_i,
43
+ guests_attributes: Array.new(1, guest_attributes)
44
+ }
45
+ ]
46
+ }
47
+ else
48
+ {
49
+ channel: construct_channel(order_data[:order_channel]),
50
+ email: order_data[:email],
51
+ phone_number: order_data[:phone_number],
52
+ line_items_attributes: [
53
+ {
54
+ quantity: order_data[:quantity].to_i,
55
+ sku: order_data[:variant_sku],
56
+ guests_attributes: Array.new(order_data[:quantity].to_i, guest_attributes)
57
+ }
58
+ ]
59
+ }
60
+ end
46
61
  end
47
62
 
48
63
  def create_order(params, index)