spree_cm_commissioner 2.5.13.pre.pre.pre.3 → 2.5.13

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +3 -0
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/spree/admin/classifications_controller.rb +10 -3
  5. data/app/controllers/spree/admin/inventory_items_controller.rb +3 -3
  6. data/app/controllers/spree/admin/inventory_monitorings_controller.rb +39 -0
  7. data/app/controllers/spree/admin/stock_managements_controller.rb +5 -1
  8. data/app/controllers/spree/api/v2/operator/recalculate_tickets_controller.rb +4 -9
  9. data/app/controllers/spree/api/v2/tenant/guests_controller.rb +3 -6
  10. data/app/controllers/spree_cm_commissioner/orders_controller.rb +14 -2
  11. data/app/finders/spree_cm_commissioner/inventory_items/recently_changed_finder.rb +88 -0
  12. data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +11 -1
  13. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +7 -3
  14. data/app/jobs/spree_cm_commissioner/maintenance_tasks/orchestrate_job.rb +28 -0
  15. data/app/jobs/spree_cm_commissioner/maintenance_tasks/process_job.rb +18 -0
  16. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +16 -11
  17. data/app/models/concerns/spree_cm_commissioner/product_relation_type.rb +29 -0
  18. data/app/models/spree_cm_commissioner/exports/operator_guest_json_gzip.rb +123 -37
  19. data/app/models/spree_cm_commissioner/inventory_item.rb +5 -3
  20. data/app/models/spree_cm_commissioner/maintenance_task.rb +62 -0
  21. data/app/models/spree_cm_commissioner/maintenance_tasks/event.rb +61 -0
  22. data/app/models/spree_cm_commissioner/order_decorator.rb +8 -0
  23. data/app/models/spree_cm_commissioner/pricing_model.rb +1 -1
  24. data/app/models/spree_cm_commissioner/product_decorator.rb +5 -2
  25. data/app/models/spree_cm_commissioner/product_relation.rb +23 -0
  26. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +1 -0
  27. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +20 -6
  28. data/app/overrides/spree/admin/shared/sub_menu/_stock/inventory_monitorings_tab.html.erb.deface +3 -0
  29. data/app/serializers/spree/api/v2/platform/create_guest_adjustment_serializer.rb +10 -0
  30. data/app/serializers/spree/api/v2/platform/create_line_item_adjustment_serializer.rb +10 -0
  31. data/app/serializers/spree/api/v2/platform/create_route_adjustment_serializer.rb +10 -0
  32. data/app/serializers/spree/api/v2/platform/pricing_action_serializer.rb +15 -0
  33. data/app/serializers/spree/v2/tenant/cart_serializer.rb +4 -0
  34. data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +39 -0
  35. data/app/services/spree_cm_commissioner/pricing_models/update_with_rule_groups.rb +1 -6
  36. data/app/services/spree_cm_commissioner/trips/clone.rb +195 -0
  37. data/app/services/spree_cm_commissioner/trips/create_single_leg.rb +2 -0
  38. data/app/views/spree/admin/inventory_monitorings/index.html.erb +119 -0
  39. data/app/views/spree/admin/stock_managements/index.html.erb +1 -1
  40. data/app/views/spree_cm_commissioner/layouts/order_mailer.html.erb +15 -0
  41. data/config/locales/en.yml +5 -0
  42. data/config/routes.rb +5 -4
  43. data/db/migrate/20260202095500_create_cm_product_relations.rb +32 -0
  44. data/db/migrate/20260218100000_create_cm_maintenance_tasks.rb +23 -0
  45. data/lib/spree_cm_commissioner/cached_inventory_item.rb +8 -7
  46. data/lib/spree_cm_commissioner/test_helper/factories/product_relation_factory.rb +9 -0
  47. data/lib/spree_cm_commissioner/transit/trip_form.rb +1 -0
  48. data/lib/spree_cm_commissioner/version.rb +1 -1
  49. metadata +23 -14
  50. data/app/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +0 -63
  51. data/app/interactors/spree_cm_commissioner/conversion_pre_calculator.rb +0 -64
  52. data/app/interactors/spree_cm_commissioner/enqueue_cart/add_item.rb +0 -43
  53. data/app/interactors/spree_cm_commissioner/enqueue_cart/add_item_status_marker.rb +0 -66
  54. data/app/interactors/spree_cm_commissioner/trip_clone_creator.rb +0 -138
  55. data/app/jobs/spree_cm_commissioner/conversion_pre_calculator_job.rb +0 -12
  56. data/app/jobs/spree_cm_commissioner/enqueue_cart/add_item_job.rb +0 -17
  57. data/app/serializables/spree_cm_commissioner/queue_item.rb +0 -13
  58. data/app/serializers/spree/v2/storefront/cart_serializer_decorator.rb +0 -23
  59. data/app/serializers/spree/v2/storefront/firestore_queue_serializer.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abe974606cb45f190fc9763b7e50c693e2349044545ad369da2cf595433a2ea4
