spree_cm_commissioner 1.10.0.pre.pre1 → 1.11.0.pre.pre

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test_and_build_gem.yml +131 -100
  3. data/.vscode/settings.json +1 -1
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +33 -4
  6. data/app/controllers/spree/admin/stock_managements_controller.rb +39 -0
  7. data/app/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +2 -2
  8. data/app/interactors/spree_cm_commissioner/create_event.rb +23 -0
  9. data/app/interactors/spree_cm_commissioner/ensure_correct_product_type.rb +40 -0
  10. data/app/interactors/spree_cm_commissioner/stock/inventory_items_adjuster.rb +13 -0
  11. data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +6 -2
  12. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +24 -0
  13. data/app/interactors/spree_cm_commissioner/vattanac_bank_initiator.rb +2 -2
  14. data/app/jobs/spree_cm_commissioner/ensure_correct_product_type_job.rb +7 -0
  15. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +11 -0
  16. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +3 -2
  17. data/app/models/concerns/spree_cm_commissioner/product_delegation.rb +1 -3
  18. data/app/models/spree_cm_commissioner/inventory_item.rb +21 -3
  19. data/app/models/spree_cm_commissioner/line_item_decorator.rb +16 -6
  20. data/app/models/spree_cm_commissioner/place.rb +11 -2
  21. data/app/models/spree_cm_commissioner/product_decorator.rb +8 -2
  22. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +14 -2
  23. data/app/models/spree_cm_commissioner/redis_stock/line_items_cached_inventory_items_builder.rb +2 -8
  24. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +18 -0
  25. data/app/models/spree_cm_commissioner/taxon_decorator.rb +11 -0
  26. data/app/models/spree_cm_commissioner/taxon_option_type.rb +8 -0
  27. data/app/models/spree_cm_commissioner/taxon_option_value.rb +8 -0
  28. data/app/models/spree_cm_commissioner/trip.rb +0 -11
  29. data/app/models/spree_cm_commissioner/trip_connection.rb +1 -0
  30. data/app/models/spree_cm_commissioner/trip_stop.rb +12 -5
  31. data/app/models/spree_cm_commissioner/variant_decorator.rb +19 -11
  32. data/app/models/spree_cm_commissioner/variant_options.rb +0 -14
  33. data/app/models/spree_cm_commissioner/vendor_stop.rb +2 -1
  34. data/app/queries/spree_cm_commissioner/trip_query.rb +11 -11
  35. data/app/queries/spree_cm_commissioner/vendor_stop_place_query.rb +54 -0
  36. data/app/serializers/spree/v2/tenant/guest_serializer.rb +1 -0
  37. data/app/services/spree_cm_commissioner/organizer/export_guest_csv_service.rb +2 -0
  38. data/app/views/spree/admin/stock_managements/_events_popover.html.erb +12 -6
  39. data/app/views/spree/admin/stock_managements/_variant_stock_items.html.erb +3 -1
  40. data/app/views/spree/admin/stock_managements/calendar.html.erb +6 -3
  41. data/app/views/spree/admin/stock_managements/index.html.erb +9 -0
  42. data/config/initializers/spree_permitted_attributes.rb +5 -0
  43. data/db/migrate/20250418072528_add_nested_set_columns_to_places.rb +10 -0
  44. data/db/migrate/20250430091742_create_cm_taxon_option_types.rb +9 -0
  45. data/db/migrate/20250430092928_create_cm_taxon_option_values.rb +9 -0
  46. data/db/migrate/20250502025848_add_index_to_spree_products.rb +5 -0
  47. data/db/migrate/20250502030001_add_product_type_to_spree_variants.rb +5 -0
  48. data/db/migrate/20250502030002_add_product_type_to_spree_line_items.rb +5 -0
  49. data/db/migrate/20250506092929_add_trip_count_to_cm_vendor_stops.rb +5 -0
  50. data/lib/generators/spree_cm_commissioner/install/install_generator.rb +11 -3
  51. data/lib/spree_cm_commissioner/test_helper/factories/homepage_section_relatable_factory.rb +1 -1
  52. data/lib/spree_cm_commissioner/test_helper/factories/inventory_item_factory.rb +1 -1
  53. data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +1 -1
  54. data/lib/spree_cm_commissioner/test_helper/factories/place_factory.rb +11 -1
  55. data/lib/spree_cm_commissioner/test_helper/factories/product_factory.rb +18 -5
  56. data/lib/spree_cm_commissioner/test_helper/factories/stock_location_factory.rb +2 -2
  57. data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +12 -1
  58. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +1 -1
  59. data/lib/spree_cm_commissioner/version.rb +1 -1
  60. data/lib/tasks/ensure_correct_product_type.rake +7 -0
  61. data/lib/tasks/migrate_and_rebuild_place_hierarchy.rake +9 -0
  62. data/lib/tasks/update_orphan_root_places.rake +7 -0
  63. metadata +20 -3
  64. data/app/models/spree_cm_commissioner/stock_movement_decorator.rb +0 -34
