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
@@ -10,6 +10,8 @@ module SpreeCmCommissioner
10
10
  assign_prototype
11
11
  create_child_taxon
12
12
  build_home_banner
13
+ assign_option_types
14
+ assign_option_values
13
15
  end
14
16
  end
15
17
 
@@ -61,5 +63,26 @@ module SpreeCmCommissioner
61
63
 
62
64
  context.fail!(message: 'Home banner upload failed') unless banner.persisted?
63
65
  end
66
+
67
+ def assign_options(model_class, param_key, foreign_key)
68
+ return unless params[param_key]
69
+
70
+ params[param_key].each do |id|
71
+ record = model_class.new(
72
+ taxon_id: @parent_taxon.id,
73
+ foreign_key => id
74
+ )
75
+
76
+ context.fail!(message: record.errors.full_messages.join(', ')) unless record.save
77
+ end
78
+ end
79
+
80
+ def assign_option_types
81
+ assign_options(SpreeCmCommissioner::TaxonOptionType, :option_type_id, :option_type_id)
82
+ end
83
+
84
+ def assign_option_values
85
+ assign_options(SpreeCmCommissioner::TaxonOptionValue, :option_value_id, :option_value_id)
86
+ end
64
87
  end
65
88
  end
@@ -0,0 +1,40 @@
1
+ module SpreeCmCommissioner
2
+ class EnsureCorrectProductType < BaseInteractor
3
+ def call
4
+ Spree::Product
5
+ .left_joins(variants_including_master: %i[inventory_items line_items])
6
+ .where(
7
+ 'spree_variants.product_type IS NULL OR
8
+ spree_variants.product_type != spree_products.product_type OR
9
+
10
+ cm_inventory_items.product_type IS NULL OR
11
+ cm_inventory_items.product_type != spree_products.product_type OR
12
+
13
+ spree_line_items.product_type IS NULL OR
14
+ spree_line_items.product_type != spree_products.product_type OR
15
+
16
+ spree_products.product_type IS NOT NULL
17
+ '
18
+ )
19
+ .distinct.find_each do |product|
20
+ sync_product_type_for(product)
21
+ end
22
+ end
23
+
24
+ def sync_product_type_for(product)
25
+ product_type = Spree::Variant.product_types[product.product_type]
26
+
27
+ product.variants_including_master
28
+ .where('spree_variants.product_type IS NULL OR spree_variants.product_type != ?', product_type)
29
+ .update_all(product_type: product_type) # rubocop:disable Rails/SkipsModelValidations
30
+
31
+ product.line_items
32
+ .where('spree_line_items.product_type IS NULL OR spree_line_items.product_type != ?', product_type)
33
+ .update_all(product_type: product_type) # rubocop:disable Rails/SkipsModelValidations
34
+
35
+ product.inventory_items
36
+ .where('cm_inventory_items.product_type IS NULL OR cm_inventory_items.product_type != ?', product_type)
37
+ .update_all(product_type: product_type) # rubocop:disable Rails/SkipsModelValidations
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,25 @@
1
+ module SpreeCmCommissioner
2
+ class InventoryItemSyncer < BaseInteractor
3
+ # inventory_id_and_quantities = [{ inventory_id: inventory_item1.id, quantity: 5 } ]
4
+ delegate :inventory_id_and_quantities, to: :context
5
+
6
+ def call
7
+ ActiveRecord::Base.transaction do
8
+ inventory_items.each do |inventory_item|
9
+ quantity = inventory_id_and_quantities.find { |item| item[:inventory_id] == inventory_item.id }&.dig(:quantity) || 0
10
+ adjust_quantity_available(inventory_item, quantity)
11
+ end
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def adjust_quantity_available(inventory_item, quantity)
18
+ inventory_item.update!(quantity_available: inventory_item.quantity_available + quantity)
19
+ end
20
+
21
+ def inventory_items
22
+ @inventory_items ||= InventoryItem.where(id: inventory_id_and_quantities.pluck(:inventory_id))
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeCmCommissioner
2
+ module Stock
3
+ class InventoryItemsAdjuster < BaseInteractor
4
+ delegate :variant, :quantity, to: :context
5
+
6
+ def call
7
+ variant.inventory_items.active.find_each do |inventory_item|
8
+ inventory_item.adjust_quantity!(quantity)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,75 @@
1
+ module SpreeCmCommissioner
2
+ module Stock
3
+ class PermanentInventoryItemsGenerator < BaseInteractor
4
+ delegate :variant_ids, to: :context
5
+
6
+ def variants_per_batch = 1000
7
+
8
+ def pre_inventory_days_for(variant)
9
+ context.pre_inventory_days || variant.pre_inventory_days
10
+ end
11
+
12
+ def call
13
+ variants.in_batches(of: variants_per_batch) do |batch|
14
+ generate_inventory_items_for_batch(batch)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def generate_inventory_items_for_batch(batch)
21
+ total_on_hand_by_variant = total_on_hand_for(batch)
22
+ batch.each do |variant|
23
+ count_on_hand = total_on_hand_by_variant[variant.id] || 0
24
+ generate_inventory_items_for_variant(variant, count_on_hand)
25
+ end
26
+ end
27
+
28
+ def generate_inventory_items_for_variant(variant, count_on_hand)
29
+ inventory_dates_for(variant).each do |inventory_date|
30
+ next if inventory_exist?(variant, inventory_date)
31
+
32
+ create_inventory_item(variant, inventory_date, count_on_hand)
33
+ end
34
+ end
35
+
36
+ def inventory_dates_for(variant)
37
+ start_date = Time.zone.tomorrow
38
+ end_date = Time.zone.today + pre_inventory_days_for(variant)
39
+
40
+ (start_date..end_date)
41
+ end
42
+
43
+ def inventory_exist?(variant, inventory_date)
44
+ variant.inventory_items.exists?(inventory_date: inventory_date)
45
+ end
46
+
47
+ def create_inventory_item(variant, inventory_date, count_on_hand)
48
+ variant.inventory_items.create!(
49
+ inventory_date: inventory_date,
50
+ quantity_available: count_on_hand,
51
+ max_capacity: count_on_hand,
52
+ product_type: variant.product_type
53
+ )
54
+ end
55
+
56
+ # Returns a hash: { variant_id => total_on_hand, ... }
57
+ def total_on_hand_for(variants)
58
+ variant_ids = variants.pluck(:id)
59
+
60
+ Spree::StockItem
61
+ .joins(:stock_location)
62
+ .where(deleted_at: nil, variant_id: variant_ids)
63
+ .where(spree_stock_locations: { active: true })
64
+ .group(:variant_id)
65
+ .sum(:count_on_hand)
66
+ end
67
+
68
+ def variants
69
+ scope = Spree::Variant.active.with_permanent_stock.where(is_master: false)
70
+ scope = scope.where(id: variant_ids) if variant_ids.present?
71
+ scope
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,24 @@
1
+ module SpreeCmCommissioner
2
+ module Stock
3
+ class StockMovementCreator < BaseInteractor
4
+ delegate :variant_id, :stock_location_id, :current_store, :stock_movement_params, to: :context
5
+
6
+ def call
7
+ variant = current_store.variants.find(variant_id)
8
+
9
+ return context.fail!(message: Spree.t(:doesnt_track_inventory)) unless variant.track_inventory?
10
+
11
+ stock_location = Spree::StockLocation.find(stock_location_id)
12
+ stock_movement = stock_location.stock_movements.build(stock_movement_params)
13
+ stock_movement.stock_item = stock_location.set_up_stock_item(variant)
14
+
15
+ if stock_movement.save
16
+ SpreeCmCommissioner::Stock::InventoryItemsAdjusterJob.perform_later(variant_id: variant.id, quantity: stock_movement.quantity)
17
+ context.stock_movement = stock_movement
18
+ else
19
+ context.fail!(message: stock_movement.errors.full_messages.join(', '))
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -25,10 +25,8 @@ module SpreeCmCommissioner
25
25
  end