4
- data.tar.gz: 2e661fab59d721c66ef210688322477a59d2a2fc39fa5811b42f1699209872c7
3
+ metadata.gz: 3e4173db9eaef31ba44153b210477bd83e359559c6b007f644723d320e9e8f30
4
+ data.tar.gz: 38aba375ecb1177922443efbe00c09e923c75f3f2547c3df5cb383378e45a8b8
5
5
  SHA512:
6
- metadata.gz: 05ff40748bfe49857e34a430dc47c1e0c809b2a6e7b40b6e8a326c21655dfe9a4493fc5bfefff857825dfa42a7cbfed926f405e86916ce705fb826ad523bcdbb
7
- data.tar.gz: 31a426d613c9ba438c843ee92cc7fcc2ed26ae53c474caaa7dc5f099c5abc5f753de816ab08cb266f1a679e0a6309862ce5de8e747837fc0afaf3d8af46202b7
6
+ metadata.gz: 698f5ca602839ec3b8293cd2aecf31a1cc89d6125c16b831a8500ec0aa8c01d705bf15bc215dde6cd50de568c3d505803cf97ad4c4c6a4493f7018f7b535dde1
7
+ data.tar.gz: ef9ffdee2d499c102c1f8e2c7166d3ed0d3ccea24f9f6259d74555f4cdc8f7646b9e97ae373710f1412cf256f510e0296259e44fa5d693a19d927b9eecf51396
data/.env.example CHANGED
@@ -40,3 +40,6 @@ CACHE_SEMI_STATIC_MAX_AGE=3600 # Semi-static content (menus, homepage background
40
40
  CACHE_MODERATE_MAX_AGE=1800 # Moderate freshness (products, events, vendors) - 30 minutes
41
41
  CACHE_REALTIME_MAX_AGE=300 # High freshness (trips, trip search) - 5 minutes
42
42
  CACHE_DEFAULT_MAX_AGE=300 # Default for unlisted controllers - 5 minutes
43
+
44
+ # Batch size for processing maintenance tasks in SpreeCmCommissioner::MaintenanceTasks::OrchestrateJob
45
+ MAINTENANCE_TASKS_BATCH_SIZE=100
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.5.13.pre.pre.pre.3)
37
+ spree_cm_commissioner (2.5.13)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -4,11 +4,18 @@ module Spree
4
4
  before_action :load_taxon_and_products
5
5
 
6
6
  def recalculate_conversions
7
- @taxon.products.each do |product|
8
- SpreeCmCommissioner::ConversionPreCalculatorJob.perform_later(product_id: product.id)
7
+ if @taxon.parent.event?
8
+ SpreeCmCommissioner::MaintenanceTasks::Event.pending.find_or_create_by(
9
+ maintainable_type: 'Spree::Taxon',
10
+ maintainable_id: @taxon.parent.id,
11
+ manually_triggered: true
12
+ ).async_execute
13
+
14
+ flash[:success] = flash_message_for(@taxon, :successfully_updated)
15
+ else
16
+ flash[:error] = 'Conversion recalculation can only be performed for taxons under an event.' # rubocop:disable Rails/I18nLocaleTexts
9
17
  end
10
18
 
11
- flash[:success] = flash_message_for(@taxon, :successfully_updated)
12
19
  redirect_to collection_url
13
20
  end
14
21
 
@@ -42,8 +42,8 @@ module Spree
42
42
  inventory_items = @product.inventory_items.where(id: inventory_item_ids)
43
43
  target_quantity = params[:quantity].to_i
44
44
 
45
- if target_quantity.zero?
46
- flash[:error] = "Target quantity (#{target_quantity}) must be greater than zero"
45
+ if target_quantity.negative?
46
+ flash[:error] = "Target quantity (#{target_quantity}) must not be negative"
47
47
  return redirect_back(fallback_location: admin_product_stock_managements_path(@product))
