spree_cm_commissioner 1.10.0 → 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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test_and_build_gem.yml +131 -98
  3. data/.gitignore +2 -1
  4. data/.vscode/settings.json +1 -1
  5. data/Gemfile.lock +22 -1
  6. data/Rakefile +33 -4
  7. data/app/controllers/spree/admin/stock_managements_controller.rb +56 -1
  8. data/app/controllers/spree/api/v2/storefront/accommodations/variants_controller.rb +42 -0
  9. data/app/controllers/spree/api/v2/storefront/accommodations_controller.rb +14 -31
  10. data/app/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +2 -2
  11. data/app/finders/spree_cm_commissioner/accommodations/find.rb +40 -0
  12. data/app/finders/spree_cm_commissioner/accommodations/find_variant.rb +35 -0
  13. data/app/interactors/spree_cm_commissioner/create_event.rb +23 -0
  14. data/app/interactors/spree_cm_commissioner/ensure_correct_product_type.rb +40 -0
  15. data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +25 -0
  16. data/app/interactors/spree_cm_commissioner/stock/inventory_items_adjuster.rb +13 -0
  17. data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +75 -0
  18. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +24 -0
  19. data/app/interactors/spree_cm_commissioner/vattanac_bank_initiator.rb +27 -8
  20. data/app/jobs/spree_cm_commissioner/ensure_correct_product_type_job.rb +7 -0
  21. data/app/jobs/spree_cm_commissioner/inventory_item_syncer_job.rb +7 -0
  22. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +11 -0
  23. data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +9 -0
  24. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +26 -0
  25. data/app/models/concerns/spree_cm_commissioner/product_delegation.rb +1 -3
  26. data/app/models/concerns/spree_cm_commissioner/product_type.rb +10 -0
  27. data/app/models/spree_cm_commissioner/inventory.rb +11 -0
  28. data/app/models/spree_cm_commissioner/inventory_item.rb +55 -0
  29. data/app/models/spree_cm_commissioner/line_item_decorator.rb +16 -5
  30. data/app/models/spree_cm_commissioner/order_decorator.rb +15 -0
  31. data/app/models/spree_cm_commissioner/place.rb +11 -2
  32. data/app/models/spree_cm_commissioner/product_decorator.rb +9 -2
  33. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +40 -0
  34. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +126 -0
  35. data/app/models/spree_cm_commissioner/redis_stock/line_items_cached_inventory_items_builder.rb +36 -0
  36. data/app/models/spree_cm_commissioner/redis_stock/variant_cached_inventory_items_builder.rb +27 -0
  37. data/app/models/spree_cm_commissioner/stock/availability_checker.rb +27 -25
  38. data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +2 -1
  39. data/app/models/spree_cm_commissioner/stock/line_item_availability_checker.rb +3 -3
  40. data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +44 -0
  41. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +18 -0
  42. data/app/models/spree_cm_commissioner/taxon_decorator.rb +11 -0
  43. data/app/models/spree_cm_commissioner/taxon_option_type.rb +8 -0
  44. data/app/models/spree_cm_commissioner/taxon_option_value.rb +8 -0
  45. data/app/models/spree_cm_commissioner/trip.rb +0 -11
  46. data/app/models/spree_cm_commissioner/trip_stop.rb +11 -4
  47. data/app/models/spree_cm_commissioner/variant_decorator.rb +39 -27
  48. data/app/models/spree_cm_commissioner/vendor_stop.rb +2 -1
  49. data/app/queries/spree_cm_commissioner/vendor_stop_place_query.rb +54 -0
  50. data/app/request_schemas/spree_cm_commissioner/accommodation_request_schema.rb +3 -0
  51. data/app/request_schemas/spree_cm_commissioner/application_request_schema.rb +1 -1
  52. data/app/request_schemas/spree_cm_commissioner/variant_request_schema.rb +19 -0
  53. data/app/serializers/spree/v2/storefront/accommodation_serializer.rb +2 -0
  54. data/app/serializers/spree/v2/tenant/guest_serializer.rb +1 -0
  55. data/app/services/spree_cm_commissioner/aes_encryption_service.rb +6 -4
  56. data/app/services/spree_cm_commissioner/organizer/export_guest_csv_service.rb +2 -0
  57. data/app/views/spree/admin/stock_managements/_events_popover.html.erb +23 -0
  58. data/app/views/spree/admin/stock_managements/_variant_stock_items.html.erb +3 -1
  59. data/app/views/spree/admin/stock_managements/calendar.html.erb +35 -0
  60. data/app/views/spree/admin/stock_managements/index.html.erb +40 -5
  61. data/config/initializers/spree_permitted_attributes.rb +5 -0
  62. data/config/routes.rb +11 -2
  63. data/db/migrate/20250304293518_create_cm_inventory_items.rb +21 -0
  64. data/db/migrate/20250418072528_add_nested_set_columns_to_places.rb +10 -0
  65. data/db/migrate/20250429094228_add_lock_version_to_cm_inventory_items.rb +5 -0
  66. data/db/migrate/20250430091742_create_cm_taxon_option_types.rb +9 -0
  67. data/db/migrate/20250430092928_create_cm_taxon_option_values.rb +9 -0
  68. data/db/migrate/20250502025848_add_index_to_spree_products.rb +5 -0
  69. data/db/migrate/20250502030001_add_product_type_to_spree_variants.rb +5 -0
  70. data/db/migrate/20250502030002_add_product_type_to_spree_line_items.rb +5 -0
  71. data/db/migrate/20250506092929_add_trip_count_to_cm_vendor_stops.rb +5 -0
  72. data/docker-compose.yml +1 -1
  73. data/lib/generators/spree_cm_commissioner/install/install_generator.rb +11 -3
  74. data/lib/generators/spree_cm_commissioner/install/templates/app/javascript/{spree_cm_commissioner → spree_dashboard/spree_cm_commissioner}/utilities.js +4 -0
  75. data/lib/spree_cm_commissioner/cached_inventory_item.rb +23 -0
  76. data/lib/spree_cm_commissioner/calendar_event.rb +11 -1
  77. data/lib/spree_cm_commissioner/test_helper/factories/homepage_section_relatable_factory.rb +1 -1
  78. data/lib/spree_cm_commissioner/test_helper/factories/inventory_item_factory.rb +9 -0
  79. data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +1 -1
  80. data/lib/spree_cm_commissioner/test_helper/factories/place_factory.rb +11 -1
  81. data/lib/spree_cm_commissioner/test_helper/factories/product_factory.rb +18 -5
  82. data/lib/spree_cm_commissioner/test_helper/factories/stock_location_factory.rb +2 -2
  83. data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +39 -6
  84. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +1 -1
  85. data/lib/spree_cm_commissioner/version.rb +1 -1
  86. data/lib/spree_cm_commissioner.rb +34 -0
  87. data/lib/tasks/create_default_non_permanent_inventory_items.rake +16 -0
  88. data/lib/tasks/ensure_correct_product_type.rake +7 -0
  89. data/lib/tasks/generate_inventory_items.rake +7 -0
  90. data/lib/tasks/migrate_and_rebuild_place_hierarchy.rake +9 -0
  91. data/lib/tasks/update_orphan_root_places.rake +7 -0
  92. data/spree_cm_commissioner.gemspec +5 -0
  93. metadata +88 -7
  94. data/app/queries/spree_cm_commissioner/variant_availability/non_permanent_stock_query.rb +0 -45
  95. data/app/queries/spree_cm_commissioner/variant_availability/permanent_stock_query.rb +0 -55
