spree_cm_commissioner 2.7.1.pre.pre5 → 2.7.1.pre.pre6

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 (197) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +12 -0
  3. data/.gitignore +0 -4
  4. data/Gemfile.lock +1 -1
  5. data/app/controllers/concerns/spree_cm_commissioner/order_concern.rb +1 -0
  6. data/app/controllers/spree/admin/classifications_controller.rb +1 -1
  7. data/app/controllers/spree/admin/inventory_holds_controller.rb +64 -0
  8. data/app/controllers/spree/admin/inventory_monitorings_controller.rb +7 -3
  9. data/app/controllers/spree/admin/stock_managements_controller.rb +10 -1
  10. data/app/controllers/spree/api/v2/operator/recalculate_tickets_controller.rb +1 -1
  11. data/app/controllers/spree/api/v2/storefront/pricing_previews_controller.rb +39 -0
  12. data/app/controllers/spree/api/v2/tenant/base_controller.rb +0 -4
  13. data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +1 -0
  14. data/app/controllers/spree/api/v2/tenant/pricing_previews_controller.rb +39 -0
  15. data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +88 -0
  16. data/app/finders/spree_cm_commissioner/accommodations/find.rb +6 -5
  17. data/app/finders/spree_cm_commissioner/inventory_items/recently_changed_finder.rb +43 -33
  18. data/app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb +0 -2
  19. data/app/jobs/spree_cm_commissioner/inventory_holds/bulk_release_stale_job.rb +11 -0
  20. data/app/jobs/spree_cm_commissioner/inventory_holds/bulk_release_stale_payment_locked_job.rb +11 -0
  21. data/app/jobs/spree_cm_commissioner/inventory_holds/release_job.rb +29 -0
  22. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +20 -0
  23. data/app/models/concerns/spree_cm_commissioner/homepage_section_bitwise.rb +1 -2
  24. data/app/models/concerns/spree_cm_commissioner/homepage_section_relatable_concern.rb +25 -0
  25. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +1 -2
  26. data/app/models/concerns/spree_cm_commissioner/order_holdable.rb +71 -0
  27. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +13 -17
  28. data/app/models/concerns/spree_cm_commissioner/product_type.rb +1 -1
  29. data/app/models/spree_cm_commissioner/homepage_background.rb +3 -0
  30. data/app/models/spree_cm_commissioner/homepage_section.rb +3 -0
  31. data/app/models/spree_cm_commissioner/inventory_hold.rb +30 -0
  32. data/app/models/spree_cm_commissioner/inventory_item.rb +8 -0
  33. data/app/models/spree_cm_commissioner/line_item_decorator.rb +10 -0
  34. data/app/models/spree_cm_commissioner/maintenance_tasks/cache_invalidation.rb +14 -0
  35. data/app/models/spree_cm_commissioner/menu_decorator.rb +3 -0
  36. data/app/models/spree_cm_commissioner/menu_item_decorator.rb +12 -0
  37. data/app/models/spree_cm_commissioner/order_decorator.rb +6 -6
  38. data/app/models/spree_cm_commissioner/payment_decorator.rb +12 -0
  39. data/app/models/spree_cm_commissioner/pricing_action.rb +2 -2
  40. data/app/models/spree_cm_commissioner/pricing_actions/create_guest_adjustments.rb +34 -42
  41. data/app/models/spree_cm_commissioner/pricing_actions/create_line_item_adjustments.rb +17 -10
  42. data/app/models/spree_cm_commissioner/pricing_actions/create_route_adjustments.rb +38 -39
  43. data/app/models/spree_cm_commissioner/pricing_model.rb +13 -15
  44. data/app/models/spree_cm_commissioner/pricing_rule.rb +1 -1
  45. data/app/models/spree_cm_commissioner/pricing_rules/age_group.rb +9 -12
  46. data/app/models/spree_cm_commissioner/pricing_rules/extra_drop_off_distance.rb +2 -2
  47. data/app/models/spree_cm_commissioner/pricing_rules/extra_pick_up_distance.rb +2 -2
  48. data/app/models/spree_cm_commissioner/pricing_rules/nationality.rb +18 -19
  49. data/app/models/spree_cm_commissioner/pricing_rules/nationality_group.rb +7 -16
  50. data/app/models/spree_cm_commissioner/product_decorator.rb +5 -6
  51. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +16 -9
  52. data/app/models/spree_cm_commissioner/reserved_block.rb +4 -0
  53. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +3 -0
  54. data/app/models/spree_cm_commissioner/taxon_decorator.rb +4 -5
  55. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +1 -10
  56. data/app/models/spree_cm_commissioner/tenant.rb +0 -9
  57. data/app/models/spree_cm_commissioner/user_decorator.rb +0 -3
  58. data/app/models/spree_cm_commissioner/variant_decorator.rb +3 -0
  59. data/app/models/spree_cm_commissioner/variant_options.rb +0 -4
  60. data/app/models/spree_cm_commissioner/vendor_decorator.rb +4 -10
  61. data/app/overrides/spree/admin/products/_form/enable_inventory_hold.html.erb.deface +18 -0
  62. data/app/overrides/spree/admin/shared/sub_menu/_stock/inventory_holds_tab.html.erb.deface +3 -0
  63. data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +1 -1
  64. data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_update_schema.rb +1 -0
  65. data/app/serializers/spree/v2/tenant/pricing_preview_serializer.rb +15 -0
  66. data/app/serializers/spree_cm_commissioner/v2/storefront/pricing_preview_serializer.rb +15 -0
  67. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +26 -8
  68. data/app/services/spree_cm_commissioner/checkout/advance_decorator.rb +1 -1
  69. data/app/services/spree_cm_commissioner/checkout/update_decorator.rb +1 -1
  70. data/app/services/spree_cm_commissioner/guests/update_seat.rb +55 -0
  71. data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +5 -2
  72. data/app/services/spree_cm_commissioner/inventory_holds/acquire.rb +175 -0
  73. data/app/services/spree_cm_commissioner/inventory_holds/bulk_release_stale.rb +41 -0
  74. data/app/services/spree_cm_commissioner/inventory_holds/bulk_release_stale_payment_locked.rb +11 -0
  75. data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +86 -0
  76. data/app/services/spree_cm_commissioner/inventory_holds/release.rb +100 -0
  77. data/app/services/spree_cm_commissioner/inventory_holds/validate_limits.rb +65 -0
  78. data/app/services/spree_cm_commissioner/inventory_items/reset.rb +23 -1
  79. data/app/services/spree_cm_commissioner/order_holds/hold.rb +71 -0
  80. data/app/services/spree_cm_commissioner/order_holds/lock_for_payment.rb +153 -0
  81. data/app/services/spree_cm_commissioner/order_holds/release.rb +56 -0
  82. data/app/services/spree_cm_commissioner/order_holds/reserve.rb +46 -0
  83. data/app/services/spree_cm_commissioner/pricing_models/activate.rb +41 -0
  84. data/app/services/spree_cm_commissioner/pricing_models/preview.rb +93 -0
  85. data/app/services/spree_cm_commissioner/reserved_blocks/hold.rb +4 -6
  86. data/app/services/spree_cm_commissioner/vendor_places/base.rb +2 -7
  87. data/app/services/spree_cm_commissioner/vendor_places/bulk_create.rb +3 -3
  88. data/app/views/spree/admin/inventory_holds/_search.html.erb +128 -0
  89. data/app/views/spree/admin/inventory_holds/index.html.erb +122 -0
  90. data/app/views/spree/admin/inventory_monitorings/index.html.erb +45 -24
  91. data/app/views/spree/admin/stock_managements/index.html.erb +32 -2
  92. data/config/initializers/spree_permitted_attributes.rb +0 -3
  93. data/config/locales/en.yml +21 -12
  94. data/config/locales/km.yml +21 -6
  95. data/config/routes.rb +10 -17
  96. data/db/migrate/20260327090000_create_cm_inventory_holds.rb +27 -0
  97. data/db/migrate/20260327090001_add_quantity_on_hold_to_cm_inventory_items.rb +5 -0
  98. data/db/migrate/20260410080000_add_payment_locked_at_to_cm_reserved_blocks.rb +5 -0
  99. data/db/migrate/20260506090000_remove_default_from_cm_guests_nationality_group.rb +6 -0
  100. data/docs/tenant/test.com.md +88 -0
  101. data/lib/spree_cm_commissioner/cached_inventory_item.rb +10 -1
  102. data/lib/spree_cm_commissioner/pricing_models/guest_context.rb +81 -0
  103. data/lib/spree_cm_commissioner/pricing_models/line_item_context.rb +63 -0
  104. data/lib/spree_cm_commissioner/pricing_models/order_context.rb +61 -0
  105. data/lib/spree_cm_commissioner/pricing_models/preview_adjustment.rb +30 -0
  106. data/lib/spree_cm_commissioner/pricing_models/pricing_preview.rb +15 -0
  107. data/lib/spree_cm_commissioner/test_helper/factories/inventory_hold_factory.rb +43 -0
  108. data/lib/spree_cm_commissioner/test_helper/factories/pricing_action_factory.rb +4 -0
  109. data/lib/spree_cm_commissioner/test_helper/factories/pricing_rule_group_factory.rb +1 -0
  110. data/lib/spree_cm_commissioner/version.rb +1 -1
  111. data/lib/spree_cm_commissioner.rb +6 -7
  112. metadata +44 -87
  113. data/app/controllers/spree/api/v2/tenant/free_vote_claims_controller.rb +0 -37
  114. data/app/controllers/spree/api/v2/tenant/show_contestants_controller.rb +0 -51
  115. data/app/controllers/spree/api/v2/tenant/show_people_controller.rb +0 -49
  116. data/app/controllers/spree/api/v2/tenant/show_person_assignments_controller.rb +0 -36
  117. data/app/controllers/spree/api/v2/tenant/shows_controller.rb +0 -34
  118. data/app/controllers/spree/api/v2/tenant/votes_controller.rb +0 -94
  119. data/app/controllers/spree/api/v2/tenant/voting_contestants_controller.rb +0 -40
  120. data/app/controllers/spree/api/v2/tenant/voting_credit_transactions_controller.rb +0 -41
  121. data/app/controllers/spree/api/v2/tenant/voting_credits_controller.rb +0 -31
  122. data/app/interactors/spree_cm_commissioner/guest_seat_updater.rb +0 -45
  123. data/app/jobs/spree_cm_commissioner/vote_fraud_event_job.rb +0 -9
  124. data/app/jobs/spree_cm_commissioner/voting_credit_allocation_job.rb +0 -10
  125. data/app/jobs/spree_cm_commissioner/voting_credit_de_allocation_job.rb +0 -10
  126. data/app/models/concerns/spree_cm_commissioner/order_seatable.rb +0 -81
  127. data/app/models/spree_cm_commissioner/maintenance_tasks/voting_session.rb +0 -19
  128. data/app/models/spree_cm_commissioner/pricing_model_handler/order.rb +0 -73
  129. data/app/models/spree_cm_commissioner/show.rb +0 -154
  130. data/app/models/spree_cm_commissioner/show_contestant.rb +0 -37
  131. data/app/models/spree_cm_commissioner/show_contestant_image.rb +0 -11
  132. data/app/models/spree_cm_commissioner/show_contestant_video.rb +0 -4
  133. data/app/models/spree_cm_commissioner/show_episode.rb +0 -109
  134. data/app/models/spree_cm_commissioner/show_person.rb +0 -15
  135. data/app/models/spree_cm_commissioner/show_person_assignment.rb +0 -20
  136. data/app/models/spree_cm_commissioner/show_person_image.rb +0 -11
  137. data/app/models/spree_cm_commissioner/vote.rb +0 -16
  138. data/app/models/spree_cm_commissioner/vote_fraud_event.rb +0 -19
  139. data/app/models/spree_cm_commissioner/voting_contestant.rb +0 -34
  140. data/app/models/spree_cm_commissioner/voting_credit.rb +0 -72
  141. data/app/models/spree_cm_commissioner/voting_credit_transaction.rb +0 -55
  142. data/app/models/spree_cm_commissioner/voting_session.rb +0 -153
  143. data/app/serializers/spree/v2/tenant/show_contestant_serializer.rb +0 -21
  144. data/app/serializers/spree/v2/tenant/show_episode_serializer.rb +0 -17
  145. data/app/serializers/spree/v2/tenant/show_person_assignment_serializer.rb +0 -16
  146. data/app/serializers/spree/v2/tenant/show_person_serializer.rb +0 -13
  147. data/app/serializers/spree/v2/tenant/show_serializer.rb +0 -26
  148. data/app/serializers/spree/v2/tenant/video_serializer.rb +0 -9
  149. data/app/serializers/spree/v2/tenant/vote_serializer.rb +0 -14
  150. data/app/serializers/spree/v2/tenant/voting_contestant_serializer.rb +0 -18
  151. data/app/serializers/spree/v2/tenant/voting_credit_serializer.rb +0 -10
  152. data/app/serializers/spree/v2/tenant/voting_credit_transaction_serializer.rb +0 -14
  153. data/app/serializers/spree/v2/tenant/voting_session_serializer.rb +0 -14
  154. data/app/services/spree_cm_commissioner/fraud_check.rb +0 -275
  155. data/app/services/spree_cm_commissioner/vote_counters/audit_counters.rb +0 -35
  156. data/app/services/spree_cm_commissioner/vote_counters/base.rb +0 -31
  157. data/app/services/spree_cm_commissioner/vote_counters/increment.rb +0 -34
  158. data/app/services/spree_cm_commissioner/vote_counters/per_contestant_counter.rb +0 -25
  159. data/app/services/spree_cm_commissioner/vote_counters/rebuild_from_db.rb +0 -41
  160. data/app/services/spree_cm_commissioner/vote_credit_deductor.rb +0 -68
  161. data/app/services/spree_cm_commissioner/vote_package/create.rb +0 -162
  162. data/app/services/spree_cm_commissioner/vote_package/update.rb +0 -172
  163. data/app/services/spree_cm_commissioner/vote_processor.rb +0 -133
  164. data/app/services/spree_cm_commissioner/voting_contestants/advancer.rb +0 -334
  165. data/app/services/spree_cm_commissioner/voting_contestants/assigner.rb +0 -32
  166. data/app/services/spree_cm_commissioner/voting_contestants/bulk_updater.rb +0 -104
  167. data/app/services/spree_cm_commissioner/voting_credits/allocate.rb +0 -77
  168. data/app/services/spree_cm_commissioner/voting_credits/claim_free_votes.rb +0 -119
  169. data/app/services/spree_cm_commissioner/voting_credits/credit_calculator.rb +0 -35
  170. data/app/services/spree_cm_commissioner/voting_credits/de_allocate.rb +0 -87
  171. data/config/schemas/show_contestant_highlight_video.json +0 -12
  172. data/db/migrate/20260309230148_create_cm_show_people.rb +0 -14
  173. data/db/migrate/20260309230149_create_cm_show_people_assignments.rb +0 -16
  174. data/db/migrate/20260310082711_create_cm_show_contestants.rb +0 -28
  175. data/db/migrate/20260310082720_create_cm_voting_sessions.rb +0 -21
  176. data/db/migrate/20260310082721_create_cm_voting_contestants.rb +0 -23
  177. data/db/migrate/20260310082734_add_voting_fields_to_spree_taxons.rb +0 -9
  178. data/db/migrate/20260310082735_add_type_to_spree_products.rb +0 -6
  179. data/db/migrate/20260310082749_create_cm_voting_credits.rb +0 -27
  180. data/db/migrate/20260326080200_create_cm_voting_credit_transactions.rb +0 -27
  181. data/db/migrate/20260330160000_create_cm_votes.rb +0 -25
  182. data/db/migrate/20260401072500_add_advanced_from_to_cm_voting_contestants.rb +0 -7
  183. data/db/migrate/20260402000001_add_voting_credit_scope_to_spree_taxons.rb +0 -6
  184. data/db/migrate/20260402000002_rename_scopeable_to_votable_in_cm_voting_credits.rb +0 -12
  185. data/db/migrate/20260403070000_add_name_to_cm_voting_sessions.rb +0 -5
  186. data/db/migrate/20260406000001_add_vendor_id_to_voting_tables.rb +0 -6
  187. data/db/migrate/20260406000001_rename_votes_remaining_to_amount_in_cm_voting_credits.rb +0 -11
  188. data/db/migrate/20260408085255_add_show_id_and_vendor_id_to_cm_voting_sessions.rb +0 -9
  189. data/db/migrate/20260420000001_rename_type_to_credit_type_in_cm_voting_credits.rb +0 -25
  190. data/db/migrate/20260422000001_create_cm_vote_fraud_events.rb +0 -23
  191. data/docs/sql/jsonb_query_guide.md +0 -57
  192. data/lib/spree_cm_commissioner/test_helper/factories/show_episode_factory.rb +0 -12
  193. data/lib/spree_cm_commissioner/test_helper/factories/show_factory.rb +0 -95
  194. data/lib/spree_cm_commissioner/test_helper/factories/vote_credit_factory.rb +0 -37
  195. data/lib/spree_cm_commissioner/test_helper/factories/vote_factory.rb +0 -28
  196. data/lib/spree_cm_commissioner/test_helper/factories/voting_credit_transaction_factory.rb +0 -11
  197. data/lib/spree_cm_commissioner/test_helper/factories/voting_session_factory.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84d0265d3f929b863daa466604c59bce056eeefaf6a68b8835fe6f4090c46260