48
48
  end
49
49
 
@@ -52,7 +52,7 @@ module Spree
52
52
  quantity_change = target_quantity - inventory_item.quantity_available
53
53
 
54
54
  # Use adjust_quantity! to update max_capacity, quantity_available, and sync to Redis
55
- inventory_item.adjust_quantity!(quantity_change) if quantity_change != 0
55
+ inventory_item.adjust_quantity!(quantity_change)
56
56
  end
57
57
 
58
58
  flash[:success] = "Successfully updated stocks for #{inventory_items.count} items"
@@ -0,0 +1,39 @@
1
+ module Spree
2
+ module Admin
3
+ class InventoryMonitoringsController < BaseController
4
+ def index
5
+ authorize! :manage, SpreeCmCommissioner::InventoryItem
6
+
7
+ @time_range = params[:time_range] || 7
8
+ @filter_type = params[:filter_type] || 'all'
9
+ @vendor_id = params[:vendor_id]
10
+
11
+ finder = SpreeCmCommissioner::InventoryItems::RecentlyChangedFinder.new(
12
+ time_range: @time_range.to_i.days.ago,
13
+ limit: 1000,
14
+ vendor_id: @vendor_id,
15
+ filter_type: @filter_type
16
+ )
17
+
18
+ @inventory_items = finder.execute
19
+ @total_count = @inventory_items.size
20
+ @out_of_sync_count = @inventory_items.count { |item| item[:out_of_sync] }
21
+ end
22
+
23
+ def reset
24
+ authorize! :manage, SpreeCmCommissioner::InventoryItem
25
+
26
+ inventory_item = SpreeCmCommissioner::InventoryItem.find(params[:id])
27
+ result = SpreeCmCommissioner::Stock::InventoryItemResetter.call(inventory_item: inventory_item)
28
+
29
+ if result.success?
30
+ flash[:success] = "Successfully reset inventory for #{inventory_item.variant.product.name}"
31
+ else
32
+ flash[:error] = "Failed to reset inventory: #{result.message}"
33
+ end
34
+
35
+ redirect_to action: :index, time_range: params[:time_range], filter_type: params[:filter_type]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -14,7 +14,11 @@ module Spree
14
14
  def index
15
15
  @variants = @product.variants.includes(:images, :default_price, stock_items: :stock_location, option_values: :option_type)
16
16
  @variants = [@product.master] if @variants.empty?
17
- @stock_locations = (@variants.flat_map(&:stock_locations) + @product.vendor.stock_locations).uniq
17
+
18
+ vendor_stock_locations = @product.vendor&.stock_locations || []
19
+ variant_stock_locations = @variants.flat_map(&:stock_locations).uniq
20
+
21
+ @stock_locations = (variant_stock_locations + vendor_stock_locations).uniq
18
22
 
19
23
  load_inventories unless @product.permanent_stock?
20
24
  end
@@ -7,13 +7,9 @@ module Spree
7
7
  before_action :load_taxon, only: :create
8
8
 
9
9
  def create
10
- classification_ids = Spree::Classification.joins(:taxon)
11
- .where(spree_taxons: { id: @taxons.pluck(:id) })
12
- .pluck(:id)
13
-
14
- classification_ids.each do |classification_id|
15
- SpreeCmCommissioner::ConversionPreCalculator.call(product_taxon: Spree::Classification.find(classification_id))
16
- end
10
+ SpreeCmCommissioner::MaintenanceTasks::Event.pending.find_or_create_by(
11
+ maintainable: @taxon
12
+ ).async_execute
17
13
 
18
14
  render json: { message: 'Conversions recalculated successfully' }, status: :ok
19
15
  end
@@ -21,8 +17,7 @@ module Spree
21
17
  private
22
18
 
23
19
  def load_taxon
24
- parent_taxon = Spree::Taxon.find(params[:taxon_id])
25
- @taxons = parent_taxon.children
20
+ @taxon = Spree::Taxon.find(params[:taxon_id])
26
21
  end
27
22
  end
28
23
  end
@@ -34,9 +34,9 @@ module Spree
34
34
 
35
35
  ActiveRecord::Base.transaction do
36
36
  resource.save!
37
+ recalculate_cart(resource.line_item)
37
38
  run_dynamic_fields(resource)
38
39
  resource.save_and_move_to_next_stage
39
- recalculate_cart(resource.line_item)
40
40
  end
41
41
 