@@ -8,19 +8,37 @@ module SpreeCmCommissioner
8
8
  # Validation
9
9
  validates :quantity_available, numericality: { greater_than_or_equal_to: 0 }
10
10
  validates :max_capacity, numericality: { greater_than_or_equal_to: 0 } # Originally inventory of each variant.
11
- validates :inventory_date, presence: true, if: -> { permanent_stock? }
11
+ validates :inventory_date, presence: true, if: :permanent_stock?
12
12
  validates :variant_id, uniqueness: { scope: :inventory_date, message: -> (object, _data) { "The variant is taken on #{object.inventory_date}" } }
13
13
 
14
14
  # Scope
15
- scope :for_product, -> (type) { where(product_type: type) }
16
15
  scope :active, -> { where(inventory_date: nil).or(where('inventory_date >= ?', Time.zone.today)) }
17
16
 
17
+ before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
18
+
18
19
  def adjust_quantity!(quantity)
19
20
  with_lock do
20
21
  self.max_capacity = max_capacity + quantity
21
22
  self.quantity_available = quantity_available + quantity
22
-
23
23
  save!
24
+
25
+ # When user has been searched or booked a product, it has cached the quantity in redis,
26
+ # So we need to update redis cache if inventory key has been created in redis
27
+ adjust_quantity_in_redis(quantity)
28
+ end
29
+ end
30
+
31
+ def adjust_quantity_in_redis(quantity)
32
+ SpreeCmCommissioner.redis_pool.with do |redis|
33
+ cached_quantity_available = redis.get("inventory:#{id}")
34
+ # ignore if redis doesn't exist
35
+ return if cached_quantity_available.nil? # rubocop:disable Lint/NonLocalExitFromIterator
36
+
37
+ if quantity.positive?
38
+ redis.incrby("inventory:#{id}", quantity)
39
+ else
40
+ redis.decrby("inventory:#{id}", quantity.abs)
41
+ end
24
42
  end
25
43
  end
26
44
 
@@ -1,6 +1,6 @@
1
1
  module SpreeCmCommissioner
2
2
  module LineItemDecorator
3
- def self.prepended(base) # rubocop:disable Metrics/MethodLength
3
+ def self.prepended(base) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
4
4
  include_modules(base)
5
5
 
6
6
  base.belongs_to :accepter, class_name: 'Spree::User', optional: true
@@ -9,7 +9,11 @@ module SpreeCmCommissioner
9
9
  base.has_one :google_wallet, class_name: 'SpreeCmCommissioner::GoogleWallet', through: :product
10
10
 
11
11
  base.has_many :option_types, through: :product
12
- base.has_many :inventory_items, through: :variant
12
+
13
+ base.has_many :inventory_items, lambda { |line_item|
14
+ where(inventory_date: nil).or(where(inventory_date: line_item.date_range))
15
+ }, through: :variant
16
+
13
17
  base.has_many :taxons, class_name: 'Spree::Taxon', through: :product
14
18
  base.has_many :guests, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
15
19
  base.has_many :pending_guests, pending_guests_query, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
@@ -22,16 +26,15 @@ module SpreeCmCommissioner
22
26
  base.validate :validate_seats_reservation, if: :transit?
23
27
 
24
28
  base.before_create :add_due_date, if: :subscription?
29
+ base.before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
25
30
 
26
31
  base.validate :ensure_not_exceed_max_quantity_per_order, if: -> { variant&.max_quantity_per_order.present? }
27
32
 
28
33
  base.whitelisted_ransackable_associations |= %w[guests order]
29
34
  base.whitelisted_ransackable_attributes |= %w[number to_date from_date vendor_id]
30
35
 
31
- base.delegate :delivery_required?, :permanent_stock?, :high_demand, :transit?,
36
+ base.delegate :delivery_required?, :high_demand,
32
37
  to: :variant
33
- base.delegate :discontinue_on, :product_type, :accommodation?, :service?, :ecommerce?, :need_confirmation,
34
- to: :product
35
38
 
36
39
  base.accepts_nested_attributes_for :guests, allow_destroy: true
37
40
  base.accepts_nested_attributes_for :line_item_seats, allow_destroy: true
@@ -42,6 +45,10 @@ module SpreeCmCommissioner
42
45
  json_api_columns << :vendor_id
43
46
  end
44
47
 
