spree_cm_commissioner 2.5.13.pre.pre11 → 2.5.13.pre.patch1

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +10 -0
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/concerns/spree/admin/service_calendars_concern.rb +93 -0
  5. data/app/controllers/spree/admin/classifications_controller.rb +11 -3
  6. data/app/controllers/spree/admin/import_existing_orders_controller.rb +3 -1
  7. data/app/controllers/spree/admin/import_new_orders_controller.rb +3 -1
  8. data/app/controllers/spree/admin/inventory_items_controller.rb +3 -3
  9. data/app/controllers/spree/admin/inventory_monitorings_controller.rb +39 -0
  10. data/app/controllers/spree/admin/product_service_calendars_controller.rb +48 -0
  11. data/app/controllers/spree/admin/stock_managements_controller.rb +5 -1
  12. data/app/controllers/spree/admin/taxons_controller_decorator.rb +6 -0
  13. data/app/controllers/spree/admin/vendor_service_calendars_controller.rb +7 -68
  14. data/app/controllers/spree/api/v2/operator/check_ins_controller.rb +15 -2
  15. data/app/controllers/spree/api/v2/operator/guest_json_gzips_controller.rb +7 -0
  16. data/app/controllers/spree/api/v2/operator/recalculate_tickets_controller.rb +4 -9
  17. data/app/controllers/spree_cm_commissioner/orders_controller.rb +14 -2
  18. data/app/finders/spree_cm_commissioner/inventory_items/recently_changed_finder.rb +88 -0
  19. data/app/helpers/spree_cm_commissioner/admin/service_calendars_helper.rb +8 -1
  20. data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +11 -1
  21. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +7 -3
  22. data/app/jobs/spree_cm_commissioner/import_order_job.rb +2 -2
  23. data/app/jobs/spree_cm_commissioner/maintenance_tasks/orchestrate_job.rb +28 -0
  24. data/app/jobs/spree_cm_commissioner/maintenance_tasks/process_job.rb +18 -0
  25. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +16 -11
  26. data/app/models/concerns/spree_cm_commissioner/product_relation_type.rb +29 -0
  27. data/app/models/spree_cm_commissioner/export.rb +12 -1
  28. data/app/models/spree_cm_commissioner/exports/operator_guest_json_gzip.rb +1 -1
  29. data/app/models/spree_cm_commissioner/inventory_item.rb +5 -3
  30. data/app/models/spree_cm_commissioner/maintenance_task.rb +62 -0
  31. data/app/models/spree_cm_commissioner/maintenance_tasks/event.rb +61 -0
  32. data/app/models/spree_cm_commissioner/order_decorator.rb +8 -0
  33. data/app/models/spree_cm_commissioner/product_decorator.rb +5 -2
  34. data/app/models/spree_cm_commissioner/product_relation.rb +23 -0
  35. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +1 -0
  36. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +20 -6
  37. data/app/overrides/spree/admin/shared/_product_tabs/service_calendars.html.erb.deface +8 -0
  38. data/app/overrides/spree/admin/shared/sub_menu/_stock/inventory_monitorings_tab.html.erb.deface +3 -0
  39. data/app/serializers/spree/v2/tenant/cart_serializer.rb +4 -0
  40. data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +39 -0
  41. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +10 -4
  42. data/app/services/spree_cm_commissioner/check_ins/create_bulk.rb +13 -2
  43. data/app/services/spree_cm_commissioner/imports/orders/base.rb +67 -0
  44. data/app/services/spree_cm_commissioner/imports/orders/create.rb +135 -0
  45. data/app/services/spree_cm_commissioner/imports/orders/update.rb +77 -0
  46. data/app/services/spree_cm_commissioner/operator_guest_json_gzips/create.rb +12 -6
  47. data/app/services/spree_cm_commissioner/organizer/export_guest_csv_service.rb +36 -0
  48. data/app/services/spree_cm_commissioner/trips/clone.rb +195 -0
  49. data/app/services/spree_cm_commissioner/trips/create_single_leg.rb +2 -0
  50. data/app/views/spree/admin/homepage_background/index.html.erb +1 -0
  51. data/app/views/spree/admin/inventory_monitorings/index.html.erb +119 -0
  52. data/app/views/spree/admin/product_service_calendars/index.html.erb +70 -0
  53. data/app/views/spree/admin/product_service_calendars/new.html.erb +9 -0
  54. data/app/views/spree/admin/{vendor_service_calendars → shared/service_calendars}/_form.html.erb +1 -1
  55. data/app/views/spree/admin/stock_managements/index.html.erb +1 -1
  56. data/app/views/spree/admin/taxon_childrens/index.html.erb +4 -0
  57. data/app/views/spree/admin/vendor_service_calendars/new.html.erb +2 -2
  58. data/app/views/spree_cm_commissioner/layouts/order_mailer.html.erb +15 -0
  59. data/config/locales/en.yml +7 -0
  60. data/config/routes.rb +11 -4
  61. data/db/migrate/20260202095500_create_cm_product_relations.rb +32 -0
  62. data/db/migrate/20260207100000_add_index_created_at_to_cm_imports.rb +5 -0
  63. data/db/migrate/20260217162827_add_index_to_cm_guests_on_event_id_and_bib_prefix_and_bib_number.rb +8 -0
  64. data/db/migrate/20260218100000_create_cm_maintenance_tasks.rb +23 -0
  65. data/lib/spree_cm_commissioner/cached_inventory_item.rb +8 -7
  66. data/lib/spree_cm_commissioner/test_helper/factories/product_relation_factory.rb +9 -0
  67. data/lib/spree_cm_commissioner/transit/trip_form.rb +1 -0
  68. data/lib/spree_cm_commissioner/version.rb +1 -1
  69. metadata +29 -17
  70. data/app/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +0 -63
  71. data/app/interactors/spree_cm_commissioner/conversion_pre_calculator.rb +0 -64
  72. data/app/interactors/spree_cm_commissioner/enqueue_cart/add_item.rb +0 -43
  73. data/app/interactors/spree_cm_commissioner/enqueue_cart/add_item_status_marker.rb +0 -66
  74. data/app/interactors/spree_cm_commissioner/trip_clone_creator.rb +0 -138
  75. data/app/jobs/spree_cm_commissioner/conversion_pre_calculator_job.rb +0 -12
  76. data/app/jobs/spree_cm_commissioner/enqueue_cart/add_item_job.rb +0 -17
  77. data/app/serializables/spree_cm_commissioner/queue_item.rb +0 -13
  78. data/app/serializers/spree/v2/storefront/cart_serializer_decorator.rb +0 -23
  79. data/app/serializers/spree/v2/storefront/firestore_queue_serializer.rb +0 -9
  80. data/app/services/spree_cm_commissioner/imports/base_import_order_service.rb +0 -55
  81. data/app/services/spree_cm_commissioner/imports/create_order_service.rb +0 -99
  82. data/app/services/spree_cm_commissioner/imports/update_order_service.rb +0 -41
  83. /data/app/views/spree/admin/{vendor_service_calendars → shared/service_calendars}/_exception_rules.html.erb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0199f4ae031efcea098fb06b2e7e20a58a848b21ff29c1d5182fe85caa4e13f3'