4
- data.tar.gz: 4e7479e3a9625ac96dc232247e68ced8ad65b52942c6ad415c8f6504bbdee411
3
+ metadata.gz: f88085ac9f274a24587f64dca172c3219d84b76e02385419388bc84196bdbc2e
4
+ data.tar.gz: 3865b929388b20b69d3a641bbdf2695ae79055e5636c5fed7033bc297b511ff5
5
5
  SHA512:
6
- metadata.gz: bff8835ece695f1ab781c31eb0d3b279ca9417b5034509ba9070883b9ceb95a883465ca4bdfaa5dc2687d4c8a682744405edc2fcae30dbb83b39956f321440a8
7
- data.tar.gz: 5adfe3d241415a2f6fd3a9537adb951882201bee5564741ccd2d7f767d6033f92f0211f1ac26901fe4821539551931381605fe240a55134e9e71783283d9a047
6
+ metadata.gz: ec9862ee95fb74dc2031f22534084dbbfe01c7fd5351a248014f5a8011a97fd990d0145b1536d697877face277480bdc837fa126e64b73a41039a55268072182
7
+ data.tar.gz: 4b84c53d6a14a05702b5eaa78a76b1eb6f80471f15b4cc95bee9fef3772f00abe49f40c9c9d0cd78ec7761d5aa012c1d4546931a121e7e8ae2e264e7788e5bb2
data/.env.example CHANGED
@@ -50,3 +50,15 @@ EXPORT_PRESIGNED_URL_EXPIRATION_MINUTES=15
50
50
 