42
42
  render_serialized_payload(201) { serialize_resource(resource) }
@@ -49,8 +49,8 @@ module Spree
49
49
  run_dynamic_fields(resource) if guest_params[:guest_dynamic_fields_attributes]
50
50
  resource.assign_attributes(guest_params.except(:guest_dynamic_fields_attributes))
51
51
  resource.save!
52
- resource.save_and_move_to_next_stage
53
52
  recalculate_cart(resource.line_item)
53
+ resource.save_and_move_to_next_stage
54
54
  end
55
55
 
56
56
  render_serialized_payload { serialize_resource(resource) }
@@ -117,10 +117,7 @@ module Spree
117
117
  end
118
118
 
119
119
  def recalculate_cart(line_item)
120
- ::Spree::Dependencies.cart_recalculate_service.constantize.call(
121
- line_item: line_item,
122
- order: spree_current_order
123
- )
120
+ ::Spree::Dependencies.cart_recalculate_service.constantize.call(line_item: line_item, order: spree_current_order)
124
121
  end
125
122
  end
126
123
  end
@@ -3,14 +3,26 @@ module SpreeCmCommissioner
3
3
  layout 'spree_cm_commissioner/layouts/order_mailer'
4
4
  helper 'spree/mail'
5
5
 
6
- def show
6
+ def show # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
7
7
  @order = if params[:id].match?(/^R\d{9,}-([A-Za-z0-9_\-]+)$/)
8
8
  Spree::Order.search_by_qr_data!(params[:id])
9
9
  else
10
- SpreeCmCommissioner::Orders::JwtToken::Verify.call(token: params[:t]).value[:order]
10
+ result = SpreeCmCommissioner::Orders::JwtToken::Verify.call(token: params[:t])
11
+ unless result.success?
12
+ @title = I18n.t('views.sign_in.errors.request_invalid')
13
+ @message = I18n.t('views.sign_in.errors.request_invalid_message')
14
+
15
+ return render(
16
+ template: 'shared_console/errors/request_invalid',
17
+ layout: 'error'
18
+ )
19
+ end
20
+
21
+ result.value[:order]
11
22
  end
12
23
 
13
24
  @product_type = @order.products.first&.product_type || 'accommodation'
25
+ @expired_at = Time.zone.at(result&.value&.dig(:payload, 'exp')) if result&.value&.dig(:payload, 'exp')
14
26
 
15
27
  if @order.tenant.present?
16
28
  @name = @order.tenant.name
@@ -0,0 +1,88 @@
1
+ module SpreeCmCommissioner
2
+ module InventoryItems
3
+ class RecentlyChangedFinder
4
+ def initialize(time_range: 7.days.ago, limit: 1000, vendor_id: nil, filter_type: 'all')
5
+ @time_range = time_range
6
+ @limit = limit
7
+ @vendor_id = vendor_id
8
+ @filter_type = filter_type
9
+ end
10
+
11
+ # Finds recently changed items with Redis comparison
12
+ # Returns array of hashes sorted by biggest discrepancy first
13
+ def execute
14
+ items = fetch_recent_items
15
+ items_with_comparison = fetch_quantity_in_redis_batch(items)
16
+
17
+ # Sort by biggest discrepancy first
18
+ items_with_comparison.sort_by { |item| (item[:quantity_in_redis].to_i - item[:quantity_available]).abs }.reverse
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_recent_items
24
+ query = SpreeCmCommissioner::InventoryItem.active
25
+ .where('cm_inventory_items.updated_at > ?', @time_range)
26
+ .joins(variant: :product)
27
+ .select(
28
+ 'cm_inventory_items.id',
29
+ 'cm_inventory_items.variant_id',
30
+ 'cm_inventory_items.inventory_date',
31
+ 'cm_inventory_items.quantity_available',
32
+ 'cm_inventory_items.updated_at',
33
+ 'cm_inventory_items.product_type',
34
+ 'spree_variants.sku',
35
+ 'spree_products.name as product_name',
36
+ 'spree_products.slug as product_slug'
37
+ )
38
+
39
+ # Apply filter_type at query level
40
+ query = case @filter_type
41
+ when 'permanent'
42
+ query.where(cm_inventory_items: { inventory_date: nil })
43
+ when 'non_permanent'
44
+ query.where.not(cm_inventory_items: { inventory_date: nil })
45
+ else
46
+ query
47
+ end
48
+
49
+ query = query.where(spree_variants: { vendor_id: @vendor_id }) if @vendor_id.present?
50
+
51
+ query.limit(@limit).order('cm_inventory_items.updated_at DESC')
52
+ end
53
+
54
+ # Batch fetch from Redis to avoid N+1 queries
55
+ def fetch_quantity_in_redis_batch(items)
56
+ return [] if items.empty?
57
+
58
+ keys = items.map { |item| "inventory:#{item.id}" }
59
+
60
+ # Use mget for efficient batch Redis fetch
61
+ quantity_in_redis_array = SpreeCmCommissioner.inventory_redis_pool.with do |redis|
62
+ redis.mget(*keys)
63
+ end
64
+
65
+ # Combine with DB data
66
+ items.each_with_index.map do |item, index|
67
+ quantity_in_redis = quantity_in_redis_array[index]&.to_i
68
+ quantity_available = item.quantity_available
69
+
70
+ {
71
+ inventory_item_id: item.id,
72
+ variant_id: item.variant_id,
73
+ product_name: item.product_name,
74
+ product_slug: item.product_slug,
75
+ sku: item.sku,
76
+ inventory_date: item.inventory_date,
77
+ quantity_available: quantity_available,
78
+ quantity_in_redis: quantity_in_redis,
79
+ difference: quantity_in_redis ? (quantity_in_redis - quantity_available) : nil,
80
+ out_of_sync: quantity_in_redis.nil? || quantity_in_redis != quantity_available,
81
+ last_updated: item.updated_at,
82
+ product_type: item.product_type
83
+ }
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -29,7 +29,17 @@ module SpreeCmCommissioner
29
29
  # ❌ DO NOT use increment! because it skips model validations.