4
- data.tar.gz: f15098ef969208ca80524115e0d713a6edf5cc0364fff5c65f609ee652196ad0
3
+ metadata.gz: 2ca95409fb32a7b43530a6b61ffdc869564893d7c9a8abcf68e72f9db8c9362d
4
+ data.tar.gz: 93e7de99a30c3f58f899426132837c134ca9a6a725e799a90443b9699b91213f
5
5
  SHA512:
6
- metadata.gz: 3a7379b89a5b6f40e796719d634587baf0b23947435797bce34f82ceb47abd0e3733dab7f2bee17e4041720201253fbaf677f5d9c3729081b02e61c34f8d8a18
7
- data.tar.gz: 4064ac92ac295cfd51ce359faa41c6d7c6e6ab3fdfc3e48f151f8dcde6fa72652a6dc2014b7b9fc225fd8fc4b21c0ff38e4fd0faf5719c92f3bbea8b850a64fe
6
+ metadata.gz: efd4bc5698835f6eaebfa42e71e213c6cb9ace7d5a6eb5219b3143e49993ed8679145da7c0732ee16176b8428509973f6bd1ead97dcbf0ede168af042e67ae0c
7
+ data.tar.gz: 2ea123009aa6a3ecb2208117d35e27a8ece4010e78142b619f8c71323a6741b453bb37e8f2148f23fca7320d933923e81be037ed20baf616b43b294c7be52739
data/.env.example CHANGED
@@ -40,3 +40,13 @@ 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 app/jobs/spree_cm_commissioner/maintenance_tasks/orchestrate_job.rb
45
+ MAINTENANCE_TASKS_BATCH_SIZE=100
46
+
47
+ # Export related:
48
+ OPERATOR_GUEST_JSON_GZIP_CACHE_HOURS=24
49
+ EXPORT_PRESIGNED_URL_EXPIRATION_MINUTES=15
50
+
51
+ # import order batch size
52
+ IMPORT_ORDERS_BATCH_SIZE=50
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.pre11)
37
+ spree_cm_commissioner (2.5.13.pre.patch1)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -0,0 +1,93 @@
1
+ module Spree
2
+ module Admin
3
+ module ServiceCalendarsConcern
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ new_action.before :set_exception_rules
8
+
9
+ helper 'spree_cm_commissioner/admin/service_calendars'
10
+ end
11
+
12
+ def update_status
13
+ if @object.update(active: !@object.active)
14
+ flash[:success] = flash_message_for(@object, :successfully_updated)
15
+ else
16
+ flash[:error] = @object.errors.full_messages.to_sentence
17
+ end
18
+
19
+ redirect_to collection_url
20
+ rescue ActiveRecord::RecordInvalid => e
21
+ flash[:error] = e.message
22
+ redirect_to collection_url
23
+ end
24
+
25
+ protected
26
+
27
+ def model_class
28
+ SpreeCmCommissioner::ServiceCalendar
29
+ end
30
+
31
+ def object_name
32
+ 'spree_cm_commissioner_service_calendars'
33
+ end
34
+
35
+ def permitted_resource_params
36
+ service_calendar_params = params.require(:spree_cm_commissioner_service_calendar)
37
+ .permit(:calendarable_id, :calendarable_type, :start_date, :end_date,
38
+ :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday
39
+ )
40
+ service_calendar_params[:exception_rules] = build_exception_rules(params[:spree_cm_commissioner_service_calendar][:exception_rules])
41
+ service_calendar_params
42
+ end
43
+
44
+ private
45
+
46
+ def build_exception_rules(exception_rules)
47
+ exception_rules.values.reject! { |rule| rule['from'].blank? || rule['to'].blank? || rule['type'].blank? } || exception_rules.values
48
+ end
49
+
50
+ def collection
51
+ load_calendarable
52
+
53
+ @objects = model_class.where(
54
+ calendarable_type: calendarable_type,
55
+ calendarable_id: calendarable_id
56
+ ).order(id: :desc)
57
+ end
58
+
59
+ def set_exception_rules
60
+ @exception_rules = [{ from: DateTime.now, to: DateTime.now, type: 'exclusion', reason: nil }]
61
+ end
62
+
63
+ def build_resource
64
+ today = Time.zone.today
65
+ model_class.new(start_date: today, end_date: today.next_year(3),
66
+ exception_rules: [{ from: nil, to: nil, type: 'inclusion' }]
67
+ )
68
+ end
69
+
70
+ def set_calendarable
71
+ @object.calendarable_type = calendarable_type
72
+ @object.calendarable_id = calendarable_id
73
+ end
74
+
75
+ # To be implemented by including controllers
76
+ def load_calendarable
77
+ raise NotImplementedError, 'must implement load_calendarable'
78
+ end
79
+
80
+ def calendarable
81
+ raise NotImplementedError, 'must implement calendarable'
82
+ end
83
+
84
+ def calendarable_id
85
+ calendarable.id
86
+ end
87
+
88
+ def calendarable_type
89
+ calendarable.class.name
90
+ end
91
+ end
92
+ end
93
+ end
@@ -4,11 +4,19 @@ 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
+ ) do |task|
12
+ task.manually_triggered = true
13
+ end.async_execute
14
+
15
+ flash[:success] = flash_message_for(@taxon, :successfully_updated)
16
+ else
17
+ flash[:error] = 'Conversion recalculation can only be performed for taxons under an event.' # rubocop:disable Rails/I18nLocaleTexts
9
18
  end