@@ -0,0 +1,40 @@
1
+ module SpreeCmCommissioner
2
+ module RedisStock
3
+ class CachedInventoryItemsBuilder
4
+ attr_reader :inventory_items
5
+
6
+ def initialize(inventory_items)
7
+ @inventory_items = inventory_items
8
+ end
9
+
10
+ # output: [ CachedInventoryItem(...), CachedInventoryItem(...) ]
11
+ def call
12
+ keys = inventory_items.map { |item| "inventory:#{item.id}" }
13
+ return [] unless keys.any?
14
+
15
+ counts = SpreeCmCommissioner.redis_pool.with { |redis| redis.mget(*keys) }
16
+ inventory_items.map.with_index do |inventory_item, i|
17
+ ::SpreeCmCommissioner::CachedInventoryItem.new(
18
+ inventory_key: keys[i],
19
+ active: inventory_item.active?,
20
+ quantity_available: cache_inventory(keys[i], inventory_item, counts[i]),
21
+ inventory_item_id: inventory_item.id,
22
+ variant_id: inventory_item.variant_id
23
+ )
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def cache_inventory(key, inventory_item, count_in_redis)
30
+ return count_in_redis.to_i if count_in_redis.present?
31
+
32
+ SpreeCmCommissioner.redis_pool.with do |redis|
33
+ redis.set(key, inventory_item.quantity_available, ex: inventory_item.redis_expired_in)
34
+ end
35
+
36
+ inventory_item.quantity_available
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,126 @@
1
+ module SpreeCmCommissioner
2
+ module RedisStock
3
+ class InventoryUpdater
4
+ class UnableToRestock < StandardError; end
5
+ class UnableToUnstock < StandardError; end
6
+
7
+ def initialize(line_item_ids)
8
+ @line_item_ids = line_item_ids
9
+ end
10
+
11
+ def unstock!
12
+ keys, quantities, inventory_ids = extract_inventory_data
13
+
14
+ raise UnableToUnstock, Spree.t(:insufficient_stock_lines_present) unless unstock(keys, quantities)
15
+
16
+ inventory_id_and_quantities = inventory_ids.map.with_index do |inventory_id, i|
17
+ { inventory_id: inventory_id, quantity: -quantities[i] }
18
+ end
19
+
20
+ schedule_sync_inventory(inventory_id_and_quantities)
21
+ end
22
+
23
+ def restock!
24
+ keys, quantities, inventory_ids = extract_inventory_data
25
+
26
+ raise UnableToRestock unless restock(keys, quantities)
27
+
28
+ inventory_id_and_quantities = inventory_ids.map.with_index do |inventory_id, i|
29
+ { inventory_id: inventory_id, quantity: quantities[i] }
30
+ end
31
+
32
+ schedule_sync_inventory(inventory_id_and_quantities)
33
+ end
34
+
35
+ private
36
+
37
+ # only unstock or restock when variants is available, track inventory, not backorderable & not need_confirmation.
38
+ # otherwise, we can ignore them.
39
+ def line_items
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
51
+ end
52
+
53
+ def unstock(keys, quantities)
54
+ SpreeCmCommissioner.redis_pool.with do |redis|
55
+ redis.eval(unstock_redis_script, keys: keys, argv: quantities)
56
+ end.positive?
57
+ end
58
+
59
+ def restock(keys, quantities)
60
+ SpreeCmCommissioner.redis_pool.with do |redis|
61
+ redis.eval(restock_redis_script, keys: keys, argv: quantities)
62
+ end.positive?
63
+ end
64
+
65
+ # Return: [CachedInventoryItem(...), CachedInventoryItem(...)]
66
+ def cached_inventory_items
67
+ @cached_inventory_items ||= SpreeCmCommissioner::RedisStock::LineItemsCachedInventoryItemsBuilder.new(line_item_ids: line_items.pluck(:id))
68
+ .call.values.flatten
69
+ end
70
+
71
+ def extract_inventory_data
72
+ keys = []
73
+ quantities = []
74
+ inventory_ids = []
75
+
76
+ cached_inventory_items.each do |cached_inventory_item|
77
+ keys << cached_inventory_item.inventory_key
78
+ quantities << line_items.find { |item| item.variant_id == cached_inventory_item.variant_id }.quantity
79
+ inventory_ids << cached_inventory_item.inventory_item_id
80
+ end
81
+
82
+ [keys, quantities, inventory_ids]
83
+ end
84
+
85
+ def unstock_redis_script
86
+ <<~LUA
87
+ local keys = KEYS
88
+ local quantities = ARGV
89
+
90
+ -- Check availability first
91
+ for i, key in ipairs(keys) do
92
+ local current = tonumber(redis.call('GET', key) or 0)
93
+ if current - tonumber(quantities[i]) < 0 then
94
+ return 0
95
+ end
96
+ end
97
+
98
+ -- Apply updates
99
+ for i, key in ipairs(keys) do
100
+ redis.call('DECRBY', key, tonumber(quantities[i]))
101
+ end
102
+
103
+ return 1
104
+ LUA
105
+ end
106
+
107
+ def restock_redis_script
108
+ <<~LUA
109
+ local keys = KEYS
110
+ local quantities = ARGV
111
+
112
+ -- Apply restock updates
113
+ for i, key in ipairs(keys) do
114
+ redis.call('INCRBY', key, tonumber(quantities[i]))
115
+ end
116
+
117
+ return 1
118
+ LUA
119
+ end
120
+
121
+ def schedule_sync_inventory(inventory_id_and_quantities)
122
+ SpreeCmCommissioner::InventoryItemSyncerJob.perform_later(inventory_id_and_quantities:)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,36 @@
1
+ module SpreeCmCommissioner
2
+ module RedisStock
3
+ class LineItemsCachedInventoryItemsBuilder
4
+ attr_reader :line_item_ids
5
+
6
+ def initialize(line_item_ids:)
7
+ @line_item_ids = line_item_ids
8
+ end
9
+
10
+ # return list of inventory items group by :line_item_id:
11
+ # {
12
+ # 1: [ CachedInventoryItem(...), CachedInventoryItem(...) ],
13
+ # 2: [ CachedInventoryItem(...), CachedInventoryItem(...) ],
14
+ # }
15
+ def call
16
+ cached_inventory_items.group_by do |cached_inventory_item|
17
+ line_item = line_items.find { |item| item.variant_id == cached_inventory_item.variant_id }
18
+ line_item.id
19
+ end
20
+ end
21
+
22
+ def cached_inventory_items
23
+ @cached_inventory_items ||= SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(inventory_items)
24
+ .call
25
+ end
26
+
27
+ def inventory_items
28
+ @inventory_items ||= line_items.flat_map(&:inventory_items)
29
+ end
30
+
31
+ def line_items
32
+ @line_items ||= Spree::LineItem.where(id: line_item_ids).includes(:inventory_items)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ module SpreeCmCommissioner
2
+ module RedisStock
3
+ class VariantCachedInventoryItemsBuilder
4
+ attr_reader :variant_id, :from_date, :to_date
5
+
6
+ def initialize(variant_id:, from_date: nil, to_date: nil)
7
+ @variant_id = variant_id
8
+ @from_date = from_date
9
+ @to_date = to_date
10
+ end
11
+
12
+ # output: [ CachedInventoryItem(...), CachedInventoryItem(...) ]
13
+ def call
14
+ ::SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(inventory_items).call
15
+ end
16
+
17
+ def inventory_items
18
+ variant = Spree::Variant.find(variant_id)
19
+
20
+ inventory_items = variant.inventory_items
21
+ inventory_items.where(inventory_date: from_date..to_date) if variant.permanent_stock?
22
+
23
+ inventory_items
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,47 +1,49 @@
1
1
  module SpreeCmCommissioner