26
26
 
27
27
  def verify_signature
28
- public_key = ENV['VATTANAC_PUBLIC_KEY'].presence || Rails.application.credentials.vattanac.public_key
29
-
30
28
  rsa_service = SpreeCmCommissioner::RsaService.new(
31
- public_key: public_key
29
+ public_key: vattanac_public_key
32
30
  )
33
31
 
34
32
  return if rsa_service.verify(context.encrypted_data, context.signature)
@@ -37,8 +35,6 @@ module SpreeCmCommissioner
37
35
  end
38
36
 
39
37
  def decrypt_payload
40
- aes_key = ENV['VATTANAC_AES_SECRET_KEY'].presence || Rails.application.credentials.vattanac.aes_secret_key
41
-
42
38
  context.fail!(message: 'Invalid AES key length', status: :unprocessable_entity) unless aes_key
43
39
 
44
40
  begin
@@ -57,7 +53,7 @@ module SpreeCmCommissioner
57
53
  user_data = context.user_data
58
54
 
59
55
  identity = SpreeCmCommissioner::UserIdentityProvider.vattanac_bank
60
- .find_or_initialize_by(sub: user_data['id'])
56
+ .find_or_initialize_by(sub: user_data['phoneNum'])
61
57
 
62
58
  if identity.persisted?