51
51
  # import order batch size
52
52
  IMPORT_ORDERS_BATCH_SIZE=50
53
+
54
+ # Inventory Hold System
55
+ # See: app/services/spree_cm_commissioner/order_holds/hold.rb
56
+ HOLD_DURATION_IN_MINUTES=8 # How long a hold is active after checkout begins (address step)
57
+
58
+ # See: app/services/spree_cm_commissioner/order_holds/lock_for_payment.rb
59
+ PAYMENT_LOCK_MIN_DURATION_IN_MINUTES=5 # Minimum remaining hold time required when entering the payment step; hold is extended if below this threshold
60
+
61
+ # See: app/services/spree_cm_commissioner/inventory_holds/validate_limits.rb
62
+ MAX_ACTIVE_HOLDS_PER_USER=3 # Max concurrent active holds allowed per user across all orders
63
+ MAX_HOLDS_PER_IP_PER_HOUR=5 # Max hold attempts per IP address within a rolling 1-hour window (abuse prevention)
64
+ HOLD_COOLDOWN_AFTER_EXPIRY_IN_MINUTES=2 # Cooldown period before a user can re-acquire a hold after one expired on them
data/.gitignore CHANGED
@@ -34,7 +34,3 @@ vendor/bundle/
34
34
  # Cursor