2
2
  module Stock
3
3
  class AvailabilityChecker
4
- attr_reader :variant, :error_message
4
+ attr_reader :variant, :options, :error_message
5
5
 
6
- def initialize(variant)
6
+ def initialize(variant, options = {})
7
7
  @variant = variant
8
+ @options = options
8
9
  @error_message = nil
9
10
  end
10
11
 
11
- def can_supply?(quantity = 1, options = {})
12
+ def can_supply?(quantity = 1)
12
13
  return false unless variant.available?
13
14
  return true unless variant.should_track_inventory?
14
15
  return true if variant.backorderable?
15
16
  return true if variant.need_confirmation?
16
17
 
17
- # when delivery required, shipment will dynamically add / remove unit from stock item.
18
- # so we can directly check can_supply with stock items directly.
19
- return variant.stock_items.sum(:count_on_hand) >= quantity if variant.delivery_required?
18
+ variant_available?(quantity)
19
+ end
20
20
 
21
- if variant.permanent_stock?
22
- permanent_stock_variant_available?(quantity, options)
23
- else
24
- variant_available?(quantity, options)
21
+ def variant_available?(quantity = 1)
22
+ return false if cached_inventory_items.empty?
23
+
24
+ cached_inventory_items.all? do |cached_inventory_item|
25
+ cached_inventory_item.active? && cached_inventory_item.quantity_available >= quantity
25
26
  end