48
+ def discontinue_on
49
+ variant.discontinue_on || product.discontinue_on
50
+ end
51
+
45
52
  def base.search_by_qr_data!(data)
46
53
  matches = data.match(/(R\d+)-([A-Za-z0-9_\-]+)-(L\d+)/)&.captures
47
54
 
@@ -60,6 +67,7 @@ module SpreeCmCommissioner
60
67
  base.include SpreeCmCommissioner::LineItemDurationable
61
68
  base.include SpreeCmCommissioner::LineItemsFilterScope
62
69
  base.include SpreeCmCommissioner::LineItemGuestsConcern
70
+ base.include SpreeCmCommissioner::ProductType
63
71
  base.include SpreeCmCommissioner::ProductDelegation
64
72
  base.include SpreeCmCommissioner::KycBitwise
65
73
  end
@@ -175,7 +183,7 @@ module SpreeCmCommissioner
175
183
 
176
184
  # override
177
185
  def sufficient_stock?
178
- return transit_sufficient_stock? if variant.product.product_type == 'transit'
186
+ return transit_sufficient_stock? if transit?
179
187
 
180
188
  SpreeCmCommissioner::Stock::LineItemAvailabilityChecker.new(self).can_supply?(quantity)
181
189
  end
@@ -231,6 +239,8 @@ module SpreeCmCommissioner
231
239
  end
232
240
 
233
241
  def validate_seats_reservation
242
+ return if reservation_trip.blank?
243
+
234
244
  if reservation_trip.allow_seat_selection && !selected_seats_available?
235
245
  errors.add(:base, :some_seats_are_booked, message: 'Some seats are already booked')
236
246
  elsif !reservation_trip.allow_seat_selection && !seat_quantity_available?(reservation_trip)
@@ -2,6 +2,8 @@ require_dependency 'spree_cm_commissioner'
2
2
 
3
3
  module SpreeCmCommissioner
4
4
  class Place < ApplicationRecord
5
+ acts_as_nested_set
6
+
5
7
  validates :reference, presence: true, if: :validate_reference?
6
8
  validates :lat, presence: true, if: :validate_lat?
7
9
  validates :lon, presence: true, if: :validate_lon?
@@ -13,15 +15,22 @@ module SpreeCmCommissioner
13
15
 
14
16
  has_many :product_places, class_name: 'SpreeCmCommissioner::ProductPlace', dependent: :destroy
15
17
  has_many :products, through: :product_places
16
- has_many :children, class_name: 'SpreeCmCommissioner::Place', foreign_key: :parent_id, dependent: :destroy
17
- belongs_to :parent, class_name: 'SpreeCmCommissioner::Place', optional: true
18
18
 
19
+ has_many :children, -> { order(:lft) }, class_name: 'SpreeCmCommissioner::Place', foreign_key: :parent_id, dependent: :destroy
19
20
  has_many :vendor_stops, class_name: 'SpreeCmCommissioner::VendorStop', dependent: :destroy
20
21
 
21
22
  def self.ransackable_attributes(auth_object = nil)
22
23
  super & %w[name code]
23
24
  end
24
25
 
26
+ def full_path_name
27
+ self_and_ancestors.map(&:name).reverse.join(', ')
28
+ end
29
+
30
+ def path_ids
31
+ self_and_ancestors.map(&:id)
32
+ end
33
+
25
34
  def validate_reference?
26
35
  Spree::Store.default.code.exclude?('billing')
27
36
  end
@@ -58,9 +58,8 @@ module SpreeCmCommissioner
58
58
  base.scope :subscribable, -> { where(subscribable: 1) }
59
59
 
60
60
  base.validate :validate_event_taxons, if: -> { taxons.event.present? }
61
-
62
61
  base.validate :validate_product_date, if: -> { available_on.present? && discontinue_on.present? }
63
-
62
+ base.validate :product_type_unchanged, on: :update
64
63
  base.validates :commission_rate, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true
65
64
 
66
65
  base.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status vendor_id short_name route_type]
@@ -103,6 +102,13 @@ module SpreeCmCommissioner
103
102
 
104
103
  errors.add(:discontinue_on, 'must be after the available on date')
105
104
  end
105
+
106
+ def product_type_unchanged
107
+ return if product_type_was.nil?
108
+ return unless product_type_changed?
109
+
110
+ errors.add(:product_type, 'cannot be changed once set')
111
+ end
106
112
  end
107
113
  end
108
114
 
@@ -34,8 +34,20 @@ module SpreeCmCommissioner
34
34
 
35
35
  private
36
36
 
37
+ # only unstock or restock when variants is available, track inventory, not backorderable & not need_confirmation.
38
+ # otherwise, we can ignore them.
37
39
  def line_items