30
30
  # We need validations to run so negative quantities are caught and surfaced
31
31
  # as errors, not silently allowed by the database.
32
- inventory_item.update!(quantity_available: inventory_item.quantity_available + quantity)
32
+ #
33
+ # LOCKING STRATEGY: Dual Layer Protection
34
+ # 1. with_lock (Pessimistic): SELECT ... FOR UPDATE prevents concurrent writes
35
+ # 2. lock_version (Optimistic): Catches race conditions if row is modified between
36
+ # SELECT and UPDATE (e.g., admin adjusts stock while job runs)
37
+ #
38
+ # Why both? Pessimistic lock protects against concurrent job execution.
39
+ # Optimistic lock catches stale reads from outside transactions.
40
+ inventory_item.with_lock do
41
+ inventory_item.update!(quantity_available: inventory_item.quantity_available + quantity)
42
+ end
33
43
  end
34
44
 
35
45
  def inventory_items
@@ -11,7 +11,7 @@ module SpreeCmCommissioner
11
11
  updated = inventory_item.update(max_capacity: max_capacity, quantity_available: quantity_available)
12
12
  return context.fail!(message: 'Failed to update inventory item', errors: inventory_item.errors.full_messages) unless updated
13
13
 
14
- clear_inventory_cache
14
+ reset_redis_inventory
15
15
  end
16
16
 
17
17
  def variant_total_inventory
@@ -34,9 +34,13 @@ module SpreeCmCommissioner
34
34
  end
35
35
  end
36
36
 
37
- def clear_inventory_cache
37
+ def reset_redis_inventory
38
38
  SpreeCmCommissioner.inventory_redis_pool.with do |redis|
39
- redis.del(inventory_item.redis_key)
39
+ redis.setex(
40
+ inventory_item.redis_key,
41
+ inventory_item.redis_expired_in,
42
+ inventory_item.quantity_available
43
+ )
40
44
  end
41
45
  end
42
46
  end