26
27
  end
27
28
 
28
- def variant_available?(quantity = 1, options = {})
29
- query = SpreeCmCommissioner::VariantAvailability::NonPermanentStockQuery.new(
30
- variant: variant,
31
- except_line_item_id: options[:except_line_item_id]
32
- )
33
- result = query.available?(quantity)
34
- @error_message = query.error_message unless result
35
- result
29
+ def cached_inventory_items
30
+ return @cached_inventory_items if defined?(@cached_inventory_items)
31
+
32
+ if variant.permanent_stock?
33
+ return [] if options[:from_date].blank? || options[:to_date].blank?
34
+
35
+ @cached_inventory_items = builder_klass.new(
36
+ variant_id: variant.id,
37
+ from_date: options[:from_date].to_date,
38
+ to_date: options[:to_date].to_date
39
+ ).call
40
+ else
41
+ @cached_inventory_items = builder_klass.new(variant_id: variant.id).call
42
+ end
36
43
  end
37
44
 
38
- def permanent_stock_variant_available?(quantity = 1, options = {})
39
- SpreeCmCommissioner::VariantAvailability::PermanentStockQuery.new(
40
- variant: variant,
41
- from_date: options[:from_date].to_date,
42
- to_date: options[:to_date].to_date,
43
- except_line_item_id: options[:except_line_item_id]
44
- ).available?(quantity)
45
+ def builder_klass
46
+ ::SpreeCmCommissioner::RedisStock::VariantCachedInventoryItemsBuilder
45
47
  end