10
19
 
11
- flash[:success] = flash_message_for(@taxon, :successfully_updated)
12
20
  redirect_to collection_url
13
21
  end
14
22
 
@@ -3,7 +3,9 @@ module Spree
3
3
  class ImportExistingOrdersController < BaseImportOrdersController
4
4
  # override
5
5
  def collection
6
- @collection ||= model_class.existing_order.page(params[:page])
6
+ @collection ||= model_class.existing_order
7
+ .order(created_at: :desc)
8
+ .page(params[:page])
7
9
  .per(params[:per_page])
8
10
  end
9
11
 
@@ -3,7 +3,9 @@ module Spree
3
3
  class ImportNewOrdersController < BaseImportOrdersController
4
4
  # override
5
5
  def collection
6
- @collection ||= model_class.new_order.page(params[:page])
6
+ @collection ||= model_class.new_order
7
+ .order(created_at: :desc)
8
+ .page(params[:page])
7
9
  .per(params[:per_page])
8
10
  end
9
11
 
@@ -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
@@ -0,0 +1,48 @@
1
+ module Spree
2
+ module Admin
3
+ class ProductServiceCalendarsController < Spree::Admin::ResourceController
4
+ include Spree::Admin::ServiceCalendarsConcern
5
+
6
+ before_action :load_product
7
+ before_action :ensure_product_type_supports_calendar
8
+ before_action :ensure_product_has_no_calendar, only: %i[new create]
9
+
10
+ create.before :set_calendarable
11
+ update.before :set_calendarable
12
+
13
+ protected
14
+
15
+ def collection_url(options = {})
16
+ admin_product_product_service_calendars_url(options)
17
+ end
18
+
19
+ private
20
+
21
+ def load_product
22
+ @product ||= (Spree::Product.find_by(slug: params[:product_id]) || Spree::Product.find_by(id: params[:product_id]))
23
+ end
24
+
25
+ def ensure_product_type_supports_calendar
26
+ return if @product.permanent_stock?
27
+
28
+ flash[:error] = I18n.t('vendor.service_calendars.not_available_for_product_type')
29
+ redirect_to edit_admin_product_url(@product)
30
+ end
31
+
32
+ def ensure_product_has_no_calendar
33
+ return if @product.service_calendar.blank?
34
+
35
+ flash[:error] = I18n.t('vendor.service_calendars.already_has_calendar')
36
+ redirect_to admin_product_product_service_calendars_url(@product)
37
+ end
38
+
39
+ def load_calendarable
40
+ load_product
41
+ end
42
+
43
+ def calendarable
44
+ @product
45
+ end
46
+ end
47
+ end
48
+ 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
@@ -5,6 +5,12 @@ module Spree
5
5
  base.before_action :build_assets, only: %i[create update]