38
- @line_items ||= Spree::LineItem.where(id: @line_item_ids)
40
+ return @line_items if defined?(@line_items)
41
+
42
+ @line_items = Spree::LineItem.where(id: @line_item_ids).includes(variant: :stock_items)
43
+ @line_items = @line_items.map do |line_item|
44
+ next unless line_item.variant.available?
45
+ next unless line_item.variant.should_track_inventory?
46
+ next if line_item.variant.backorderable?
47
+ next if line_item.variant.need_confirmation?
48
+
49
+ line_item
50
+ end.compact
39
51
  end
40
52
 
41
53
  def unstock(keys, quantities)
@@ -52,7 +64,7 @@ module SpreeCmCommissioner
52
64
 
53
65
  # Return: [CachedInventoryItem(...), CachedInventoryItem(...)]
54
66
  def cached_inventory_items
55
- @cached_inventory_items ||= SpreeCmCommissioner::RedisStock::LineItemsCachedInventoryItemsBuilder.new(line_item_ids: @line_item_ids)
67
+ @cached_inventory_items ||= SpreeCmCommissioner::RedisStock::LineItemsCachedInventoryItemsBuilder.new(line_item_ids: line_items.pluck(:id))
56
68
  .call.values.flatten
57
69
  end
58
70
 
@@ -25,17 +25,11 @@ module SpreeCmCommissioner
25
25
  end
26
26
 
27
27
  def inventory_items
28
- @inventory_items ||= line_items.flat_map do |line_item|
29
- # TODO: N+1, we could fix but have a product_type in line item
30
- # then include inventory_items in line item directly #2581
31
- scope = line_item.inventory_items
32
- scope = scope.where(inventory_date: line_item.date_range) if line_item.permanent_stock?
33
- scope
34
- end
28
+ @inventory_items ||= line_items.flat_map(&:inventory_items)
35
29
  end
36
30
 
37
31
  def line_items
38
- @line_items ||= Spree::LineItem.where(id: line_item_ids).includes(variant: %i[product])
32
+ @line_items ||= Spree::LineItem.where(id: line_item_ids).includes(:inventory_items)
39
33
  end
40
34
  end
41
35
  end
@@ -3,11 +3,29 @@ module SpreeCmCommissioner
3
3
  def self.prepended(base)
4
4
  base.has_one :vendor, through: :variant
5
5
  base.after_save :update_vendor_total_inventory, if: :saved_change_to_count_on_hand?
6
+
7
+ base.after_commit :create_inventory_items, on: :create
8
+ base.after_commit :adjust_inventory_items_async, on: :destroy
6
9
  end
7
10
 
8
11
  def update_vendor_total_inventory
9
12
  SpreeCmCommissioner::VendorJob.perform_later(vendor.id) if vendor.present?
10
13
  end
14
+
15
+ private
16
+
17
+ def create_inventory_items
18
+ if variant.permanent_stock?
19
+ SpreeCmCommissioner::Stock::PermanentInventoryItemsGeneratorJob.perform_later(variant_ids: [variant.id])
20
+ else
21
+ variant.create_default_non_permanent_inventory_item! unless variant.default_inventory_item_exist?
22
+ end
23
+ end
24
+
25
+ # When admin delete stock item, it will deduct stock from inventory item
26
+ def adjust_inventory_items_async
27
+ SpreeCmCommissioner::Stock::InventoryItemsAdjusterJob.perform_later(variant_id: variant_id, quantity: -count_on_hand)
28
+ end
11
29
  end
12
30
  end
13
31
 
@@ -57,6 +57,9 @@ module SpreeCmCommissioner
57
57
  base.has_many :event_blazer_queries, class_name: 'SpreeCmCommissioner::TaxonBlazerQuery'
58
58
  base.has_many :blazer_queries, through: :event_blazer_queries, class_name: 'Blazer::Query'
59
59
 
60
+ base.has_many :taxon_option_types, class_name: 'SpreeCmCommissioner::TaxonOptionType'
61
+ base.has_many :taxon_option_values, class_name: 'SpreeCmCommissioner::TaxonOptionValue'
62
+
60
63
  def base.active_homepage_events
61
64
  joins(:homepage_section_relatables)