46
48
  end
47
49
  end
@@ -3,7 +3,8 @@ module SpreeCmCommissioner
3
3
  module AvailabilityValidatorDecorator
4
4
  # override
5
5
  def item_available?(line_item, quantity)
6
- SpreeCmCommissioner::Stock::LineItemAvailabilityChecker.new(line_item).can_supply?(quantity)
6
+ SpreeCmCommissioner::Stock::LineItemAvailabilityChecker.new(line_item)
7
+ .can_supply?(quantity)
7
8
  end
8
9
  end
9
10
  end
@@ -8,14 +8,14 @@ module SpreeCmCommissioner
8
8
  end
9
9
 
10
10
  def can_supply?(quantity)
11
- AvailabilityChecker.new(line_item.variant).can_supply?(quantity, options)
11
+ ::SpreeCmCommissioner::Stock::AvailabilityChecker.new(line_item.variant, options)
12
+ .can_supply?(quantity)
12
13
  end
13
14
 
14
15
  def options
15
16
  {
16
17
  from_date: line_item.from_date,
17
- to_date: line_item.to_date,
18
- except_line_item_id: line_item.id
18
+ to_date: line_item.to_date
19
19
  }
20
20
  end
21
21
  end
@@ -0,0 +1,44 @@
1
+ # On top of SpreeCmCommissioner::Stock::AvailabilityChecker, this class does the same thing but in bulk check with redis.
2
+ # Which mean, it check basic condition with db first, once pass, it check with redis.
3
+ module SpreeCmCommissioner
4
+ module Stock
5
+ class OrderAvailabilityChecker
6
+ attr_reader :order
7
+
8
+ def initialize(order)
9
+ @order = order
10
+ end
11
+
12
+ def can_supply_all?
13
+ insufficient_stock_lines.empty?
14
+ end
15
+
16
+ def insufficient_stock_lines
17
+ cached_inventory_items_group_by_line_item_id.map do |line_item_id, cached_inventory_items|
18
+ line_item = order.line_items.find { |item| item.id == line_item_id }
19
+ line_item unless sufficient_stock_for?(line_item, cached_inventory_items)
20
+ end.compact
21
+ end
22
+
23
+ # {
24
+ # 1: [ {inventory_key: "inventory:1", active: true, quantity_available: 5, inventory_item_id: 1, variant_id: 1}],
25
+ # 2: [ {inventory_key: "inventory:2", active: true, quantity_available: 5, inventory_item_id: 2, variant_id: 2}],
26
+ # }
27
+ def cached_inventory_items_group_by_line_item_id
28
+ @cached_inventory_items_group_by_line_item_id ||=
29
+ ::SpreeCmCommissioner::RedisStock::LineItemsCachedInventoryItemsBuilder.new(
30
+ line_item_ids: order.line_items.pluck(:id)
31
+ ).call
32
+ end
33
+
34
+ def sufficient_stock_for?(line_item, cached_inventory_items)
35
+ return false unless line_item.variant.available?
36
+ return true unless line_item.variant.should_track_inventory?
37
+ return true if line_item.variant.backorderable?
38
+ return true if line_item.variant.need_confirmation?
39
+
40
+ cached_inventory_items.all? { |item| item.active? && item.quantity_available >= line_item.quantity }
41
+ end
42
+ end
43
+ end
44
+ 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
 