63
59
  context.user = identity.user
@@ -82,7 +78,7 @@ module SpreeCmCommissioner
82
78
  identity = context.identity
83
79
 
84
80
  identity.name = full_name
85
-
81
+ identity.email = user_data['email']
86
82
  context.user = Spree::User.new(
87
83
  first_name: user_data['firstName'],
88
84
  last_name: user_data['lastName'],
@@ -99,18 +95,41 @@ module SpreeCmCommissioner
99
95
 
100
96
  def construct_data
101
97
  user = context.user
102
- context.data = {
98
+
99
+ raw_data = {
103
100
  sessionId: session_id,
104
101
  name: user.full_name,
105
102
  phone: user.phone_number,
106
103
  email: user.email,
107
104
  webUrl: "#{Spree::Store.default.formatted_url}/vattanac_bank_web_app?session_id=#{session_id}"
108
105
  }
106
+
107
+ json_data = raw_data.to_json
108
+
109
+ encrypted_data = SpreeCmCommissioner::AesEncryptionService.encrypt(json_data, aes_key)
110
+
111
+ rsa_service = SpreeCmCommissioner::RsaService.new(private_key: bookmeplus_private_key)
112
+
113
+ signed_data = rsa_service.sign(encrypted_data)
114
+
115
+ context.data = signed_data
109
116
  end
110
117
 
111
118
  def session_id
112
119
  payload = { user_id: context.user.id }
113
120
  SpreeCmCommissioner::UserSessionJwtToken.encode(payload, context.user.reload.secure_token)
114
121
  end
122
+
123
+ def aes_key
124
+ ENV['VATTANAC_AES_SECRET_KEY'].presence || Rails.application.credentials.vattanac.aes_secret_key
125
+ end
126
+
127
+ def bookmeplus_private_key
128
+ ENV['BOOKMEPLUS_PRIVATE_KEY'].presence || Rails.application.credentials.bookmeplus.private_key
129
+ end
130
+
131
+ def vattanac_public_key
132
+ ENV['VATTANAC_PUBLIC_KEY'].presence || Rails.application.credentials.vattanac.public_key
133
+ end
115
134
  end
116
135
  end
@@ -0,0 +1,7 @@
1
+ module SpreeCmCommissioner
2
+ class EnsureCorrectProductTypeJob < ApplicationJob
3
+ def perform
4
+ SpreeCmCommissioner::EnsureCorrectProductType.call
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module SpreeCmCommissioner
2
+ class InventoryItemSyncerJob < ApplicationUniqueJob
3
+ def perform(inventory_id_and_quantities:)
4
+ InventoryItemSyncer.call(inventory_id_and_quantities:)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ module Stock
3
+ class InventoryItemsAdjusterJob < ApplicationUniqueJob
4
+ def perform(variant_id:, quantity:)
5
+ variant = Spree::Variant.find(variant_id)
6
+
7
+ SpreeCmCommissioner::Stock::InventoryItemsAdjuster.call(variant: variant, quantity: quantity)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ module Stock
3
+ class PermanentInventoryItemsGeneratorJob < ApplicationUniqueJob
4
+ def perform
5
+ SpreeCmCommissioner::Stock::PermanentInventoryItemsGenerator.call
6
+ end
7
+ end
8
+ end
9
+ end
@@ -13,10 +13,13 @@ module SpreeCmCommissioner
13
13
  state_machine.after_transition to: :complete, do: :notify_order_complete_telegram_notification_to_user, unless: :subscription?
14
14
  state_machine.after_transition to: :complete, do: :send_order_complete_telegram_alert_to_vendors, unless: :need_confirmation?
15
15
  state_machine.after_transition to: :complete, do: :send_order_complete_telegram_alert_to_store, unless: :need_confirmation?
16
+ state_machine.around_transition to: :complete, do: :handle_unstock_in_redis
16
17
 
17
18
  state_machine.after_transition to: :resumed, do: :precalculate_conversion
19
+ state_machine.around_transition to: :resumed, do: :handle_unstock_in_redis
18
20
 
19
21
  state_machine.after_transition to: :canceled, do: :precalculate_conversion
22
+ state_machine.after_transition to: :canceled, do: :restock_inventory_in_redis!
20
23
 
21
24
  scope :accepted, -> { where(request_state: 'accepted') }
22
25
 
@@ -66,6 +69,29 @@ module SpreeCmCommissioner
66
69
  end
67
70
  end
68
71
 
72
+ def handle_unstock_in_redis
73
+ ActiveRecord::Base.transaction do
74
+ yield # Equal to block.call
75
+
76
+ # After the transition is complete, the following code will execute first before proceeding to other `after_transition` callbacks.
77
+ # This ensures that if `unstock_inventory_in_redis!` fails, the state will be rolled back,
78
+ # and neither the `finalize!` method nor any notifications will be triggered.
79
+ # The payment will be reversed in vPago gem, and `Spree::Checkout::Complete` will be called, which checks `order.reload.complete?`.
80
+ # This is critical because if the order state is complete, the payment will be marked as paid.
81
+ CmAppLogger.log(label: 'order_state_machine_before_unstock', data: { order_id: id, state: state })
82
+ unstock_inventory_in_redis!
83
+ # We rollback only order state, and we keep payment state as it is.
84
+ # We implement payment in vPago gem, and it will be reversed in the gem.
85
+ # Some bank has api for refund, but some don't have the api to refund yet. So we keep the payment state as it is and refund manually.
86
+ CmAppLogger.log(label: 'order_state_machine_after_unstock', data: { order_id: id, state: state })
87
+ end
88
+ rescue StandardError => e
89
+ CmAppLogger.log(label: 'order_state_machine',
90
+ data: { order_id: id, error: e.message, type: e.class.name, backtrace: e.backtrace.first(5).join("\n") }
91
+ )
92
+ raise e
93
+ end
94
+
69
95
  def generate_bib_number
70
96
  line_items.find_each(&:generate_remaining_guests)
71
97
 
@@ -3,12 +3,10 @@ module SpreeCmCommissioner
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- delegate :product_type,
7
- :subscribable?,
6
+ delegate :subscribable?,
8
7
  :allowed_upload_later?,
9
8
  :need_confirmation?, :need_confirmation, :kyc,
10
9
  :allow_anonymous_booking,
11
- :accommodation?, :service?, :ecommerce?,
12
10
  :associated_event,
13
11
  :allow_self_check_in,
14
12
  :allow_self_check_in?,
@@ -6,10 +6,20 @@ module SpreeCmCommissioner
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  PRODUCT_TYPES = %i[accommodation service ecommerce transit].freeze
9
+ PERMANENT_STOCK_PRODUCT_TYPES = %w[accommodation service transit].freeze
10
+ PRE_INVENTORY_DAYS = { 'transit' => 90, 'accommodation' => 365, 'service' => 30 }.freeze
9
11
 
10
12
  included do
11
13
  enum product_type: PRODUCT_TYPES if table_exists? && column_names.include?('product_type')
12
14
  enum primary_product_type: PRODUCT_TYPES if table_exists? && column_names.include?('primary_product_type')
13
15
  end
16
+
17
+ def permanent_stock?
18
+ PERMANENT_STOCK_PRODUCT_TYPES.include?(product_type)
19
+ end
20
+
21
+ def pre_inventory_days
22
+ PRE_INVENTORY_DAYS[product_type]
23
+ end
14
24
  end
15
25
  end
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ class Inventory
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor :variant_id, :inventory_date, :quantity_available, :max_capacity, :product_type
6
+
7
+ validates :variant_id, presence: true
8
+ validates :quantity_available, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
9
+ validates :max_capacity, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
10
+ end
11
+ end
@@ -0,0 +1,55 @@
1
+ module SpreeCmCommissioner
2
+ class InventoryItem < ApplicationRecord
3
+ include SpreeCmCommissioner::ProductType
4
+
5
+ # Association
6
+ belongs_to :variant, class_name: 'Spree::Variant'
7
+
8
+ # Validation
9
+ validates :quantity_available, numericality: { greater_than_or_equal_to: 0 }
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?
12
+ validates :variant_id, uniqueness: { scope: :inventory_date, message: -> (object, _data) { "The variant is taken on #{object.inventory_date}" } }
13
+
14
+ # Scope
15
+ scope :active, -> { where(inventory_date: nil).or(where('inventory_date >= ?', Time.zone.today)) }
16
+
17
+ before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
18
+
19
+ def adjust_quantity!(quantity)
20
+ with_lock do
21
+ self.max_capacity = max_capacity + quantity
22
+ self.quantity_available = quantity_available + quantity
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
42
+ end
43
+ end
44
+
45
+ def active?
46
+ inventory_date.nil? || inventory_date >= Time.zone.today
47
+ end
48
+
49
+ def redis_expired_in
50
+ expired_in = 31_536_000 # 1 year for normal stock
51
+ expired_in = Time.parse(inventory_date.to_s).end_of_day.to_i - Time.zone.now.to_i if inventory_date.present?
52
+ [expired_in, 0].max
53
+ end
54
+ end
55
+ end
@@ -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,6 +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
+
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
+
12
17
  base.has_many :taxons, class_name: 'Spree::Taxon', through: :product
13
18
  base.has_many :guests, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
14
19
  base.has_many :pending_guests, pending_guests_query, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
@@ -21,16 +26,15 @@ module SpreeCmCommissioner
21
26
  base.validate :validate_seats_reservation, if: :transit?
22
27
 
23
28
  base.before_create :add_due_date, if: :subscription?
29
+ base.before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
24
30
 
25
31
  base.validate :ensure_not_exceed_max_quantity_per_order, if: -> { variant&.max_quantity_per_order.present? }
26
32
 
27
33
  base.whitelisted_ransackable_associations |= %w[guests order]
28
34
  base.whitelisted_ransackable_attributes |= %w[number to_date from_date vendor_id]
29
35
 
30
- base.delegate :delivery_required?, :permanent_stock?, :high_demand, :transit?,
36
+ base.delegate :delivery_required?, :high_demand,
31
37
  to: :variant
32
- base.delegate :discontinue_on, :product_type, :accommodation?, :service?, :ecommerce?, :need_confirmation,
33
- to: :product
34
38
 
35
39
  base.accepts_nested_attributes_for :guests, allow_destroy: true
36
40
  base.accepts_nested_attributes_for :line_item_seats, allow_destroy: true
@@ -41,6 +45,10 @@ module SpreeCmCommissioner
41
45
  json_api_columns << :vendor_id
42
46
  end
43
47
 
48
+ def discontinue_on
49
+ variant.discontinue_on || product.discontinue_on
50
+ end
51
+
44
52
  def base.search_by_qr_data!(data)
45
53
  matches = data.match(/(R\d+)-([A-Za-z0-9_\-]+)-(L\d+)/)&.captures
46
54
 
@@ -59,6 +67,7 @@ module SpreeCmCommissioner
59
67
  base.include SpreeCmCommissioner::LineItemDurationable
60
68
  base.include SpreeCmCommissioner::LineItemsFilterScope
61
69
  base.include SpreeCmCommissioner::LineItemGuestsConcern
70
+ base.include SpreeCmCommissioner::ProductType
62
71
  base.include SpreeCmCommissioner::ProductDelegation
63
72
  base.include SpreeCmCommissioner::KycBitwise
64
73
  end
@@ -174,7 +183,7 @@ module SpreeCmCommissioner
174
183
 
175
184
  # override
176
185
  def sufficient_stock?
177
- return transit_sufficient_stock? if variant.product.product_type == 'transit'
186
+ return transit_sufficient_stock? if transit?
178
187
 
179
188
  SpreeCmCommissioner::Stock::LineItemAvailabilityChecker.new(self).can_supply?(quantity)
180
189
  end
@@ -230,6 +239,8 @@ module SpreeCmCommissioner
230
239
  end
231
240
 
232
241
  def validate_seats_reservation
242
+ return if reservation_trip.blank?
243
+
233
244
  if reservation_trip.allow_seat_selection && !selected_seats_available?
234
245
  errors.add(:base, :some_seats_are_booked, message: 'Some seats are already booked')
235
246
  elsif !reservation_trip.allow_seat_selection && !seat_quantity_available?(reservation_trip)
@@ -58,6 +58,13 @@ module SpreeCmCommissioner
58
58
  end
59
59
  end
60
60
 
61
+ # override
62
+ # spree use this method to check stock availability & consider whether :order can continue to next state.
63
+ def insufficient_stock_lines
64
+ checker = SpreeCmCommissioner::Stock::OrderAvailabilityChecker.new(self)
65
+ checker.insufficient_stock_lines
66
+ end
67
+
61
68
  def ticket_seller_user?
62
69
  return false if user.nil?
63
70
 
@@ -194,6 +201,14 @@ module SpreeCmCommissioner
194
201
 
195
202
  private
196
203
 
204
+ def unstock_inventory_in_redis!
205
+ SpreeCmCommissioner::RedisStock::InventoryUpdater.new(line_item_ids).unstock!
206
+ end
207
+
208
+ def restock_inventory_in_redis!
209
+ SpreeCmCommissioner::RedisStock::InventoryUpdater.new(line_item_ids).restock!
210
+ end
211
+
197
212
  # override :spree_api
198
213
  def webhook_payload_body
199
214
  resource_serializer.new(
@@ -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
@@ -33,6 +33,7 @@ module SpreeCmCommissioner
33
33
  base.has_one :google_wallet, class_name: 'SpreeCmCommissioner::GoogleWallet', dependent: :destroy
34
34
 
35
35
  base.has_many :complete_line_items, through: :classifications, source: :line_items
36
+ base.has_many :inventory_items, through: :variants
36
37
 
37
38
  base.has_many :product_places, class_name: 'SpreeCmCommissioner::ProductPlace', dependent: :destroy
38
39
  base.has_many :places, through: :product_places
@@ -57,9 +58,8 @@ module SpreeCmCommissioner
57
58
  base.scope :subscribable, -> { where(subscribable: 1) }
58
59
 
59
60
  base.validate :validate_event_taxons, if: -> { taxons.event.present? }
60
-
61
61
  base.validate :validate_product_date, if: -> { available_on.present? && discontinue_on.present? }
62
-
62
+ base.validate :product_type_unchanged, on: :update
63
63
  base.validates :commission_rate, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true
64
64
 
65
65
  base.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status vendor_id short_name route_type]
@@ -102,6 +102,13 @@ module SpreeCmCommissioner
102
102
 
103
103
  errors.add(:discontinue_on, 'must be after the available on date')
104
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
105
112
  end
106
113
  end
107
114