62
65
  .joins("INNER JOIN spree_taxons taxon ON taxon.id = cm_homepage_section_relatables.relatable_id
@@ -96,6 +99,14 @@ module SpreeCmCommissioner
96
99
  .uniq
97
100
  end
98
101
 
102
+ def selected_option_types
103
+ taxon_option_types.pluck(:option_type_id)
104
+ end
105
+
106
+ def selected_option_values
107
+ taxon_option_values.pluck(:option_value_id)
108
+ end
109
+
99
110
  def event_url
100
111
  "https://#{Spree::Store.default.url}/t/#{permalink}"
101
112
  end
@@ -0,0 +1,8 @@
1
+ require_dependency 'spree_cm_commissioner'
2
+
3
+ module SpreeCmCommissioner
4
+ class TaxonOptionType < ApplicationRecord
5
+ belongs_to :taxon, class_name: 'Spree::Taxon', dependent: :destroy
6
+ belongs_to :option_type, class_name: 'Spree::OptionType', dependent: :destroy
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ require_dependency 'spree_cm_commissioner'
2
+
3
+ module SpreeCmCommissioner
4
+ class TaxonOptionValue < ApplicationRecord
5
+ belongs_to :taxon, class_name: 'Spree::Taxon', dependent: :destroy
6
+ belongs_to :option_value, class_name: 'Spree::OptionValue', dependent: :destroy
7
+ end
8
+ end
@@ -14,23 +14,12 @@ module SpreeCmCommissioner
14
14
  validates :duration, numericality: { greater_than: 0 }
15
15
  validate :origin_and_destination_cannot_be_the_same
16
16
 
17
- has_many :trip_stops, class_name: 'SpreeCmCommissioner::TripStop', dependent: :destroy
18
-
19
- after_commit :create_trip_stops
20
-
21
- accepts_nested_attributes_for :trip_stops, allow_destroy: true
22
-
23
17
  def convert_duration_to_seconds
24
18
  return if hours.blank? && minutes.blank? && seconds.blank?
25
19
 
26
20
  self.duration = (hours.to_i * 3600) + (minutes.to_i * 60) + seconds.to_i
27
21
  end
28
22
 
29
- def create_trip_stops
30
- trip_stops.create(stop_type: :boarding, stop_id: origin_id)
31
- trip_stops.create(stop_type: :drop_off, stop_id: destination_id)
32
- end
33
-
34
23
  def duration_in_hms
35
24
  return { hours: 0, minutes: 0, seconds: 0 } if duration.nil?
36
25
 
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
5
5
 
6
6
  validate :both_trip_cannot_be_the_same
7
7
  before_validation :calculate_connection_time_minutes
8
+ validates :from_trip_id, uniqueness: { scope: :to_trip_id }
8
9
 
9
10
  private
10
11
 
@@ -8,22 +8,29 @@ module SpreeCmCommissioner
8
8
  belongs_to :stop, class_name: 'SpreeCmCommissioner::Place'
9
9
 
10
10
  before_validation :set_stop_name
11
- after_create :create_vendor_stop
12
-
11
+ after_destroy :decrement_trip_count
12
+ after_commit :create_vendor_stop
13
13
  validates :stop_id, uniqueness: { scope: :trip_id }
14
14
 
15
15
  def set_stop_name
16
- self.stop_name = stop.name
16
+ self.stop_name = stop.name if stop.present?
17
17
  end
18
18
 
19
19
  def create_vendor_stop
20
- vendor.vendor_stops.where(stop_id: stop_id, stop_type: stop_type).first_or_create
20
+ vendor_stop = vendor.vendor_stops.where(stop_id: stop_id, stop_type: stop_type).first_or_create
21
+ vendor_stop.update(trip_count: vendor_stop.trip_count.to_i + 1)
21
22
  end
22
23
 
23
24
  private
24
25
 
26
+ def decrement_trip_count
27
+ vendor.vendor_stops.where(stop_id: stop_id, stop_type: stop_type).find_each do |vendor_stop|
28
+ vendor_stop.update(trip_count: [vendor_stop.trip_count - 1, 0].max)
29
+ end
30
+ end
31
+
25
32
  def vendor
26
- Spree::Product.find(trip.product_id).vendor
33
+ Spree::Product.find(trip&.product_id).vendor
27
34
  end
28
35
  end
29
36
  end
@@ -1,10 +1,9 @@
1
1
  module SpreeCmCommissioner
2
2
  module VariantDecorator
3
- def self.prepended(base)
3
+ def self.prepended(base) # rubocop:disable Metrics/AbcSize
4
4
  base.include SpreeCmCommissioner::ProductType
5
5
  base.include SpreeCmCommissioner::ProductDelegation
6
6
  base.include SpreeCmCommissioner::VariantOptionsConcern
7
-
8
7
  base.after_commit :update_vendor_price
9
8
  base.validate :validate_option_types
10
9
  base.before_save -> { self.track_inventory = false }, if: :subscribable?
@@ -25,13 +24,26 @@ module SpreeCmCommissioner
25
24
  base.has_many :inventory_items, class_name: 'SpreeCmCommissioner::InventoryItem'
26
25
 
27
26
  base.scope :subscribable, -> { active.joins(:product).where(product: { subscribable: true, status: :active }) }
28
- base.scope :with_permanent_stock, -> { joins(:product).where(product: { product_type: base::PERMANENT_STOCK_PRODUCT_TYPES }) }
29
- base.scope :with_non_permanent_stock, -> { joins(:product).where.not(product: { product_type: base::PERMANENT_STOCK_PRODUCT_TYPES }) }
27
+ base.scope :with_permanent_stock, -> { where(product_type: base::PERMANENT_STOCK_PRODUCT_TYPES) }
28
+ base.scope :with_non_permanent_stock, -> { where.not(product_type: base::PERMANENT_STOCK_PRODUCT_TYPES) }
30
29
 
31
30
  base.has_one :trip,
32
31
  class_name: 'SpreeCmCommissioner::Trip'
32
+ base.has_many :trip_stops, class_name: 'SpreeCmCommissioner::TripStop', dependent: :destroy, foreign_key: :trip_id
33
33
  base.accepts_nested_attributes_for :option_values
34
- base.after_commit :sync_trip, if: -> { product.product_type == 'transit' }
34
+
35
+ base.before_save -> { self.product_type = product.product_type }, if: -> { product_type.nil? }
36
+
37
+ base.after_commit :sync_trip, if: :transit?
38
+ base.accepts_nested_attributes_for :trip_stops, allow_destroy: true
39
+ base.after_commit :create_trip_stops, if: :transit?
40
+ end
41
+
42
+ def create_trip_stops
43
+ return if is_master?
44
+
45
+ trip_stops.find_or_create_by(stop_type: :boarding, stop_id: options.origin)
46
+ trip_stops.find_or_create_by(stop_type: :drop_off, stop_id: options.destination)
35
47
  end
36
48
 
37
49
  def delivery_required?
@@ -82,10 +94,6 @@ module SpreeCmCommissioner
82
94
  "#{display_sku} - #{display_price}"
83
95
  end
84
96
 
85
- def transit?
86
- product.product_type == 'transit'
87
- end
88
-
89
97
  # override
90
98
  def in_stock?(options = {})
91
99
  SpreeCmCommissioner::Stock::AvailabilityChecker.new(self, options).can_supply?
@@ -94,7 +102,7 @@ module SpreeCmCommissioner
94
102
  private
95
103
 
96
104
  def update_vendor_price
97
- return unless vendor.present? && product&.product_type == vendor&.primary_product_type
105
+ return unless vendor.present? && product_type == vendor&.primary_product_type
98
106
 
99
107
  vendor.update(min_price: price) if price < vendor.min_price
100
108
  vendor.update(max_price: price) if price > vendor.max_price
@@ -120,7 +128,7 @@ module SpreeCmCommissioner
120
128
  end
121
129
 
122
130
  def sync_trip
123
- return unless product.product_type == 'transit'
131
+ return unless transit?
124
132
 
125
133
  trip = SpreeCmCommissioner::Trip.find_or_initialize_by(variant_id: id)
126
134
 
@@ -166,19 +166,5 @@ module SpreeCmCommissioner
166
166
  Time.zone.parse(time) if time.present?
167
167
  end
168
168
  end
169
-
170
- def arrival_time
171
- @arrival_time ||= departure_time + total_duration_in_minutes.minutes
172
- end
173
-
174
- def total_duration_in_minutes
175
- total_duration_in_minutes = 0
176
-
177
- total_duration_in_minutes += duration_in_hours * 60 if duration_in_hours.present?
178
- total_duration_in_minutes += duration_in_minutes if duration_in_minutes.present?
179
- total_duration_in_minutes += duration_in_seconds / 60 if duration_in_seconds.present?
180
-
181
- total_duration_in_minutes
182
- end
183
169
  end
184
170
  end
@@ -4,6 +4,7 @@ module SpreeCmCommissioner
4
4
  belongs_to :vendor, class_name: 'Spree::Vendor'
5
5
  belongs_to :stop, class_name: 'SpreeCmCommissioner::Place'
6
6
 
7
- enum stop_type: { boarding: 0, drop_off: 1 }
7
+ validates :trip_count, numericality: { greater_than_or_equal_to: 0 }
8
+ enum :stop_type, { boarding: 0, drop_off: 1 }
8
9
  end
9
10
  end
@@ -34,11 +34,11 @@ module SpreeCmCommissioner
34
34
  trips.vehicle_id AS vehicle_id'
35
35
  )