6
6
  end
7
7
 
8
+ # override
9
+ def new
10
+ @taxon.parent_id = params[:parent_id] if params[:parent_id].present?
11
+ super
12
+ end
13
+
8
14
  def remove_category_icon
9
15
  remove_asset(@taxon.category_icon)
10
16
  end
@@ -1,92 +1,31 @@
1
1
  module Spree
2
2
  module Admin
3
3
  class VendorServiceCalendarsController < Spree::Admin::ResourceController
4
- before_action :load_vendor
5
- new_action.before :set_exception_rules
6
-
7
- create.before :set_vendor
8
- update.before :set_vendor
9
-
10
- helper 'spree_cm_commissioner/admin/service_calendars'
4
+ include Spree::Admin::ServiceCalendarsConcern
11
5
 
12
- def update_status
13
- if @object.update(active: !@object.active)
14
- flash[:success] = flash_message_for(@object, :successfully_updated)
15
- else
16
- flash[:error] = @object.errors.full_messages.to_sentence
17
- end
6
+ before_action :load_vendor
18
7
 
19
- redirect_to admin_vendor_vendor_service_calendars_url
20
- rescue ActiveRecord::RecordInvalid => e
21
- flash[:error] = e.message
22
- redirect_to admin_vendor_vendor_service_calendars_url
23
- end
8
+ create.before :set_calendarable
9
+ update.before :set_calendarable
24
10
 
25
11
  protected
26
12
 
27
- def model_class
28
- SpreeCmCommissioner::ServiceCalendar
29
- end
30
-
31
- def object_name
32
- 'spree_cm_commissioner_service_calendars'
33
- end
34
-
35
13
  def collection_url(options = {})
36
14
  admin_vendor_vendor_service_calendars_url(options)
37
15
  end
38
16
 
39
- def permitted_resource_params
40
- service_calendar_params = params.require(:spree_cm_commissioner_service_calendar)
41
- .permit(:calendarable_id, :calendarable_type, :start_date, :end_date,
42
- :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday
43
- )
44
- service_calendar_params[:exception_rules] = build_exception_rules(params[:spree_cm_commissioner_service_calendar][:exception_rules])
45
- service_calendar_params
46
- end
47
-
48
17
  private
49
18
 
50
19
  def load_vendor
