spree_cm_commissioner 2.0.3.pre.pre4 → 2.0.3.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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test_and_build_gem.yml +16 -2
  3. data/.gitignore +2 -1
  4. data/Gemfile.lock +28 -1
  5. data/Rakefile +33 -4
  6. data/app/assets/stylesheets/spree_cm_commissioner/backend/calendar.scss +8 -11
  7. data/app/controllers/spree/admin/inventory_items_controller.rb +83 -0
  8. data/app/controllers/spree/admin/stock_managements_controller.rb +63 -1
  9. data/app/controllers/spree/api/v2/storefront/accommodations/variants_controller.rb +42 -0
  10. data/app/controllers/spree/api/v2/storefront/accommodations_controller.rb +14 -31
  11. data/app/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +2 -2
  12. data/app/controllers/spree/api/v2/storefront/trip_places_controller.rb +29 -0
  13. data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +62 -0
  14. data/app/controllers/spree_cm_commissioner/admin/products_controller_decorator.rb +14 -0
  15. data/app/finders/spree_cm_commissioner/accommodations/find.rb +37 -0
  16. data/app/finders/spree_cm_commissioner/accommodations/find_variant.rb +32 -0
  17. data/app/interactors/spree_cm_commissioner/ensure_correct_product_type.rb +40 -0
  18. data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +25 -0
  19. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +44 -0
  20. data/app/interactors/spree_cm_commissioner/stock/inventory_items_adjuster.rb +13 -0
  21. data/app/interactors/spree_cm_commissioner/stock/inventory_items_generator.rb +15 -0
  22. data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +75 -0
  23. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +32 -0
  24. data/app/interactors/spree_cm_commissioner/taxi_trip_creator.rb +66 -0
  25. data/app/interactors/spree_cm_commissioner/transit/draft_order_creator.rb +132 -0
  26. data/app/interactors/spree_cm_commissioner/variant_block_updater.rb +62 -0
  27. data/app/interactors/spree_cm_commissioner/vehicle_updater.rb +41 -0
  28. data/app/jobs/spree_cm_commissioner/application_job.rb +20 -0
  29. data/app/jobs/spree_cm_commissioner/application_unique_job.rb +20 -0
  30. data/app/jobs/spree_cm_commissioner/ensure_correct_product_type_job.rb +7 -0
  31. data/app/jobs/spree_cm_commissioner/inventory_item_syncer_job.rb +7 -0
  32. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +11 -0
  33. data/app/jobs/spree_cm_commissioner/stock/inventory_items_generator_job.rb +11 -0
  34. data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +9 -0
  35. data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +18 -15
  36. data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +29 -15
  37. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +89 -0
  38. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +2 -12
  39. data/app/models/concerns/spree_cm_commissioner/order_seatable.rb +44 -0
  40. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +39 -0
  41. data/app/models/concerns/spree_cm_commissioner/product_delegation.rb +1 -3
  42. data/app/models/concerns/spree_cm_commissioner/product_type.rb +10 -0
  43. data/app/models/concerns/spree_cm_commissioner/route_type.rb +1 -1
  44. data/app/models/concerns/spree_cm_commissioner/taxon_kind.rb +1 -1
  45. data/app/models/concerns/spree_cm_commissioner/vehicle_type.rb +11 -0
  46. data/app/models/spree_cm_commissioner/address_decorator.rb +36 -2
  47. data/app/models/spree_cm_commissioner/block.rb +43 -0
  48. data/app/models/spree_cm_commissioner/guest.rb +35 -0
  49. data/app/models/spree_cm_commissioner/inventory.rb +11 -0
  50. data/app/models/spree_cm_commissioner/inventory_item.rb +69 -0
  51. data/app/models/spree_cm_commissioner/line_item_decorator.rb +47 -78
  52. data/app/models/spree_cm_commissioner/notification_taxon.rb +1 -1
  53. data/app/models/spree_cm_commissioner/option_type_decorator.rb +1 -11
  54. data/app/models/spree_cm_commissioner/order/address_book_decorator.rb +30 -12
  55. data/app/models/spree_cm_commissioner/order_decorator.rb +38 -1
  56. data/app/models/spree_cm_commissioner/place.rb +3 -4
  57. data/app/models/spree_cm_commissioner/price_decorator.rb +9 -0
  58. data/app/models/spree_cm_commissioner/product_decorator.rb +39 -6
  59. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +41 -0
  60. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +126 -0
  61. data/app/models/spree_cm_commissioner/redis_stock/line_items_cached_inventory_items_builder.rb +36 -0
  62. data/app/models/spree_cm_commissioner/redis_stock/variant_cached_inventory_items_builder.rb +25 -0
  63. data/app/models/spree_cm_commissioner/reserved_block.rb +30 -0
  64. data/app/models/spree_cm_commissioner/saved_guest.rb +15 -0
  65. data/app/models/spree_cm_commissioner/seat_layout.rb +20 -0
  66. data/app/models/spree_cm_commissioner/seat_section.rb +16 -0
  67. data/app/models/spree_cm_commissioner/seats/blocks_canceler.rb +30 -0
  68. data/app/models/spree_cm_commissioner/seats/blocks_holder.rb +53 -0
  69. data/app/models/spree_cm_commissioner/seats/blocks_reserver.rb +49 -0
  70. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_on_hold_by_other_guest.rb +4 -0
  71. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_other_guest.rb +4 -0
  72. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_same_guest.rb +4 -0
  73. data/app/models/spree_cm_commissioner/seats/errors/unable_to_save_reserved_block_record.rb +4 -0
  74. data/app/models/spree_cm_commissioner/service_calendar.rb +0 -2
  75. data/app/models/spree_cm_commissioner/state_decorator.rb +0 -1
  76. data/app/models/spree_cm_commissioner/stock/availability_checker.rb +26 -25
  77. data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +2 -1
  78. data/app/models/spree_cm_commissioner/stock/line_item_availability_checker.rb +3 -3
  79. data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +44 -0
  80. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +17 -0
  81. data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -1
  82. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +0 -6
  83. data/app/models/spree_cm_commissioner/trip.rb +19 -10
  84. data/app/models/spree_cm_commissioner/trip_connection.rb +5 -5
  85. data/app/models/spree_cm_commissioner/trip_stop.rb +10 -21
  86. data/app/models/spree_cm_commissioner/variant_block.rb +17 -0
  87. data/app/models/spree_cm_commissioner/variant_decorator.rb +37 -47
  88. data/app/models/spree_cm_commissioner/variant_options.rb +0 -23
  89. data/app/models/spree_cm_commissioner/vehicle.rb +11 -16
  90. data/app/models/spree_cm_commissioner/vendor_decorator.rb +14 -14
  91. data/app/models/spree_cm_commissioner/vendor_place.rb +13 -3
  92. data/app/overrides/spree/admin/products/_form/industry_taxons.html.erb.deface +26 -0
  93. data/app/overrides/spree/admin/products/_form/taxons.html.erb.deface +26 -0
  94. data/app/queries/spree_cm_commissioner/trip_query.rb +110 -131
  95. data/app/request_schemas/spree_cm_commissioner/accommodation_request_schema.rb +3 -0
  96. data/app/request_schemas/spree_cm_commissioner/application_request_schema.rb +1 -1
  97. data/app/request_schemas/spree_cm_commissioner/trip_search_request_schema.rb +20 -0
  98. data/app/request_schemas/spree_cm_commissioner/variant_request_schema.rb +19 -0
  99. data/app/serializers/spree/v2/storefront/accommodation_serializer.rb +2 -0
  100. data/app/serializers/spree/v2/storefront/amenity_serializer.rb +15 -0
  101. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +1 -0
  102. data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +2 -0
  103. data/app/serializers/spree/v2/storefront/trip_place_serializer.rb +10 -0
  104. data/app/serializers/spree/v2/storefront/trip_query_result_serializer.rb +14 -0
  105. data/app/serializers/spree/v2/storefront/trip_result_serializer.rb +17 -0
  106. data/app/serializers/spree/v2/storefront/trip_vehicle_serializer.rb +11 -0
  107. data/app/serializers/spree/v2/storefront/trip_vendor_serializer.rb +13 -0
  108. data/app/views/spree/admin/inventory_items/show.html.erb +72 -0
  109. data/app/views/spree/admin/stock_managements/_variant_stock_items.html.erb +7 -2
  110. data/app/views/spree/admin/stock_managements/calendar.html.erb +56 -0
  111. data/app/views/spree/admin/stock_managements/index.html.erb +55 -6
  112. data/config/initializers/paper_trail.rb +1 -0
  113. data/config/initializers/spree_permitted_attributes.rb +5 -0
  114. data/config/routes.rb +24 -2
  115. data/db/migrate/20240202080634_update_counter_cache_of_vehicle_type.rb +3 -1
  116. data/db/migrate/20250122000000_move_route_type_from_vehicles_to_trips.rb +7 -0
  117. data/db/migrate/20250304293518_create_cm_inventory_items.rb +21 -0
  118. data/db/migrate/20250429094228_add_lock_version_to_cm_inventory_items.rb +5 -0
  119. data/db/migrate/20250502025848_add_index_to_spree_products.rb +5 -0
  120. data/db/migrate/20250502030001_add_product_type_to_spree_variants.rb +5 -0
  121. data/db/migrate/20250502030002_add_product_type_to_spree_line_items.rb +5 -0
  122. data/db/migrate/20250603035256_add_inventory_item_to_spree_prices.rb +7 -0
  123. data/db/migrate/20250619073724_drop_table_cm_line_item_seats.rb +5 -0
  124. data/db/migrate/20250619073812_drop_table_cm_vehicle_seats.rb +5 -0
  125. data/db/migrate/20250619073844_drop_table_cm_vehicle_types.rb +9 -0
  126. data/db/migrate/20250619073957_drop_table_cm_option_value_vehicle_types.rb +5 -0
  127. data/db/migrate/20250619082354_remove_unnecessary_fields_from_cm_places.rb +9 -0
  128. data/db/migrate/20250619082736_remove_route_type_from_spree_products.rb +5 -0
  129. data/db/migrate/20250619083055_remove_unnecessary_fields_from_spree_taxons.rb +5 -0
  130. data/db/migrate/20250620083055_remove_variant_id_from_cm_trips.rb +5 -0
  131. data/db/migrate/20250620090000_update_cm_trip_connections_to_use_cm_trips.rb +6 -0
  132. data/db/migrate/20250620090001_create_cm_seat_layouts.rb +17 -0
  133. data/db/migrate/20250620090002_create_cm_seat_sections.rb +18 -0
  134. data/db/migrate/20250620090003_create_cm_blocks.rb +18 -0
  135. data/db/migrate/20250624091005_create_cm_reserved_blocks.rb +29 -0
  136. data/db/migrate/20250626083642_create_cm_variant_blocks.rb +24 -0
  137. data/db/migrate/20250627023314_add_block_id_to_cm_guests.rb +13 -0
  138. data/db/migrate/20250716022821_add_location_reference_to_cm_vendor_places.rb +5 -0
  139. data/db/migrate/20250716031743_drop_table_cm_vendor_stops.rb +5 -0
  140. data/db/migrate/20250717023824_add_vendor_reference_to_cm_trips.rb +5 -0
  141. data/db/migrate/20250717041414_add_location_place_reference_to_cm_trip_stops.rb +5 -0
  142. data/db/migrate/20250717042539_rename_cm_trip_stops_stop_id_column_to_stop_place_id.rb +7 -0
  143. data/db/migrate/20250717042707_rename_cm_trips_origin_and_destination_to_origin_place_and_destination_place.rb +11 -0
  144. data/db/migrate/20250721080738_add_vehicle_type_to_cm_vehicles.rb +7 -0
  145. data/db/migrate/20250725092713_add_block_type_to_cm_blocks.rb +5 -0
  146. data/db/migrate/20250731062816_add_departure_time_and_arrival_time_to_trip_stop.rb +6 -0
  147. data/db/migrate/20250807033035_create_spree_cm_commissioner_saved_guests.rb +23 -0
  148. data/db/migrate/20250808054835_add_saved_guest_reference_to_cm_blocks.rb +5 -0
  149. data/db/migrate/20250812015522_remove_unique_indexes_from_cm_user_identity_providers.rb +8 -0
  150. data/db/migrate/20250814073854_add_call_to_action_to_spree_products.rb +5 -0
  151. data/docker-compose.yml +1 -1
  152. data/lib/cm_app_logger.rb +11 -4
  153. data/lib/generators/spree_cm_commissioner/install/install_generator.rb +14 -11
  154. data/lib/generators/spree_cm_commissioner/install/templates/app/javascript/{spree_cm_commissioner → spree_dashboard/spree_cm_commissioner}/utilities.js +4 -0
  155. data/lib/spree_cm_commissioner/cached_inventory_item.rb +23 -0
  156. data/lib/spree_cm_commissioner/calendar_event.rb +11 -1
  157. data/lib/spree_cm_commissioner/test_helper/factories/block_factory.rb +16 -0
  158. data/lib/spree_cm_commissioner/test_helper/factories/guest_factory.rb +10 -0
  159. data/lib/spree_cm_commissioner/test_helper/factories/homepage_section_relatable_factory.rb +1 -1
  160. data/lib/spree_cm_commissioner/test_helper/factories/inventory_item_factory.rb +9 -0
  161. data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +1 -1
  162. data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +6 -30
  163. data/lib/spree_cm_commissioner/test_helper/factories/order_factory.rb +0 -36
  164. data/lib/spree_cm_commissioner/test_helper/factories/product_factory.rb +18 -34
  165. data/lib/spree_cm_commissioner/test_helper/factories/reserved_block_factory.rb +27 -0
  166. data/lib/spree_cm_commissioner/test_helper/factories/seat_layout_factory.rb +11 -0
  167. data/lib/spree_cm_commissioner/test_helper/factories/seat_section_factory.rb +16 -0
  168. data/lib/spree_cm_commissioner/test_helper/factories/stock_location_factory.rb +2 -2
  169. data/lib/spree_cm_commissioner/test_helper/factories/trip_connection_factory.rb +6 -0
  170. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +11 -3
  171. data/lib/spree_cm_commissioner/test_helper/factories/trip_stop_factory.rb +13 -0
  172. data/lib/spree_cm_commissioner/test_helper/factories/variant_block_factory.rb +7 -0
  173. data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +41 -19
  174. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_factory.rb +1 -1
  175. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +6 -1
  176. data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +13 -1
  177. data/lib/spree_cm_commissioner/transit/leg.rb +37 -0
  178. data/lib/spree_cm_commissioner/transit/seat_selection.rb +27 -0
  179. data/lib/spree_cm_commissioner/trip_query_result.rb +50 -0
  180. data/lib/spree_cm_commissioner/trip_result.rb +19 -10
  181. data/lib/spree_cm_commissioner/version.rb +1 -1
  182. data/lib/spree_cm_commissioner.rb +37 -1
  183. data/lib/tasks/create_default_non_permanent_inventory_items.rake +16 -0
  184. data/lib/tasks/ensure_correct_product_type.rake +7 -0
  185. data/lib/tasks/generate_inventory_items.rake +7 -0
  186. data/spree_cm_commissioner.gemspec +7 -0
  187. metadata +167 -26
  188. data/app/controllers/concerns/spree_cm_commissioner/transit/taxon_bitwise.rb +0 -44
  189. data/app/finders/spree_cm_commissioner/line_items/find_by_variant_decorator.rb +0 -20
  190. data/app/models/spree_cm_commissioner/branch.rb +0 -12
  191. data/app/models/spree_cm_commissioner/line_item_seat.rb +0 -10
  192. data/app/models/spree_cm_commissioner/option_value_vehicle_type.rb +0 -8
  193. data/app/models/spree_cm_commissioner/stop.rb +0 -23
  194. data/app/models/spree_cm_commissioner/vehicle_seat.rb +0 -11
  195. data/app/models/spree_cm_commissioner/vehicle_type.rb +0 -76
  196. data/app/models/spree_cm_commissioner/vendor_stop.rb +0 -10
  197. data/app/queries/spree_cm_commissioner/trip_search_query.rb +0 -76
  198. data/app/queries/spree_cm_commissioner/variant_availability/non_permanent_stock_query.rb +0 -45
  199. data/app/queries/spree_cm_commissioner/variant_availability/permanent_stock_query.rb +0 -55
  200. data/app/queries/spree_cm_commissioner/vendor_stop_place_query.rb +0 -54
  201. data/app/services/spree_cm_commissioner/vehicle_option_value_creator.rb +0 -11
  202. data/lib/spree_cm_commissioner/test_helper/factories/branch_factory.rb +0 -12
  203. data/lib/spree_cm_commissioner/test_helper/factories/departure_time_option_type_factory.rb +0 -8
  204. data/lib/spree_cm_commissioner/test_helper/factories/duration_option_type_factory.rb +0 -8
  205. data/lib/spree_cm_commissioner/test_helper/factories/line_item_seat_factory.rb +0 -7
  206. data/lib/spree_cm_commissioner/test_helper/factories/stop_factory.rb +0 -14
  207. data/lib/spree_cm_commissioner/test_helper/factories/transit_place_factory.rb +0 -8
  208. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_option_type_factory.rb +0 -8
  209. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_type_factory.rb +0 -96
  210. data/lib/spree_cm_commissioner/trip_seat_layout_result.rb +0 -11