36
36
  .joins('INNER JOIN cm_trips AS trips ON trips.variant_id = spree_variants.id')
37
- .joins('INNER JOIN cm_trip_stops AS boarding ON boarding.trip_id = trips.id AND boarding.stop_type = 0')
38
- .joins('INNER JOIN cm_trip_stops AS drop_off ON drop_off.trip_id = trips.id AND drop_off.stop_type = 1')
37
+ .joins('INNER JOIN cm_trip_stops AS boarding ON boarding.trip_id = spree_variants.id AND boarding.stop_type = 0')
38
+ .joins('INNER JOIN cm_trip_stops AS drop_off ON drop_off.trip_id = spree_variants.id AND drop_off.stop_type = 1')
39
39
  .joins('INNER JOIN spree_vendors AS vendors ON vendors.id = spree_variants.vendor_id')
40
40
  .joins('INNER JOIN spree_products AS routes ON routes.id = spree_variants.product_id')
41
- .where('trips.origin_id = ? AND trips.destination_id = ?', origin_id, destination_id)
41
+ .where('boarding.stop_id = ? AND drop_off.stop_id = ?', origin_id, destination_id)
42
42
 
43
43
  result.map do |trip|
44
44
  trip_result_options = {
@@ -69,9 +69,9 @@ module SpreeCmCommissioner
69
69
  INNER JOIN spree_products AS routes2 ON routes2.id = variant2.product_id
70
70
  INNER JOIN cm_trips AS trip1 ON trip1.variant_id = cm_trip_connections.from_trip_id
71
71
  INNER JOIN cm_trips AS trip2 ON trip2.variant_id = cm_trip_connections.to_trip_id
72
- INNER JOIN cm_places trip1_origin ON trip1_origin.id = trip1.origin_id
73
- INNER JOIN cm_places trip2_origin ON trip2_origin.id = trip2.origin_id
74
- INNER JOIN cm_places trip2_destination ON trip2_destination.id = trip2.destination_id
72
+ INNER JOIN cm_trip_stops trip1_origin ON trip1_origin.trip_id = variant1.id AND trip1_origin.stop_type = 0
73
+ INNER JOIN cm_trip_stops trip2_origin ON trip2_origin.trip_id = variant2.id AND trip2_origin.stop_type = 0
74
+ INNER JOIN cm_trip_stops trip2_destination ON trip2_destination.trip_id = variant2.id AND trip2_destination.stop_type = 1
75
75
  INNER JOIN spree_vendors AS vendor1 ON vendor1.id = variant1.vendor_id
76
76
  INNER JOIN spree_vendors AS vendor2 ON vendor2.id = variant2.vendor_id'
77
77
  )