@@ -0,0 +1,28 @@
1
+ # Usage inside config/schedule.yml:
2
+ #
3
+ # maintenance_tasks_orchestrate:
4
+ # cron: "*/5 * * * *" # Every 5 minutes
5
+ # class: InvokeJob
6
+ # args: { class: "SpreeCmCommissioner::MaintenanceTasks::OrchestrateJob" }
7
+ #
8
+ # Or if you prefer less frequent:
9
+ # cron: "*/10 * * * *"
10
+ module SpreeCmCommissioner
11
+ module MaintenanceTasks
12
+ class OrchestrateJob < ApplicationJob
13
+ queue_as :maintenance
14
+
15
+ BATCH_SIZE = ENV.fetch('MAINTENANCE_TASKS_BATCH_SIZE', '100').to_i
16
+
17
+ # Scheduled job that orchestrates maintenance tasks by enqueuing
18
+ # individual processor jobs. Thin wrapper for Orchestrate service.
19
+ # ApplicationJob handles error logging via around_perform hook.
20
+ def perform
21
+ tasks = SpreeCmCommissioner::MaintenanceTask.due_for_retry.order(created_at: :asc).limit(BATCH_SIZE)
22
+ return if tasks.empty?
23
+
24
+ tasks.each(&:async_execute)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ module SpreeCmCommissioner
2
+ module MaintenanceTasks
3
+ class ProcessJob < ApplicationUniqueJob
4
+ queue_as :maintenance
5
+
6
+ # Process a single maintenance task by calling task.execute directly.
7
+ # ApplicationJob handles error logging via around_perform hook.
8
+ def perform(options = {})
9
+ task_id = options[:task_id]
10
+ task = SpreeCmCommissioner::MaintenanceTask.find_by(id: task_id)
11
+
12
+ return if task.nil? || task.completed_at.present?
13
+
14
+ task.execute
15
+ end
16
+ end
17
+ end
18
+ end
@@ -53,9 +53,7 @@ module SpreeCmCommissioner
53
53
  after_transition from: :requested, to: :accepted, do: :decrement_user_request_orders_count
54
54
  after_transition to: :accepted, do: :send_order_accepted_app_notification_to_user
55
55
  after_transition to: :accepted, do: :send_order_accepted_telegram_alert_to_store
56
-
57
- # call confirmation_delivered column directly since confirmation_delivered? is overrided
58
- after_transition to: :accepted, if: :confirmation_delivered, do: :deliver_order_confirmation_email
56
+ after_transition to: :accepted, if: :confirmation_delivered?, do: :deliver_order_confirmation_email
59
57
 
60
58
  event :reject do
61
59
  transition from: :requested, to: :rejected
@@ -76,6 +74,14 @@ module SpreeCmCommissioner
76
74
  end
77
75
  end
78
76
 
77
+ # override
78
+ def deliver_order_confirmation_email
79
+ return if requested?
80
+ return if any_app_only_products?
81
+
82
+ super
83
+ end
84
+
79
85
  def increment_user_request_orders_count
80
86
  return if user.nil?
81
87
 
@@ -98,8 +104,13 @@ module SpreeCmCommissioner
98
104
  end
99
105
 
100
106
  def precalculate_conversion
101
- line_items.each do |item|
102
- SpreeCmCommissioner::ConversionPreCalculatorJob.perform_later(product_id: item.product_id)
107
+ event_ids = line_items.pluck(:event_id).compact.uniq
108
+ event_ids.each do |event_id|
109
+ # Just create task, CRON job will pick it up and execute.
110
+ SpreeCmCommissioner::MaintenanceTasks::Event.pending.find_or_create_by(
111
+ maintainable_type: 'Spree::Taxon',
112
+ maintainable_id: event_id
113
+ )
103
114
  end
104
115
  end
105
116
 
@@ -126,12 +137,6 @@ module SpreeCmCommissioner
126
137
  end
127
138
  end
128
139
 
129
- # overrided not to send email yet to user if order needs confirmation
130
- # it will send after vendors accepted.
131
- def confirmation_delivered?
132
- confirmation_delivered || need_confirmation?
133
- end
134
-
135
140
  # allow authorized user to accept all requested line items
136
141
  def accepted_by(user)
137
142
  transaction do
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpreeCmCommissioner
4
+ module ProductRelationType
5
+ extend ActiveSupport::Concern
6
+
7
+ # Relation types for linking products together
8
+ # - open_dated_pair: Links fixed-date trip to open-dated trip (for flexible ticket redemption)
9
+ # - add_on: Links main product to optional extras
10
+ PRODUCT_RELATION_TYPES = %i[
11
+ open_dated_pair
12
+ add_on
13
+ ].freeze
14
+
15
+ included do
16
+ enum relation_type: PRODUCT_RELATION_TYPES.each_with_index.to_h { |type, index| [type, index] }
17
+ end
18
+
19
+ def self.options
20
+ PRODUCT_RELATION_TYPES.map { |k| [I18n.t("product_relation_type.#{k}"), k] }
21
+ end
22
+
23
+ def display_relation_type
24
+ return nil if relation_type.blank?
25
+
26
+ I18n.t("product_relation_type.#{relation_type}")
27
+ end
28
+ end
29
+ end