@@ -8,8 +8,8 @@ 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
@@ -17,13 +17,20 @@ module SpreeCmCommissioner
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 if trip.present?
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,9 +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
+ base.include SpreeCmCommissioner::ProductType
4
5
  base.include SpreeCmCommissioner::ProductDelegation
5
6
  base.include SpreeCmCommissioner::VariantOptionsConcern
6
-
7
7
  base.after_commit :update_vendor_price
8
8
  base.validate :validate_option_types
9
9
  base.before_save -> { self.track_inventory = false }, if: :subscribable?
@@ -21,14 +21,29 @@ module SpreeCmCommissioner
21
21
  base.has_many :variant_guest_card_class, class_name: 'SpreeCmCommissioner::VariantGuestCardClass'
22
22
  base.has_many :guest_card_classes, class_name: 'SpreeCmCommissioner::GuestCardClass', through: :variant_guest_card_class
23
23
 
24
+ base.has_many :inventory_items, class_name: 'SpreeCmCommissioner::InventoryItem'
25
+
24
26
  base.scope :subscribable, -> { active.joins(:product).where(product: { subscribable: true, status: :active }) }
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) }
29
+
25
30
  base.has_one :trip,
26
31
  class_name: 'SpreeCmCommissioner::Trip'
27
32
  base.has_many :trip_stops, class_name: 'SpreeCmCommissioner::TripStop', dependent: :destroy, foreign_key: :trip_id
28
33
  base.accepts_nested_attributes_for :option_values
29
- base.has_many :trip_stops, class_name: 'SpreeCmCommissioner::TripStop', foreign_key: :trip_id, dependent: :destroy
30
- 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?
31
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)
32
47
  end
33
48
 
34
49
  def delivery_required?
@@ -44,14 +59,26 @@ module SpreeCmCommissioner
44
59
  super || product.discontinued?
45
60
  end
46
61
 
47
- def permanent_stock?
48
- accommodation?
49
- end
50
-
51
62
  def event
52
63
  taxons.event.first
53
64
  end
54
65
 
66
+ def default_inventory_item_exist?
67
+ inventory_items.exists?(inventory_date: nil)
68
+ end
69
+
70
+ def create_default_non_permanent_inventory_item!(quantity_available: nil, max_capacity: nil)
71
+ return if product_type.blank? # handle in case product not exist for variant.
72
+ return unless should_track_inventory?
73
+ return if default_inventory_item_exist?
74
+
75
+ inventory_items.create!(
76
+ product_type: product_type,
77
+ quantity_available: [0, quantity_available || total_on_hand].max,
78
+ max_capacity: [0, max_capacity || total_on_hand].max
79
+ )
80
+ end
81
+
55
82
  # override
56
83
  def options_text
57
84
  @options_text ||= Spree::Variants::VisableOptionsPresenter.new(self).to_sentence
@@ -67,30 +94,15 @@ module SpreeCmCommissioner
67
94
  "#{display_sku} - #{display_price}"
68
95
  end
69
96
 
70
- def transit?
71
- product.product_type == 'transit'
72
- end
73
-
74
97
  # override
75
- def in_stock?
76
- available_quantity.positive?
98
+ def in_stock?(options = {})
99
+ SpreeCmCommissioner::Stock::AvailabilityChecker.new(self, options).can_supply?
77
100
  end
78
101
 
79
102
  private
80
103
 
81
- def total_purchases
82
- Spree::LineItem.complete.where(variant_id: id).sum(:quantity).to_i
83
- end
84
-
85
- def available_quantity
86
- stock_count = stock_items.sum(&:count_on_hand)
87
- return stock_count if delivery_required?
88
-
89
- stock_count - total_purchases
90
- end
91
-
92
104
  def update_vendor_price
93
- return unless vendor.present? && product&.product_type == vendor&.primary_product_type
105
+ return unless vendor.present? && product_type == vendor&.primary_product_type
94
106
 
95
107
  vendor.update(min_price: price) if price < vendor.min_price
96
108
  vendor.update(max_price: price) if price > vendor.max_price
@@ -116,7 +128,7 @@ module SpreeCmCommissioner
116
128
  end
117
129
 
118
130
  def sync_trip
119
- return unless product.product_type == 'transit'
131
+ return unless transit?
120
132
 
121
133
  trip = SpreeCmCommissioner::Trip.find_or_initialize_by(variant_id: id)
122
134
 
@@ -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