51
20
  @vendor ||= (Spree::Vendor.find_by(slug: params[:vendor_id]) || Spree::Vendor.find_by(id: params[:vendor_id]))
52
21
  end
53
22
 
54
- def set_vendor
55
- @object.calendarable_type = calendarable_type
56
- @object.calendarable_id = calendarable_id
57
- end
58
-
59
- def build_exception_rules(exception_rules)
60
- exception_rules.values.reject! { |rule| rule['from'].blank? || rule['to'].blank? || rule['type'].blank? } || exception_rules.values
61
- end
62
-
63
- def collection
23
+ def load_calendarable
64
24
  load_vendor
65
-
66
- @objects = model_class.where(
67
- calendarable_type: calendarable_type,
68
- calendarable_id: calendarable_id
69
- ).order(id: :desc)
70
- end
71
-
72
- def set_exception_rules
73
- @exception_rules = [{ from: DateTime.now, to: DateTime.now, type: 'exclusion', reason: nil }]
74
- end
75
-
76
- # it load before :load_vendor
77
- def build_resource
78
- today = Time.zone.today
79
- model_class.new(start_date: today, end_date: today.next_year(3),
80
- exception_rules: [{ from: nil, to: nil, type: 'inclusion' }]
81
- )
82
- end
83
-
84
- def calendarable_id
85
- @vendor.id
86
25
  end
87
26
 
88
- def calendarable_type
89
- @vendor.class.name
27
+ def calendarable
28
+ @vendor
90
29
  end
91
30
  end
92
31
  end
@@ -5,10 +5,23 @@ module Spree
5
5
  class CheckInsController < ::Spree::Api::V2::ResourceController
6
6
  before_action :require_spree_current_user, only: %i[index create]
7
7
 
8
+ # Check-in history data requires fresh/recent data for operator dashboards
9
+ # Short cache duration ensures operators see near real-time check-in activity
10
+ CACHE_EXPIRES_IN = 1.minute
11
+
8
12
  def collection
9
- @collection = SpreeCmCommissioner::CheckIn.where(checkinable: params[:taxon_id])
13
+ @collection ||= SpreeCmCommissioner::CheckIn
14
+ .where(checkinable_type: 'Spree::Taxon', checkinable_id: params[:taxon_id])
15
+ .page(params[:page])
16
+ .per(params[:per_page])
17
+ end
10
18
 
11
- @collection = @collection.page(params[:page]).per(params[:per_page])
19
+ # override
20
+ def collection_cache_opts
21
+ {
22
+ namespace: Spree::Api::Config[:api_v2_collection_cache_namespace],
23
+ expires_in: CACHE_EXPIRES_IN
24
+ }
12
25
  end
13
26
 
14
27
  def create
@@ -3,7 +3,10 @@ module Spree
3
3
  module V2
4
4
  module Operator
5
5
  class GuestJsonGzipsController < ::Spree::Api::V2::ResourceController
6
+ include ActiveStorage::SetCurrent
7
+
6
8
  before_action :require_spree_current_user
9
+ before_action :require_event_user
7
10
 
8
11
  # POST /api/v2/operator/guest_json_gzips
9
12
  # - taxon_id=1
@@ -29,6 +32,10 @@ module Spree
29
32
  end
30
33
  end
31
34
 
35
+ def require_event_user
36
+ raise CanCan::AccessDenied unless spree_current_user.events.exists?(id: params[:taxon_id])
37
+ end
38
+
32
39
  # override
33
40
  def scope
34
41
  SpreeCmCommissioner::Exports::OperatorGuestJsonGzip.where(
@@ -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
@@ -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
@@ -5,9 +5,16 @@ module SpreeCmCommissioner
5
5
  label = resource.active ? 'Active' : 'Disabled'
6
6
  btn_active_class = resource.active ? 'btn-primary' : 'btn-warning'
7
7
 
8
+ # Determine the correct URL based on the calendarable type
9
+ url = if resource.calendarable_type == 'Spree::Product'
10
+ update_status_admin_product_product_service_calendar_path(resource.calendarable, resource)
11
+ else
12
+ update_status_admin_vendor_vendor_service_calendar_url(resource.calendarable, resource)
13
+ end
14
+
8
15
  button_to(
9
16
  label,
10
- update_status_admin_vendor_vendor_service_calendar_url(resource.calendarable, resource),
17
+ url,
11
18
  form: { data: { confirm: 'Are you sure?' }, class: "btn btn-sm btn-active #{btn_active_class}" },
12
19
  method: :patch
13
20
  )
@@ -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