@@ -93,12 +93,12 @@ module SpreeCmCommissioner
93
93
  trip2.vehicle_id AS trip2_vehicle,
94
94
  routes2.short_name AS route2_short_name,
95
95
  routes2.name AS route2_name,
96
- trip1_origin.name AS trip1_origin,
97
- trip2_origin.name AS trip2_origin,
98
- trip2_destination.name AS trip2_destination,
96
+ trip1_origin.stop_name AS trip1_origin,
97
+ trip2_origin.stop_name AS trip2_origin,
98
+ trip2_destination.stop_name AS trip2_destination,
99
99
  vendor2.name AS trip2_vendor_name'
100
- )
101
- .where('trip1_origin.id = ? AND trip2_destination.id = ?', origin_id, destination_id)
100
+ ).where('trip1_origin.stop_id = ? AND trip2_destination.stop_id = ?', origin_id, destination_id
101
+ ).uniq
102
102
 
103
103
  return [] if result.blank?
104
104
 
@@ -0,0 +1,54 @@
1
+ module SpreeCmCommissioner
2
+ class VendorStopPlaceQuery
3
+ attr_reader :query, :vendor_id, :stop_type, :reference_stop_id
4
+
5
+ def initialize(query:, vendor_id:, stop_type: :boarding, reference_stop_id: nil)
6
+ @query = query.to_s.strip
7
+ @vendor_id = vendor_id
8
+ @stop_type = stop_type
9
+ @reference_stop_id = reference_stop_id
10
+ end
11
+
12
+ def call
13
+ results = vendor_stops
14
+ return [] if results.empty?
15
+
16
+ places = results.includes(:stop).map(&:stop).compact.uniq
17
+
18
+ format_places(places)
19
+ end
20
+
21
+ private
22
+
23
+ def vendor_stops
24
+ scope = base_scope
25
+ return scope if query.blank?
26
+
27
+ scope = scope.where.not(stop_id: reference_stop_id) if reference_stop_id.present?
28
+ scope.where('cm_places.name ILIKE ?', "%#{query}%")
29
+ end
30
+
31
+ def base_scope
32
+ SpreeCmCommissioner::VendorStop
33
+ .joins(:stop)
34
+ .where(vendor_id: vendor_id)
35
+ .where(stop_type: stop_type)
36
+ .order(trip_count: :desc)
37
+ .where.not(trip_count: 0)
38
+ end
39
+
40
+ def format_places(places)
41
+ places
42
+ .sort_by { |p| [-p.depth, -p.lft] }
43
+ .filter_map do |place|
44
+ next if place.depth.zero?
45
+
46
+ {
47
+ id: place.id,
48
+ parent_id: place.parent_id,
49
+ name: place.self_and_ancestors.map(&:name).reverse.join(', ')
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -13,6 +13,7 @@ module Spree
13
13
 
14
14
  belongs_to :occupation, serializer: Spree::V2::Tenant::TaxonSerializer
15
15
  belongs_to :nationality, serializer: Spree::V2::Tenant::TaxonSerializer
16
+ has_one :id_card, serializer: Spree::V2::Tenant::IdCardSerializer
16
17
 
17
18
  # allowed_checkout updates frequently
18
19
  cache_options store: nil
@@ -48,6 +48,8 @@ module SpreeCmCommissioner
48
48
  end
49
49
 
50
50
  def fetch_option_value(guest, option_type_name)
51
+ return guest.formatted_bib_number if option_type_name == 'bib-prefix'
52
+
51
53
  guest.line_item&.variant&.find_option_value_name_for(option_type_name: option_type_name)
52
54
  end
53
55
 
@@ -1,17 +1,23 @@
1
1
  <div>
2
2
  <% events.each do |event| %>
3
- <div>
4
- <h6><%= event.options[:inventory_item].variant.options_text %></h6>
5
- <ul class="list-group mt-1">
3
+ <% inventory_item = event.options[:inventory_item] %>
4
+
5
+ <div class="mb-3">
6
+ <h6><%= inventory_item.variant.options_text %></h6>
7
+ <ul class="list-group mt-1 mb-1">
6
8
  <li class="list-group-item d-flex justify-content-between align-items-center">
7
9
  Max capacity
8
- <span class="badge badge-primary badge-pill"><%= event.options[:inventory_item].max_capacity %></span>
10
+ <span class="badge badge-primary badge-pill"><%= inventory_item.max_capacity %></span>
9
11
  </li>
10
12
  <li class="list-group-item d-flex justify-content-between align-items-center">
11
- Quantity available
12
- <span class="badge badge-primary badge-pill"><%= event.options[:inventory_item].quantity_available %></span>
13
+ Quantity available in DB
14
+ <span class="badge badge-primary badge-pill"><%= inventory_item.quantity_available %></span>
13
15
  </li>
14
16
  </ul>
17
+
18
+ <small class="text-muted">
19
+ <%= inventory_item_message(inventory_item, @cached_inventory_items[inventory_item.id]) %>
20
+ </small>
15
21
  </div>
16
22
  <% end %>
17
23
  </div>
@@ -17,9 +17,11 @@
17
17
  </td>
18
18
  <td class="text-center d-flex flex-column align-items-center justify-content-center">
19
19
  <div style="width: 90px;">
20
- <%= form_tag admin_stock_items_path(variant_id: variant.id, stock_location_id: stock_location.id), method: :post do %>
20
+ <%= form_tag admin_product_stock_managements_path(product_id: params[:product_id], variant_id: variant.id, stock_location_id: stock_location.id), method: :post do %>
21
21
  <div class="input-group input-group-sm">
22
22
  <%= number_field_tag 'stock_movement[quantity]', 0, class: 'form-control text-center p-0' %>
23
+ <%= hidden_field_tag 'stock_movement[originator_id]', spree_current_user.id %>
24
+ <%= hidden_field_tag 'stock_movement[originator_type]', spree_current_user.class.name %>
23
25
  <div class="input-group-append">
24
26
  <%= button_tag(class: 'btn btn-outline-success pl-2 pr-1') do %>
25
27
  <%= svg_icon(name: 'arrow-left-right.svg', classes: "icon", width: 14, height: 14) %>
@@ -19,9 +19,12 @@
19
19
  <%= date.day %>
20
20
  <ul class="p-0 m-0 list-unstyled">
21
21
  <% events.each do |event| %>
22
- <li class="badge badge-warning">
23
- <%= event.options[:inventory_item].quantity_available %>
24
- </li>
22
+ <% inventory_item = event.options[:inventory_item] %>
23
+ <% synced = inventory_item.quantity_available == @cached_inventory_items[inventory_item.id].quantity_available %>
24
+
25
+ <li class="badge <%= synced ? "badge-warning" : "badge-danger" %>">
26
+ <%= inventory_item.quantity_available %>
27
+ </li>
25
28
  <% end %>
26
29
  </ul>
27
30
  </div>