35
35
  .cursor
36
36
  dump.rdb
37
- # Added by code-review-graph
38
- .code-review-graph/
39
- .cursorrules
40
- .claude
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.7.1.pre.pre5)
37
+ spree_cm_commissioner (2.7.1.pre.pre6)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -11,6 +11,7 @@ module SpreeCmCommissioner
11
11
  raise CanCan::AccessDenied
12
12
  end
13
13
 
14
+ @spree_current_order.last_ip_address = request.remote_ip
14
15
  @spree_current_order
15
16
  end
16
17
 
@@ -5,7 +5,7 @@ module Spree
5
5
 
6
6
  def recalculate_conversions
7
7
  if @taxon.parent.event?
8
- SpreeCmCommissioner::MaintenanceTasks::Event.pending.find_or_create_by(
8
+ SpreeCmCommissioner::MaintenanceTasks::Event.pending.create_or_find_by(
9
9
  maintainable_type: 'Spree::Taxon',
10
10
  maintainable_id: @taxon.parent.id
11
11
  ) do |task|
@@ -0,0 +1,64 @@
1
+ module Spree
2
+ module Admin
3
+ class InventoryHoldsController < BaseController
4
+ def index
5
+ authorize! :manage, SpreeCmCommissioner::InventoryHold
6
+
7
+ q = params.fetch(:q, {})
8
+ q = q.respond_to?(:to_unsafe_h) ? q.to_unsafe_h.deep_dup : q.deep_dup
9
+ q['s'] ||= 'created_at desc'
10
+ q['created_at_gt'] = parse_time_or_nil(q['created_at_gt'], :beginning_of_day) if q['created_at_gt'].present?
11
+ q['created_at_lt'] = parse_time_or_nil(q['created_at_lt'], :end_of_day) if q['created_at_lt'].present?
12
+
13
+ @search = scope.ransack(q)
14
+ result_scope = @search.result(distinct: true)
15
+
16
+ @holds = result_scope
17
+ .includes(order: [:user, { line_items: { variant: { product: :vendor } } }])
18
+ .page(params[:page])
19
+ .per(params[:per_page] || Spree::Backend::Config[:admin_orders_per_page])
20
+
21
+ @status_counts = result_scope.reorder(nil).group(:status).count
22
+ end
23
+
24
+ def release
25
+ authorize! :manage, SpreeCmCommissioner::InventoryHold
26
+
27
+ hold = SpreeCmCommissioner::InventoryHold.find(params[:id])
28
+
29
+ if hold.finalized?
30
+ flash[:error] = "Hold ##{hold.id} is already #{hold.status} and cannot be released."
31
+ return redirect_to admin_inventory_holds_path(back_params)
32
+ end
33
+
34
+ result = SpreeCmCommissioner::InventoryHolds::Release.call(hold: hold, reason: :user_canceled)
35
+
36
+ if result.success?
37
+ flash[:success] = "Hold ##{hold.id} for order #{hold.order.number} has been released."
38
+ else
39
+ flash[:error] = "Failed to release hold ##{hold.id}: #{result.error}"
40
+ end
41
+
42
+ redirect_to admin_inventory_holds_path(back_params)
43
+ end
44
+
45
+ private
46
+
47
+ def back_params
48
+ raw_params = params[:back_params].presence || params
49
+ ActionController::Parameters.new(raw_params).permit(:page, :per_page, q: {})
50
+ end
51
+
52
+ def scope
53
+ SpreeCmCommissioner::InventoryHold
54
+ .accessible_by(current_ability, :manage)
55
+ end
56
+
57
+ def parse_time_or_nil(value, boundary)
58
+ Time.zone.parse(value).public_send(boundary)
59
+ rescue StandardError
60
+ nil
61
+ end
62
+ end
63
+ end
64
+ end
@@ -5,19 +5,20 @@ module Spree
5
5
  authorize! :manage, SpreeCmCommissioner::InventoryItem
6
6
 
7
7
  @time_range = params[:time_range] || 7
8
- @filter_type = params[:filter_type] || 'all'
8
+ @product_type = params[:product_type].presence
9
9
  @vendor_id = params[:vendor_id]
10
10
 
11
11
  finder = SpreeCmCommissioner::InventoryItems::RecentlyChangedFinder.new(
12
12
  time_range: @time_range.to_i.days.ago,
13
13
  limit: 1000,
14
14
  vendor_id: @vendor_id,
15
- filter_type: @filter_type
15
+ product_type: @product_type
16
16
  )
17
17
 
18
18
  @inventory_items = finder.execute
19
19
  @total_count = @inventory_items.size
20
20
  @out_of_sync_count = @inventory_items.count { |item| item[:out_of_sync] }
21
+ @on_hold_out_of_sync_count = @inventory_items.count { |item| item[:on_hold_out_of_sync] }
21
22
  end
22
23
 
23
24
  def reset
@@ -32,7 +33,10 @@ module Spree
32
33
  flash[:error] = "Failed to reset inventory: #{result.message}"
33
34
  end
34
35
 
35
- redirect_to action: :index, time_range: params[:time_range], filter_type: params[:filter_type]
36
+ redirect_to action: :index,
37
+ time_range: params[:time_range],
38
+ product_type: params[:product_type],
39
+ vendor_id: params[:vendor_id]
36
40
  end
37
41
  end
38
42
  end
@@ -5,7 +5,7 @@ module Spree
5
5
 
6
6
  before_action :load_parent
7
7
 
8
- helper_method :inventory_item_message
8
+ helper_method :inventory_item_message, :inventory_item_hold_message
9
9
 
10
10
  def load_parent
11
11
  @product = Spree::Product.find_by(slug: params[:product_id])
@@ -75,6 +75,15 @@ module Spree
75
75
  end
76
76
  end
77
77
 
78
+ def inventory_item_hold_message(inventory_item, cached_inventory_item)
79
+ synced = inventory_item.quantity_on_hold == cached_inventory_item.quantity_on_hold
80
+ if synced
81
+ "Synced: Quantity on hold matches in both DB and Redis (#{cached_inventory_item.quantity_on_hold})."
82
+ else
83
+ "Out of sync: Redis shows #{cached_inventory_item.quantity_on_hold} on hold, which doesn't match the database."
84
+ end
85
+ end
86
+
78
87
  private
79
88
 
80
89
  def load_inventories
@@ -7,7 +7,7 @@ module Spree
7
7
  before_action :load_taxon, only: :create
8
8
 
9
9
  def create
10
- SpreeCmCommissioner::MaintenanceTasks::Event.pending.find_or_create_by(
10
+ SpreeCmCommissioner::MaintenanceTasks::Event.pending.create_or_find_by(
11
11
  maintainable: @taxon
12
12
  ).async_execute
13
13
 
@@ -0,0 +1,39 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ class PricingPreviewsController < ::Spree::Api::V2::BaseController
6
+ # GET /api/v2/tenant/pricing_previews
7
+ def index
8
+ permitted_preview_params = params.permit(SpreeCmCommissioner::PricingModels::OrderContext::PREVIEW_PERMITTED_PARAMS)
9
+ order_context = SpreeCmCommissioner::PricingModels::OrderContext.load!(permitted_preview_params[:order_context])
10
+ pricing_models_updated_at = SpreeCmCommissioner::PricingModel.joins(:vendor)
11
+ .where(spree_vendors: { tenant_id: nil })
12
+ .maximum(:updated_at)
13
+ cache_key = 'pricing:preview:v2:' \
14
+ "#{Digest::SHA256.hexdigest(permitted_preview_params.to_json)}:" \
15
+ "#{pricing_models_updated_at.to_i}"
16
+
17
+ result = Rails.cache.fetch(cache_key, expires_in: 60.seconds) do
18
+ SpreeCmCommissioner::PricingModels::Preview.call(order_context: order_context)
19
+ end
20
+
21
+ if result.success?
22
+ render_serialized_payload { serialize_resource(result.value[:pricing_preview]) }
23
+ else
24
+ render_error_payload(result.error, 422)
25
+ end
26
+ rescue ArgumentError => e
27
+ render_error_payload(e.message, 422)
28
+ end
29
+
30
+ private
31
+
32
+ def resource_serializer
33
+ SpreeCmCommissioner::V2::Storefront::PricingPreviewSerializer
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -16,10 +16,6 @@ module Spree
16
16
  set_current_tenant(@tenant)
17
17
  end
18
18
 
19
- def current_vendor
20
- @current_vendor ||= @tenant.active_vendor
21
- end
22
-
23
19
  def render_serialized_payload(status = 200)
24
20
  render json: yield, status: status, content_type: content_type
25
21
  end
@@ -29,6 +29,7 @@ module Spree
29
29
  result = SpreeCmCommissioner::IntercityTaxiOrder::Update.call(
30
30
  order: spree_current_order,
31
31
  remark: params[:remark],
32
+ passenger_count: params[:passenger_count],
32
33
  pickup_map_place_attributes: params[:pickup_map_place_attributes]&.permit(
33
34
  :place_name,
34
35
  :lat,
@@ -0,0 +1,39 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ class PricingPreviewsController < BaseController
6
+ # GET /api/v2/tenant/pricing_previews
7
+ def index
8
+ permitted_preview_params = params.permit(SpreeCmCommissioner::PricingModels::OrderContext::PREVIEW_PERMITTED_PARAMS)
9
+ order_context = SpreeCmCommissioner::PricingModels::OrderContext.load!(permitted_preview_params[:order_context])
10
+ pricing_models_updated_at = SpreeCmCommissioner::PricingModel.joins(:vendor)
11
+ .where(spree_vendors: { tenant_id: current_tenant.id })
12
+ .maximum(:updated_at)
13
+ cache_key = 'pricing:preview:v2:' \
14
+ "#{Digest::SHA256.hexdigest(permitted_preview_params.to_json)}:" \
15
+ "#{pricing_models_updated_at.to_i}"
16
+
17
+ result = Rails.cache.fetch(cache_key, expires_in: 60.seconds) do
18
+ SpreeCmCommissioner::PricingModels::Preview.call(order_context: order_context)
19
+ end
20
+
21
+ if result.success?
22
+ render_serialized_payload { serialize_resource(result.value[:pricing_preview]) }
23
+ else
24
+ render_error_payload(result.error, 422)
25
+ end
26
+ rescue ArgumentError => e
27
+ render_error_payload(e.message, 422)
28
+ end
29
+
30
+ private
31
+
32
+ def resource_serializer
33
+ Spree::V2::Tenant::PricingPreviewSerializer
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -48,6 +48,8 @@ module SpreeCmCommissioner
48
48
  end
49
49
 
50
50
  def line_item_content(line_item)
51
+ return intercity_taxi_line_item_content(line_item) if intercity_taxi?(line_item)
52
+
51
53
  text = []
52
54
 
53
55
  text << bold(line_item.product.name.to_s)
@@ -82,6 +84,8 @@ module SpreeCmCommissioner
82
84
  text << "Email: #{inline_code(order.email)}" if order.email.present?
83
85
  text << "Delivery Address: #{formatted_shipping_address.presence || 'N/A'}" if order.delivery_required?
84
86
 
87
+ append_intercity_taxi_footer(text) if intercity_taxi_order?
88
+
85
89
  if show_details_link && order.guests.any?
86
90
  text << ''
87
91
  text << 'View Tickets:'
@@ -125,5 +129,89 @@ module SpreeCmCommissioner
125
129
 
126
130
  formatted_rows
127
131
  end
132
+
133
+ private
134
+
135
+ def intercity_taxi?(line_item)
136
+ trip_for(line_item)&.intercity_taxi?
137
+ end
138
+
139
+ def intercity_taxi_order?
140
+ selected_line_items.any? { |li| intercity_taxi?(li) }
141
+ end
142
+
143
+ def trip_for(line_item)
144
+ return nil unless line_item.respond_to?(:trip_id) && line_item.trip_id.present?
145
+
146
+ @trip_cache ||= {}
147
+ @trip_cache[line_item.trip_id] ||= SpreeCmCommissioner::Trip.find_by(id: line_item.trip_id)
148
+ end
149
+
150
+ def intercity_taxi_line_item_content(line_item)
151
+ text = []
152
+ route_text = taxi_route_text(line_item)
153
+ departure_text = taxi_departure_text(line_item)
154
+ text << bold('🚗 Trip Details')
155
+ text << "Route: #{route_text}" if route_text.present?
156
+ text << "Passengers: #{line_item.passenger_count || line_item.quantity}"
157
+ text << "Departure: #{departure_text}" if departure_text.present?
158
+
159
+ text.compact.join("\n")
160
+ end
161
+
162
+ def taxi_route_text(line_item)
163
+ trip = trip_for(line_item)
164
+ return line_item.product.name if trip.blank?
165
+
166
+ origin = trip.origin_place&.name
167
+ destination = trip.destination_place&.name
168
+ vehicle = trip.vehicle_type&.name
169
+
170
+ parts = [origin, destination].compact_blank.join(' → ')
171
+ vehicle.present? ? "#{parts} (#{vehicle})" : parts
172
+ end
173
+
174
+ def taxi_departure_text(line_item)
175
+ return nil unless line_item.date_present?
176
+
177
+ line_item.from_date.strftime('%b %d, %Y · %H:%M')
178
+ end
179
+
180
+ def nationality_text
181
+ values = order.saved_guests.map { |sg| sg.nationality&.name.presence || sg.nationality_group&.humanize }.compact.uniq
182
+ return nil if values.empty?
183
+
184
+ values.join(', ')
185
+ end
186
+
187
+ def append_intercity_taxi_footer(text)
188
+ text << "Nationality: #{nationality_text}" if nationality_text.present?
189
+
190
+ pickup = selected_line_items.filter_map(&:pickup_map_place).first
191
+ dropoff = selected_line_items.filter_map(&:dropoff_map_place).first
192
+
193
+ append_map_place(text, '📍 Pick-up Location', pickup) if pickup
194
+ append_map_place(text, '📍 Drop-off Location', dropoff) if dropoff
195
+
196
+ remark = selected_line_items.filter_map { |li| li.remark.presence }.first
197
+ return if remark.blank?
198
+
199
+ text << ''
200
+ text << bold('🗒️ Notes to Driver')
201
+ text << ERB::Util.html_escape(remark)
202
+ end
203
+
204
+ def append_map_place(text, title, map_place)
205
+ text << ''
206
+ text << bold(title)
207
+ text << "Address: #{ERB::Util.html_escape(map_place.place_name)}" if map_place.place_name.present?
208
+ text << "Google Maps: #{ERB::Util.html_escape(google_maps_link(map_place))}" if google_maps_link(map_place).present?
209
+ end
210
+
211
+ def google_maps_link(map_place)
212
+ return nil if map_place.lat.blank? || map_place.lng.blank?
213
+
214
+ "https://www.google.com/maps/search/?api=1&query=#{map_place.lat},#{map_place.lng}"
215
+ end
128
216
  end
129
217
  end
@@ -13,11 +13,11 @@ module SpreeCmCommissioner
13
13
  def execute
14
14
  scope
15
15
  .where(default_state_id: state_id)
16
- .where(inventory_items: { inventory_date: stay_dates })
17
- .where('CAST(spree_variants.public_metadata->\'cm_options\'->>\'number-of-adults\' AS INTEGER) +
18
- CAST(spree_variants.public_metadata->\'cm_options\'->>\'number-of-kids\' AS INTEGER) >= ?', number_of_guests
16
+ .where('spree_variants.track_inventory = FALSE OR cm_inventory_items.inventory_date BETWEEN ? AND ?', from_date, to_date)
17
+ .where('spree_variants.track_inventory = FALSE OR cm_inventory_items.quantity_available > 0')
18
+ .where('COALESCE(CAST(spree_variants.public_metadata->\'cm_options\'->>\'number-of-adults\' AS INTEGER), 0) +
19
+ COALESCE(CAST(spree_variants.public_metadata->\'cm_options\'->>\'number-of-kids\' AS INTEGER), 0) >= ?', number_of_guests
19
20
  )
20
- .where('inventory_items.quantity_available > 0')
21
21
  .distinct
22
22
  end
23
23
 
@@ -25,7 +25,8 @@ module SpreeCmCommissioner
25
25
 
26
26
  def scope
27
27
  Spree::Vendor
28
- .joins(variants: :inventory_items)
28
+ .joins(products: :variants)
29
+ .joins('LEFT OUTER JOIN cm_inventory_items ON cm_inventory_items.variant_id = spree_variants.id')
29
30
  .where(primary_product_type: :accommodation, state: :active)
30
31
  end
31
32
 
@@ -1,11 +1,11 @@
1
1
  module SpreeCmCommissioner
2
2
  module InventoryItems
3
3
  class RecentlyChangedFinder
4
- def initialize(time_range: 7.days.ago, limit: 1000, vendor_id: nil, filter_type: 'all')
4
+ def initialize(time_range: 7.days.ago, limit: 1000, vendor_id: nil, product_type: nil)
5
5
  @time_range = time_range
6
6
  @limit = limit
7
7
  @vendor_id = vendor_id
8
- @filter_type = filter_type
8
+ @product_type = product_type
9
9
  end
10
10
 
11
11
  # Finds recently changed items with Redis comparison
@@ -22,13 +22,15 @@ module SpreeCmCommissioner
22
22
 
23
23
  def fetch_recent_items
24
24
  query = SpreeCmCommissioner::InventoryItem.active
25
- .where('cm_inventory_items.updated_at > ?', @time_range)
26
25
  .joins(variant: :product)
26
+ .where(spree_variants: { track_inventory: true })
27
+ .where('cm_inventory_items.updated_at > ?', @time_range)
27
28
  .select(
28
29
  'cm_inventory_items.id',
29
30
  'cm_inventory_items.variant_id',
30
31
  'cm_inventory_items.inventory_date',
31
32
  'cm_inventory_items.quantity_available',
33
+ 'cm_inventory_items.quantity_on_hold',
32
34
  'cm_inventory_items.updated_at',
33
35
  'cm_inventory_items.product_type',
34
36
  'spree_variants.sku',
@@ -36,16 +38,7 @@ module SpreeCmCommissioner
36
38
  'spree_products.slug as product_slug'
37
39
  )
38
40
 
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
-
41
+ query = query.where(cm_inventory_items: { product_type: @product_type }) if @product_type.present?
49
42
  query = query.where(spree_variants: { vendor_id: @vendor_id }) if @vendor_id.present?
50
43
 
51
44
  query.limit(@limit).order('cm_inventory_items.updated_at DESC')
@@ -55,34 +48,51 @@ module SpreeCmCommissioner
55
48
  def fetch_quantity_in_redis_batch(items)
56
49
  return [] if items.empty?
57
50
 
58
- keys = items.map { |item| "inventory:#{item.id}" }
51
+ stock_keys = items.map(&:redis_key)
52
+ hold_keys = items.map(&:redis_hold_key)
53
+ all_keys = stock_keys + hold_keys
59
54
 
60
55
  # Use mget for efficient batch Redis fetch
61
- quantity_in_redis_array = SpreeCmCommissioner.inventory_redis_pool.with do |redis|
62
- redis.mget(*keys)
56
+ all_values = SpreeCmCommissioner.inventory_redis_pool.with do |redis|
57
+ redis.mget(*all_keys)
63
58
  end
64
59
 
60
+ quantity_in_redis_array = all_values.first(items.length)
61
+ on_hold_in_redis_array = all_values.last(items.length)
62
+
65
63
  # Combine with DB data
66
64
  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
- }
65
+ build_item_hash(item, index, quantity_in_redis_array, on_hold_in_redis_array)
84
66
  end
85
67
  end
68
+
69
+ def build_item_hash(item, index, quantity_in_redis_array, on_hold_in_redis_array)
70
+ quantity_in_redis = quantity_in_redis_array[index]&.to_i
71
+ quantity_available = item.quantity_available
72
+ on_hold_in_redis = on_hold_in_redis_array[index]&.to_i
73
+ quantity_on_hold = item.quantity_on_hold
74
+
75
+ # for :out_of_sync, :on_hold_out_of_sync => nil quantity does mean no data on redis,
76
+ # but does not necessarily mean it's out of sync
77
+ {
78
+ inventory_item_id: item.id,
79
+ variant_id: item.variant_id,
80
+ product_name: item.product_name,
81
+ product_slug: item.product_slug,
82
+ sku: item.sku,
83
+ inventory_date: item.inventory_date,
84
+ quantity_available: quantity_available,
85
+ quantity_in_redis: quantity_in_redis,
86
+ difference: quantity_in_redis ? (quantity_in_redis - quantity_available) : nil,
87
+ out_of_sync: !quantity_in_redis.nil? && quantity_in_redis != quantity_available,
88
+ quantity_on_hold: quantity_on_hold,
89
+ on_hold_in_redis: on_hold_in_redis,
90
+ on_hold_difference: on_hold_in_redis ? (on_hold_in_redis - quantity_on_hold) : nil,
91
+ on_hold_out_of_sync: !on_hold_in_redis.nil? && on_hold_in_redis != quantity_on_hold,
92
+ last_updated: item.updated_at,
93
+ product_type: item.product_type
94
+ }
95
+ end
86
96
  end
87
97
  end
88
98
  end
@@ -15,8 +15,6 @@ module SpreeCmCommissioner
15
15
  'badge badge-success text-uppercase'
16
16
  when :football
17
17
  'badge badge-dark text-uppercase'
18
- when :show
19
- 'badge badge-danger text-uppercase'
20
18
  else
21
19
  'badge'
22
20
  end
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ module InventoryHolds
3
+ class BulkReleaseStaleJob < SpreeCmCommissioner::ApplicationJob
4
+ queue_as :inventory_hold
5
+
6
+ def perform
7
+ SpreeCmCommissioner::InventoryHolds::BulkReleaseStale.call!
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ module InventoryHolds
3
+ class BulkReleaseStalePaymentLockedJob < SpreeCmCommissioner::ApplicationJob
4
+ queue_as :inventory_hold
5
+
6
+ def perform
7
+ SpreeCmCommissioner::InventoryHolds::BulkReleaseStalePaymentLocked.call!
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ module SpreeCmCommissioner
2
+ module InventoryHolds
3
+ # Primary release for :active holds. Scheduled at hold creation (wait_until: expires_at).
4
+ # Safety net: BulkReleaseStaleJob catches any active holds this job misses.
5
+ class ReleaseJob < SpreeCmCommissioner::ApplicationJob
6
+ queue_as :inventory_hold
7
+
8
+ def perform(options = {})
9
+ hold = SpreeCmCommissioner::InventoryHold.find(options[:hold_id])
10
+
11
+ # Skip if already finalized (e.g. delayed or retried jobs).
12
+ # The release service also enforces idempotency.
13
+ return if hold.finalized?
14
+
15
+ # When LockForPayment runs (triggered on payment creation), the hold transitions
16
+ # to :payment_locked status. ReleaseJob must not touch those holds —
17
+ # BulkReleaseStalePaymentLockedJob (cron) is the sole cleanup mechanism for them,
18
+ # allowing future custom logic (e.g. extend instead of release when payments exist).
19
+ return if hold.payment_locked?
20
+
21
+ SpreeCmCommissioner::InventoryHolds::Release.call!(
22
+ hold: hold,
23
+ reason: :hold_expired,
24
+ scheduled_release: true
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ module SpreeCmCommissioner
2
+ module InventoryItems
3
+ class BulkAdjustQuantitiesOnHoldJob < ApplicationUniqueJob
4
+ queue_as :default
5
+
6
+ # :order_id, :inventory_id_and_quantities
7
+ #
8
+ # :order_id is included for unique job key generation to prevent duplicate jobs,
9
+ # though it's not used in the perform method implementation.
10
+ def perform(options = {})
11
+ raise ArgumentError, 'order_id is required' if options[:order_id].blank?
12
+ raise ArgumentError, 'inventory_id_and_quantities is required' if options[:inventory_id_and_quantities].blank?
13
+
14
+ SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHold.call(
15
+ inventory_id_and_quantities: options[:inventory_id_and_quantities]
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -8,8 +8,7 @@ module SpreeCmCommissioner
8
8
  tour: 0b00100,
9
9
  accommodation: 0b01000,
10
10
  things_to_do: 0b10000,
11
- football: 0b100000,
12
- show: 0b1000000
11
+ football: 0b100000
13
12
  }.freeze
14
13
 
15
14
  BIT_SEGMENT.each do |segment, bit_value|