@@ -0,0 +1,37 @@
1
+ module SpreeCmCommissioner
2
+ module Accommodations
3
+ class Find
4
+ attr_reader :from_date, :to_date, :state_id, :number_of_guests
5
+
6
+ def initialize(from_date:, to_date:, state_id:, number_of_adults:, number_of_kids:)
7
+ @from_date = from_date
8
+ @to_date = to_date
9
+ @state_id = state_id
10
+ @number_of_guests = number_of_adults.to_i + number_of_kids.to_i
11
+ end
12
+
13
+ def execute
14
+ scope
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
19
+ )
20
+ .where('inventory_items.quantity_available > 0')
21
+ .distinct
22
+ end
23
+
24
+ private
25
+
26
+ def scope
27
+ Spree::Vendor
28
+ .joins(variants: :inventory_items)
29
+ .where(primary_product_type: :accommodation, state: :active)
30
+ end
31
+
32
+ def stay_dates
33
+ from_date..to_date
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ module SpreeCmCommissioner
2
+ module Accommodations
3
+ class FindVariant
4
+ attr_reader :vendor_id, :from_date, :to_date, :number_of_guests
5
+
6
+ def initialize(vendor_id:, from_date:, to_date:, number_of_adults:, number_of_kids:)
7
+ @vendor_id = vendor_id
8
+ @from_date = from_date
9
+ @to_date = to_date
10
+ @number_of_guests = number_of_adults.to_i + number_of_kids.to_i
11
+ end
12
+
13
+ def execute
14
+ Spree::Variant
15
+ .joins(:inventory_items)
16
+ .where(vendor_id: vendor_id)
17
+ .where(inventory_items: { inventory_date: stay_dates })
18
+ .where('CAST(public_metadata->\'cm_options\'->>\'number-of-adults\' AS INTEGER) +
19
+ CAST(public_metadata->\'cm_options\'->>\'number-of-kids\' AS INTEGER) >= ?', number_of_guests
20
+ )
21
+ .where('inventory_items.quantity_available > 0')
22
+ .distinct
23
+ end
24
+
25
+ private
26
+
27
+ def stay_dates
28
+ from_date..to_date
29
+ end
30
+ end
31
+ end
32
+ 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,44 @@
1
+ module SpreeCmCommissioner
2
+ module Stock
3
+ class InventoryItemResetter < BaseInteractor
4
+ delegate :inventory_item, to: :context
5
+
6
+ def call
7
+ max_capacity = variant_total_inventory
8
+ total_purchases = variant_total_purchases
9
+ quantity_available = [max_capacity - total_purchases, 0].max
10
+
11
+ updated = inventory_item.update(max_capacity: max_capacity, quantity_available: quantity_available)
12
+ return context.fail!(message: 'Failed to update inventory item', errors: inventory_item.errors.full_messages) unless updated
13
+
14
+ clear_inventory_cache
15
+ end
16
+
17
+ def variant_total_inventory
18
+ # for shipment, total_on_hand is not orignal stock. shipment does subtract the stock.
19
+ # to get desire result, we need to add to total_purchase.
20
+ if inventory_item.variant.delivery_required?
21
+ inventory_item.variant.total_on_hand.to_i + variant_total_purchases
22
+ else
23
+ inventory_item.variant.total_on_hand.to_i
24
+ end
25
+ end
26
+
27
+ def variant_total_purchases
28
+ scope = inventory_item.variant.complete_line_items
29
+
30
+ if inventory_item.permanent_stock?
31
+ scope.where('? BETWEEN from_date AND to_date', inventory_item.inventory_date).sum(:quantity).to_i
32
+ else
33
+ scope.sum(:quantity).to_i
34
+ end
35
+ end
36
+
37
+ def clear_inventory_cache
38
+ SpreeCmCommissioner.redis_pool.with do |redis|
39
+ redis.del(inventory_item.redis_key)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ 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,15 @@
1
+ module SpreeCmCommissioner
2
+ module Stock
3
+ class InventoryItemsGenerator < BaseInteractor
4
+ delegate :variant, to: :context
5
+
6
+ def call
7
+ if variant.permanent_stock?
8
+ SpreeCmCommissioner::Stock::PermanentInventoryItemsGenerator.call(variant_ids: [variant.id])
9
+ else
10
+ variant.create_default_non_permanent_inventory_item! unless variant.default_inventory_item_exist?
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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,32 @@
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
+ adjust_inventory_items_async(variant_id, 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
+
23
+ private
24
+
25
+ def adjust_inventory_items_async(variant_id, quantity)
26
+ CmAppLogger.log(label: "#{self.class.name}#adjust_inventory_items_async", data: { variant_id: variant_id, quantity: quantity }) do
27
+ SpreeCmCommissioner::Stock::InventoryItemsAdjusterJob.perform_later(variant_id: variant_id, quantity: quantity)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,66 @@
1
+ module SpreeCmCommissioner
2
+ class TaxiTripCreator < BaseInteractor
3
+ delegate :trip_params, to: :context
4
+
5
+ def call
6
+ ActiveRecord::Base.transaction do
7
+ create_trip_with_product
8
+ create_variant
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def create_trip_with_product
15
+ trip = SpreeCmCommissioner::Trip.new(trip_params)
16
+
17
+ if trip.save
18
+ context.trip = trip
19
+ context.product = trip.product
20
+ assign_option_type_to_product
21
+ context.product.save
22
+ else
23
+ context.fail!(error: "Failed to create taxi trip: #{trip.errors.full_messages.join(', ')}")
24
+ end
25
+ end
26
+
27
+ def create_variant
28
+ variant = context.product.variants.new(
29
+ price: trip_params['product_attributes']['price'],
30
+ sku: "TRIP-#{context.formatted_name}-#{Time.current.to_i}",
31
+ option_value_ids: option_value_ids
32
+ )
33
+
34
+ if variant.save
35
+ context.variant = variant
36
+ create_stock_item_for_variant(variant)
37
+ else
38
+ context.fail!(error: "Failed to create taxi trip variant: #{variant.errors.full_messages.join(', ')}")
39
+ end
40
+ end
41
+
42
+ def create_stock_item_for_variant(variant)
43
+ stock_location = context.product.vendor.stock_location
44
+ if stock_location.present?
45
+ Spree::StockItem.find_or_create_by!(variant: variant, stock_location: stock_location) do |item|
46
+ item.count_on_hand = 1
47
+ end
48
+ else
49
+ context.fail!(error: 'Vendor does not have an active stock location.')
50
+ end
51
+ end
52
+
53
+ def assign_option_type_to_product
54
+ option_type = Spree::OptionType.find_by(name: 'intercity-taxi')
55
+ return unless option_type
56
+
57
+ context.product.option_types << option_type
58
+ end
59
+
60
+ def option_value_ids
61
+ context.product.option_types.flat_map do |option_type|
62
+ option_type.option_values.pluck(:id)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,132 @@
1
+ # DraftOrderCreator creates a new order for a entire journey, including outbound and (if present) inbound directions.
2
+ #
3
+ # Attributes:
4
+ # - outbound_date: [Date] The date of outbound transit
5
+ # - inbound_date: [Date] The date of inbound transit
6
+ # - outbound_legs: [Array<SpreeCmCommissioner::Transit::Leg>] Outbound transit legs (one for direct, multiple for connecting)
7
+ # - inbound_legs: [Array<SpreeCmCommissioner::Transit::Leg>] Inbound transit legs (one for direct, multiple for connecting)
8
+ # - user: [User] The user for whom the order is being created
9
+ #
10
+ # Each line item is built with precise from_date and to_date based on legs and trip stop times.
11
+ # Each line item has guests built corresponding to seat selection blocks.
12
+ #
13
+ # Note: inbound_legs and inbound_date are optional; the order may represent a one-way (outbound only) journey.
14
+ module SpreeCmCommissioner
15
+ module Transit
16
+ class DraftOrderCreator < BaseInteractor
17
+ delegate :outbound_date,
18
+ :inbound_date,
19
+ :outbound_legs,
20
+ :inbound_legs,
21
+ :user, to: :context
22
+
23
+ def call
24
+ return context.fail!(message: 'Outbound legs are missing') if outbound_legs.blank?
25
+
26
+ begin
27
+ context.order = create_order!
28
+ rescue StandardError => e
29
+ context.fail!(message: e.message)
30
+ end
31
+ end
32
+
33
+ def create_order!
34
+ order = Spree::Order.new(state: 'cart', use_billing: true, user: user)
35
+
36
+ outbound_line_items = build_line_items_for_legs!(order: order, legs: outbound_legs, initial_date: outbound_date)
37
+ inbound_line_items = inbound_legs.blank? ? [] : build_line_items_for_legs!(order: order, legs: inbound_legs, initial_date: inbound_date)
38
+
39
+ outbound_qty = outbound_line_items.sum(&:quantity)
40
+ inbound_qty = inbound_line_items.sum(&:quantity)
41
+
42
+ raise StandardError, 'Outbound & inbound line item quantities do not match' if inbound_line_items.any? && outbound_qty != inbound_qty
43
+ raise StandardError, order.errors.full_messages.to_sentence unless order.save
44
+
45
+ order
46
+ end
47
+
48
+ # Build line items for each leg, using the previous leg's to_date as the next leg's date,
49
+ # or the initial date for the first leg.
50
+ def build_line_items_for_legs!(order:, legs:, initial_date:)
51
+ all_line_items = []
52
+ current_leg_date = initial_date
53
+
54
+ legs.each do |leg|
55
+ leg_line_items = build_line_items_for!(leg, order, current_leg_date)
56
+ leg_line_items = insert_saved_guests_per_line_items_leg(leg_line_items)
57
+
58
+ all_line_items.concat(leg_line_items)
59
+ current_leg_date = leg_line_items.last.to_date.to_date if leg_line_items.any?
60
+ end
61
+
62
+ all_line_items
63
+ end
64
+
65
+ def build_line_items_for!(leg, order, date)
66
+ trip = SpreeCmCommissioner::Trip.find(leg.trip_id)
67
+ trip_stops = trip.trip_stops.where(id: [leg.boarding_trip_stop_id, leg.drop_off_trip_stop_id]).index_by(&:id)
68
+
69
+ from_date, to_date = calculate_line_item_duration!(
70
+ date: date,
71
+ departure_time: trip_stops[leg.boarding_trip_stop_id].departure_time,
72
+ arrival_time: trip_stops[leg.drop_off_trip_stop_id].arrival_time
73
+ )
74
+
75
+ leg.seat_selections.group_by(&:variant_id).map do |variant_id, seat_selections|
76
+ line_item = order.line_items.new(
77
+ product_type: :transit,
78
+ from_date: from_date,
79
+ to_date: to_date,
80
+ variant_id: variant_id,
81
+ quantity: seat_selections.sum(&:quantity),
82
+ private_metadata: {
83
+ direction: leg.direction,
84
+ trip_id: leg.trip_id.to_s,
85
+ boarding_trip_stop_id: leg.boarding_trip_stop_id.to_s,
86
+ drop_off_trip_stop_id: leg.drop_off_trip_stop_id.to_s
87
+ }
88
+ )
89
+
90
+ build_guests_for!(line_item, seat_selections)
91
+ end
92
+ end
93
+
94
+ def build_guests_for!(line_item, seat_selections)
95
+ block_ids = seat_selections.flat_map(&:block_ids)
96
+
97
+ if block_ids.any? && block_ids.size != line_item.quantity
98
+ raise StandardError, "Number of blocks (#{block_ids.size}) does not match quantity (#{line_item.quantity})"
99
+ end
100
+
101
+ Array.new(line_item.quantity) do |index|
102
+ line_item.guests.new(block_id: block_ids[index])
103
+ end
104
+
105
+ line_item
106
+ end
107
+
108
+ def insert_saved_guests_per_line_items_leg(line_items)
109
+ line_items.flat_map(&:guests).each_with_index do |guest, index|
110
+ context.saved_guests ||= []
111
+ context.saved_guests << SpreeCmCommissioner::SavedGuest.new if context.saved_guests[index].blank?
112
+
113
+ guest.saved_guest = context.saved_guests[index]
114
+ end
115
+
116
+ line_items
117
+ end
118
+
119
+ def calculate_line_item_duration!(date:, departure_time:, arrival_time:)
120
+ raise StandardError, 'Departure or arrival time in trip stop is missing' if departure_time.blank? || arrival_time.blank?
121
+ raise StandardError, 'Arrival time cannot be before departure time' if arrival_time < departure_time
122
+
123
+ trip_duration_in_seconds = arrival_time - departure_time
124
+
125
+ from_date = date.to_datetime.change(hour: departure_time.hour, min: departure_time.min, sec: departure_time.sec)
126
+ to_date = from_date + trip_duration_in_seconds.seconds
127
+
128
+ [from_date, to_date]
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,62 @@
1
+ module SpreeCmCommissioner
2
+ class VariantBlockUpdater < BaseInteractor
3
+ delegate :variant, :data, :stock_location, :current_store, to: :context
4
+
5
+ def call
6
+ block_ids = data[:block_ids] || []
7
+
8
+ ActiveRecord::Base.transaction do
9
+ update_variant_block!(block_ids, variant)
10
+ update_stock_item!(variant)
11
+ end
12
+
13
+ context.message = 'Update variant blocks successfully'
14
+ rescue StandardError => e
15
+ context.fail!(message: "Update variant blocks Failed: #{e.message}")
16
+ end
17
+
18
+ private
19
+
20
+ def update_variant_block!(block_ids, variant)
21
+ variant.variant_blocks.where.not(block_id: block_ids).destroy_all
22
+
23
+ block_ids.each do |block_id|
24
+ next unless SpreeCmCommissioner::Block.exists?(id: block_id) # Skip invalid block_ids
25
+
26
+ block = SpreeCmCommissioner::VariantBlock.find_by(
27
+ block_id: block_id,
28
+ trip_id: variant.product.trip.id,
29
+ variant_id: variant.id
30
+ )
31
+
32
+ next unless block.nil?
33
+
34
+ SpreeCmCommissioner::VariantBlock.create!(
35
+ block_id: block_id,
36
+ trip_id: variant.product.trip.id,
37
+ variant_id: variant.id
38
+ )
39
+ end
40
+ end
41
+
42
+ def update_stock_item!(variant)
43
+ stock_items = variant.stock_items
44
+
45
+ context.fail!(message: 'Variant has more than one stock item. Please report to admin.') if stock_items.count > 1
46
+
47
+ stock_item = stock_items.first || variant.stock_items.create!(stock_location: stock_location)
48
+ quantity_difference = variant.variant_blocks.count - stock_item.count_on_hand
49
+
50
+ resource = SpreeCmCommissioner::Stock::StockMovementCreator.call(
51
+ variant_id: variant.id,
52
+ stock_location_id: stock_location.id,
53
+ current_store: current_store,
54
+ stock_movement_params: { quantity: quantity_difference }
55
+ )
56
+
57
+ return if resource.success?
58
+
59
+ context.fail!(message: "Stock update failed: #{resource.errors}")
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,41 @@
1
+ module SpreeCmCommissioner
2
+ class VehicleUpdater < BaseInteractor
3
+ def call
4
+ vehicle = context.vehicle
5
+ data = context.data
6
+
7
+ option_value_ids = data[:option_value_ids] || []
8
+ option_type_ids = data[:option_type_ids] || []
9
+
10
+ ActiveRecord::Base.transaction do
11
+ update_vehicle!(vehicle, data[:vehicle_attributes])
12
+ update_amenities!(vehicle, option_value_ids, option_type_ids)
13
+ end
14
+
15
+ context.message = I18n.t('views.transit.vehicles.update_success')
16
+ rescue StandardError => e
17
+ context.fail!(message: "Failed to update vehicle: #{e.message}")
18
+ end
19
+
20
+ private
21
+
22
+ def update_vehicle!(vehicle, attributes)
23
+ return if vehicle.update(attributes.slice(:code, :license_plate, :allow_seat_selection, :vehicle_type, :number_of_seats))
24
+
25
+ raise vehicle.errors.full_messages.to_sentence
26
+ end
27
+
28
+ def update_amenities!(vehicle, option_value_ids, option_type_ids)
29
+ vehicle.option_value_vehicles.where.not(option_value_id: option_value_ids).destroy_all
30
+ vehicle.vehicle_option_types.where.not(option_type_id: option_type_ids).destroy_all
31
+
32
+ option_value_ids.each do |option_value_id|
33
+ vehicle.option_value_vehicles.find_or_create_by(option_value_id: option_value_id)
34
+ end
35
+
36
+ option_type_ids.each do |option_type_id|
37
+ vehicle.vehicle_option_types.find_or_create_by(option_type_id: option_type_id)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,25 @@
1
1
  module SpreeCmCommissioner
2
2
  class ApplicationJob < ::ApplicationJob
3
3
  queue_as :default
4
+
5
+ around_perform :log_exceptions
6
+
7
+ private
8
+
9
+ def log_exceptions
10
+ yield
11
+ rescue StandardError => e
12
+ CmAppLogger.log(
13
+ label: "#{self.class.name} failed",
14
+ data: {
15
+ error_class: e.class.name,
16
+ error_message: e.message,
17
+ arguments: arguments.inspect,
18
+ backtrace: e.backtrace&.first(5)&.join("\n")
19
+ }
20
+ )
21
+
22
+ raise # Re-raise the exception to let Sidekiq handle retries or failure
23
+ end
4
24
  end
5
25
  end
@@ -2,5 +2,25 @@
2
2
  module SpreeCmCommissioner
3
3
  class ApplicationUniqueJob < ::ApplicationUniqueJob
4
4
  queue_as :default
5
+
6
+ around_perform :log_exceptions
7
+
8
+ private
9
+
10
+ def log_exceptions
11
+ yield
12
+ rescue StandardError => e
13
+ CmAppLogger.log(
14
+ label: "#{self.class.name} failed",
15
+ data: {
16
+ error_class: e.class.name,
17
+ error_message: e.message,
18
+ arguments: arguments.inspect,
19
+ backtrace: e.backtrace&.first(5)&.join("\n")
20
+ }
21
+ )
22
+
23
+ raise # Re-raise the exception to let Sidekiq handle retries or failure
24
+ end
5
25
  end
6
26
  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