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,7 @@
1
+ module SpreeCmCommissioner
2
+ class InventoryItemSyncerJob < ApplicationUniqueJob
3
+ def perform(inventory_id_and_quantities:)
4
+ InventoryItemSyncer.call(inventory_id_and_quantities:)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ module Stock
3
+ class InventoryItemsAdjusterJob < ApplicationUniqueJob
4
+ def perform(variant_id:, quantity:)
5
+ variant = Spree::Variant.find(variant_id)
6
+
7
+ SpreeCmCommissioner::Stock::InventoryItemsAdjuster.call(variant: variant, quantity: quantity)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ module Stock
3
+ class InventoryItemsGeneratorJob < ApplicationUniqueJob
4
+ def perform(variant_id:)
5
+ variant = Spree::Variant.find(variant_id)
6
+
7
+ SpreeCmCommissioner::Stock::InventoryItemsGenerator.call(variant: variant)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ module Stock
3
+ class PermanentInventoryItemsGeneratorJob < ApplicationUniqueJob
4
+ def perform
5
+ SpreeCmCommissioner::Stock::PermanentInventoryItemsGenerator.call
6
+ end
7
+ end
8
+ end
9
+ end
@@ -18,29 +18,32 @@ module SpreeCmCommissioner
18
18
  def date_unit
19
19
  return nil unless permanent_stock?
20
20
 
21
- date_range_excluding_checkout.size if accommodation?
21
+ date_range.size
22
22
  end
23
23
 
24
- def date_range_excluding_checkout
24
+ def date_range
25
25
  return [] unless date_present?
26
+ return [] unless permanent_stock?
26
27
 
27
- date_range = (from_date.to_date..to_date.to_date).to_a
28
- date_range.pop if date_range.size > 1
29
- date_range
30
- end
28
+ # For transit products (e.g., bus, train), only return the departure day,
29
+ # as the booking represents a single trip rather than a duration.
30
+ return [from_date.to_date] if transit?
31
31
 
32
- def date_range_including_checkout
33
- return [] unless date_present?
32
+ # For products with a stay or usage period (e.g., hotel, car rental, activities),
33
+ # return the full date range from start to end, inclusive.
34
+ return (from_date.to_date..to_date.to_date).to_a if service? || accommodation?
34
35
 
35
- (from_date.to_date..to_date.to_date).to_a
36
+ []
36
37
  end
37
38
 
38
- def date_range
39
- if accommodation?
40
- date_range_excluding_checkout
41
- else
42
- date_range_including_checkout
43
- end
39
+ def checkin_date
40
+ from_date&.to_datetime
41
+ end
42
+
43
+ def checkout_date
44
+ return to_date ? to_date.to_datetime + 1.day : nil if accommodation?
45
+
46
+ to_date&.to_datetime
44
47
  end
45
48
 
46
49
  private
@@ -1,13 +1,21 @@
1
+ # Number of guests are depend on the variant, but user is allowed to input custom number of guests
2
+ # which will be stored in public_metadata.
3
+ #
4
+ # metadata {
5
+ # 'number_of_adults': 2,
6
+ # 'number_of_kids': 1,
7
+ # }
1
8
  module SpreeCmCommissioner
2
9
  module LineItemGuestsConcern
3
10
  extend ActiveSupport::Concern
4
11
 
5
12
  included do
6
- validate :validate_total_adults, if: -> { public_metadata[:number_of_adults].present? }
7
- validate :validate_total_kids, if: -> { public_metadata[:number_of_kids].present? }
13
+ validate :validate_total_adults, if: -> { valid_integer_metadata?('number_of_adults') }
14
+ validate :validate_total_kids, if: -> { valid_integer_metadata?('number_of_kids') }
8
15
  end
9
16
 
10
- def remaining_total_guests = [number_of_guests - guests.size, 0].max
17
+ def number_of_adults = public_metadata['number_of_adults']&.to_i || (variant.number_of_adults * quantity)
18
+ def number_of_kids = public_metadata['number_of_kids']&.to_i || (variant.number_of_kids * quantity)
11
19
  def number_of_guests = number_of_adults + number_of_kids
12
20
 
13
21
  def allowed_extra_adults = variant.allowed_extra_adults * quantity
@@ -16,8 +24,7 @@ module SpreeCmCommissioner
16
24
  def allowed_total_adults = (variant.number_of_adults * quantity) + allowed_extra_adults
17
25
  def allowed_total_kids = (variant.number_of_kids * quantity) + allowed_extra_kids
18
26
 
19
- def number_of_adults = public_metadata[:number_of_adults] || (variant.number_of_adults * quantity)
20
- def number_of_kids = public_metadata[:number_of_kids] || (variant.number_of_kids * quantity)
27
+ def remaining_total_guests = [number_of_guests - guests.size, 0].max
21
28
 
22
29
  def generate_remaining_guests
23
30
  return if remaining_total_guests.zero?
@@ -28,15 +35,17 @@ module SpreeCmCommissioner
28
35
  end
29
36
 
30
37
  def extra_adults
38
+ return 0 unless valid_integer_metadata?('number_of_adults')
31
39
  return 0 unless extra_adults?
32
40
 
33
- public_metadata[:number_of_adults] - (variant.number_of_adults * quantity)
41
+ public_metadata['number_of_adults'].to_i - (variant.number_of_adults * quantity)
34
42
  end
35
43
 
36
44
  def extra_kids
45
+ return 0 unless valid_integer_metadata?('number_of_kids')
37
46
  return 0 unless extra_kids?
38
47
 
39
- public_metadata[:number_of_kids] - (variant.number_of_kids * quantity)
48
+ public_metadata['number_of_kids'].to_i - (variant.number_of_kids * quantity)
40
49
  end
41
50
 
42
51
  def guest_options
@@ -55,25 +64,30 @@ module SpreeCmCommissioner
55
64
  end
56
65
 
57
66
  def extra_adults?
58
- public_metadata[:number_of_adults].present? && public_metadata[:number_of_adults] > variant.number_of_adults * quantity
67
+ return false unless valid_integer_metadata?('number_of_adults')
68
+
69
+ public_metadata['number_of_adults'].to_i > variant.number_of_adults * quantity
59
70
  end
60
71
 
61
72
  def extra_kids?
62
- public_metadata[:number_of_kids].present? && public_metadata[:number_of_kids] > variant.number_of_kids * quantity
73
+ return false unless valid_integer_metadata?('number_of_kids')
74
+
75
+ public_metadata['number_of_kids'].to_i > variant.number_of_kids * quantity
63
76
  end
64
77
 
65
78
  private
66
79
 
67
- def validate_total_adults
68
- return if public_metadata[:number_of_adults] <= allowed_total_adults
80
+ def valid_integer_metadata?(key)
81
+ value = public_metadata[key.to_s]
82
+ value.present? && value.to_s.match?(/\A-?\d+\z/)
83
+ end
69
84
 
70
- errors.add(:quantity, 'exceed_total_adults')
85
+ def validate_total_adults
86
+ errors.add(:quantity, 'exceed_total_adults') if public_metadata['number_of_adults'].to_i > allowed_total_adults
71
87
  end
72
88
 
73
89
  def validate_total_kids
74
- return if public_metadata[:number_of_kids] <= allowed_total_kids
75
-
76
- errors.add(:quantity, 'exceed_total_kids')
90
+ errors.add(:quantity, 'exceed_total_kids') if public_metadata['number_of_kids'].to_i > allowed_total_kids
77
91
  end
78
92
  end
79
93
  end
@@ -0,0 +1,89 @@
1
+ # This concern allow line item to have trip-related metadata.
2
+ #
3
+ # Metadata structure:
4
+ # {
5
+ # 'direction': 'outbound' or 'inbound',
6
+ # 'trip_id': 123,
7
+ # 'boarding_trip_stop_id': 456,
8
+ # 'drop_off_trip_stop_id': 789
9
+ # }
10
+ #
11
+ # Direction:
12
+ # - "outbound": The trip is moving away from the origin (e.g., A → B → C → D).
13
+ # This term is more flexible than "departure" and applies to both direct and connected trips.
14
+ # - "inbound": The trip is returning toward the origin (e.g., D → C → B → A).
15
+ # This term is more flexible than "return" and applies to both direct and connected trips.
16
+ module SpreeCmCommissioner
17
+ module LineItemTransitable
18
+ extend ActiveSupport::Concern
19
+
20
+ DIRECTION = %w[outbound inbound].freeze
21
+
22
+ TripKey = Struct.new(:trip_id, :direction, :boarding_trip_stop_id, :drop_off_trip_stop_id)
23
+
24
+ included do
25
+ validates :direction, inclusion: { in: DIRECTION }, allow_nil: true
26
+ validates :trip_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
27
+ validates :boarding_trip_stop_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
28
+ validates :drop_off_trip_stop_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
29
+
30
+ # Groups LineItems by trip fields. Some fields can be null,
31
+ # so use it with caution. Output:
32
+ # {
33
+ # $<TripKey:> => [<LineItem id:1>]
34
+ # $<TripKey:> => [<LineItem id:2>]
35
+ # $<TripKey:> => [<LineItem id:3>, <LineItem id:4>]
36
+ # }
37
+ def self.group_by_trip_key
38
+ all.group_by do |line_item|
39
+ TripKey.new(
40
+ line_item.trip_id,
41
+ line_item.direction,
42
+ line_item.boarding_trip_stop_id,
43
+ line_item.drop_off_trip_stop_id
44
+ )
45
+ end
46
+ end
47
+ end
48
+
49
+ def outbound? = direction == 'outbound'
50
+ def inbound? = direction == 'inbound'
51
+
52
+ def direction = private_metadata['direction']
53
+ def trip_id = private_metadata['trip_id']&.to_i
54
+ def boarding_trip_stop_id = private_metadata['boarding_trip_stop_id']&.to_i
55
+ def drop_off_trip_stop_id = private_metadata['drop_off_trip_stop_id']&.to_i
56
+
57
+ def direction=(value)
58
+ if value.nil?
59
+ private_metadata.delete('direction')
60
+ else
61
+ private_metadata['direction'] = value.to_s
62
+ end
63
+ end
64
+
65
+ def trip_id=(value)
66
+ if value.nil?
67
+ private_metadata.delete('trip_id')
68
+ else
69
+ private_metadata['trip_id'] = value.to_s
70
+ end
71
+ end
72
+
73
+ def boarding_trip_stop_id=(value)
74
+ if value.nil?
75
+ private_metadata.delete('boarding_trip_stop_id')
76
+ else
77
+ private_metadata['boarding_trip_stop_id'] = value.to_s
78
+ end
79
+ end
80
+
81
+ def drop_off_trip_stop_id=(value)
82
+ if value.nil?
83
+ private_metadata.delete('drop_off_trip_stop_id')
84
+ else
85
+ private_metadata['drop_off_trip_stop_id'] = value.to_s
86
+ end
87
+ end
88
+ end
89
+ end
@@ -14,13 +14,6 @@ module SpreeCmCommissioner
14
14
  state_selection
15
15
  payment_option
16
16
  delivery_option
17
- amenity
18
- departure_time
19
- duration
20
- vehicle_id
21
- origin
22
- destination
23
- place_id
24
17
  ].freeze
25
18
 
26
19
  RESERVED_OPTIONS = {
@@ -48,11 +41,8 @@ module SpreeCmCommissioner
48
41
  'bib-display-prefix' => 'boolean',
49
42
  'bib-pre-generation-on-create' => 'boolean',
50
43
  'seat-number-positions' => 'array',
51
- 'origin' => 'place_id',
52
- 'destination' => 'place_id',
53
- 'departure-time' => 'time',
54
- 'vehicle' => 'vehicle_id',
55
- 'allow-seat-selection' => 'boolean'
44
+ 'seat-type' => 'string',
45
+ 'intercity-taxi' => 'string'
56
46
  }.freeze
57
47
 
58
48
  included do
@@ -0,0 +1,44 @@
1
+ module SpreeCmCommissioner
2
+ module OrderSeatable
3
+ def should_manage_blocks?
4
+ preload_block_ids.any?
5
+ end
6
+
7
+ def hold_blocks!
8
+ return unless should_manage_blocks?
9
+
10
+ CmAppLogger.log(label: "#{self.class.name}#hold_blocks!", data: { order_id: id }) do
11
+ SpreeCmCommissioner::Seats::BlocksHolder.new(line_item_ids: line_item_ids, hold_by: user).hold_blocks!
12
+ end
13
+ end
14
+
15
+ def cancel_blocks!
16
+ return unless should_manage_blocks?
17
+
18
+ CmAppLogger.log(label: "#{self.class.name}#cancel_blocks!", data: { order_id: id }) do
19
+ SpreeCmCommissioner::Seats::BlocksCanceler.new(order_id: id, cancel_by: user).cancel_blocks!
20
+ end
21
+ end
22
+
23
+ def reserve_blocks!
24
+ return unless should_manage_blocks?
25
+
26
+ CmAppLogger.log(label: "#{self.class.name}#reserve_blocks!", data: { order_id: id }) do
27
+ SpreeCmCommissioner::Seats::BlocksReserver.new(line_item_ids: line_item_ids, reserve_by: user).reserve_blocks!
28
+ end
29
+ end
30
+
31
+ # Calling `.block_ids` directly can cause many slow database queries (N+1 problem)
32
+ # every time `.should_manage_blocks?` or `.preload_block_ids` runs.
33
+ # To avoid this, we store a precomputed list of block IDs in `private_metadata`.
34
+ # This list is updated whenever a guest’s block is saved or destroy.
35
+ def preload_block_ids=(preload_block_ids = [])
36
+ self.private_metadata ||= {}
37
+ self.private_metadata['preload_block_ids'] = preload_block_ids
38
+ end
39
+
40
+ def preload_block_ids
41
+ self.private_metadata&.fetch('preload_block_ids', []) || []
42
+ end
43
+ end
44
+ end
@@ -5,6 +5,8 @@ module SpreeCmCommissioner
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
+ state_machine.before_transition to: :payment, do: :hold_blocks!
9
+
8
10
  state_machine.before_transition to: :complete, do: :request, if: :need_confirmation?
9
11
  state_machine.before_transition to: :complete, do: :generate_bib_number
10
12
 
@@ -14,10 +16,14 @@ module SpreeCmCommissioner
14
16
  state_machine.after_transition to: :complete, do: :send_order_complete_telegram_alert_to_vendors, unless: :need_confirmation?
15
17
  state_machine.after_transition to: :complete, do: :send_order_complete_telegram_alert_to_store, unless: :need_confirmation?
16
18
 
19
+ state_machine.around_transition to: :complete, do: :unstock_inventory!
20
+
17
21
  state_machine.after_transition to: :complete, do: :send_transaction_email_to_user, if: :user_has_email?
18
22
  state_machine.after_transition to: :resumed, do: :precalculate_conversion
23
+ state_machine.around_transition to: :resumed, do: :unstock_inventory!
19
24
 
20
25
  state_machine.after_transition to: :canceled, do: :precalculate_conversion
26
+ state_machine.after_transition to: :canceled, do: :restock_inventory!
21
27
 
22
28
  scope :accepted, -> { where(request_state: 'accepted') }
23
29
 
@@ -67,6 +73,39 @@ module SpreeCmCommissioner
67
73
  end
68
74
  end
69
75
 
76
+ def unstock_inventory!
77
+ ActiveRecord::Base.transaction do
78
+ yield # Equal to block.call
79
+
80
+ # After the transition is complete, the following code will execute first before proceeding to other `after_transition` callbacks.
81
+ # This ensures that if `reserve_blocks!` or `unstock_inventory_in_redis!` fails, the state will be rolled back,
82
+ # and neither the `finalize!` method nor any notifications will be triggered.
83
+ # The payment will be reversed in vPago gem, and `Spree::Checkout::Complete` will be called, which checks `order.reload.complete?`.
84
+ # This is critical because if the order state is complete, the payment will be marked as paid.
85
+
86
+ reserve_blocks!
87
+ unstock_inventory_in_redis!
88
+
89
+ # We rollback only order state, and we keep payment state as it is.
90
+ # We implement payment in vPago gem, and it will be reversed in the gem.
91
+ # Some bank has api for refund, but some don't have the api to refund yet. So we keep the payment state as it is and refund manually.
92
+ end
93
+ rescue StandardError => e
94
+ CmAppLogger.log(
95
+ label: 'SpreeCmCommissioner::OrderStateMachine#reserve_inventory failed',
96
+ data: { order_id: id, error: e.message, type: e.class.name, backtrace: e.backtrace&.first(5)&.join("\n") }
97
+ )
98
+
99
+ raise e
100
+ end
101
+
102
+ def restock_inventory!
103
+ ActiveRecord::Base.transaction do
104
+ cancel_blocks!
105
+ restock_inventory_in_redis!
106
+ end
107
+ end
108
+
70
109
  def generate_bib_number
71
110
  line_items.find_each(&:generate_remaining_guests)
72
111
 
@@ -3,12 +3,10 @@ module SpreeCmCommissioner
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- delegate :product_type,
7
- :subscribable?,
6
+ delegate :subscribable?,
8
7
  :allowed_upload_later?,
9
8
  :need_confirmation?, :need_confirmation,
10
9
  :allow_anonymous_booking,
11
- :accommodation?, :service?, :ecommerce?,
12
10
  :allow_self_check_in,
13
11
  :allow_self_check_in?,
14
12
  :required_self_check_in_location,
@@ -6,10 +6,20 @@ module SpreeCmCommissioner
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  PRODUCT_TYPES = %i[accommodation service ecommerce transit].freeze
9
+ PERMANENT_STOCK_PRODUCT_TYPES = %w[accommodation service transit].freeze
10
+ PRE_INVENTORY_DAYS = { 'transit' => 90, 'accommodation' => 365, 'service' => 30 }.freeze
9
11
 
10
12
  included do
11
13
  enum product_type: PRODUCT_TYPES if table_exists? && column_names.include?('product_type')
12
14
  enum primary_product_type: PRODUCT_TYPES if table_exists? && column_names.include?('primary_product_type')
13
15
  end
16
+
17
+ def permanent_stock?
18
+ PERMANENT_STOCK_PRODUCT_TYPES.include?(product_type)
19
+ end
20
+
21
+ def pre_inventory_days
22
+ PRE_INVENTORY_DAYS[product_type]
23
+ end
14
24
  end
15
25
  end
@@ -2,7 +2,7 @@ module SpreeCmCommissioner
2
2
  module RouteType
3
3
  extend ActiveSupport::Concern
4
4
 
5
- ROUTE_TYPES = %i[automobile subway rails ferry].freeze
5
+ ROUTE_TYPES = %i[bus intercity_taxi airport_shuttle rails ferry].freeze
6
6
 
7
7
  included do
8
8
  enum route_type: ROUTE_TYPES if table_exists? && column_names.include?('route_type')
@@ -3,7 +3,7 @@ module SpreeCmCommissioner
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- enum kind: { category: 0, cms: 1, event: 2, occupation: 3, nationality: 4, organization: 5, transit: 6 }
6
+ enum kind: { category: 0, cms: 1, event: 2, occupation: 3, nationality: 4, organization: 5, transit: 6, industry: 7 }
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ module VehicleType
3
+ extend ActiveSupport::Concern
4
+
5
+ VEHICLE_TYPES = %i[suv sedan minivan van sleeping_bus luxury_van air_bus].freeze
6
+
7
+ included do
8
+ enum vehicle_type: VEHICLE_TYPES if table_exists? && column_names.include?('vehicle_type')
9
+ end
10
+ end
11
+ end
@@ -1,10 +1,44 @@
1
1
  module SpreeCmCommissioner
2
2
  module AddressDecorator
3
3
  def self.prepended(base)
4
- # Field is not mandatory
5
4
  base.enum gender: { :not_selected => 0, :male => 1, :female => 2, :other => 3 }
5
+
6
+ base.validates :address1, presence: true, if: :require_address1?
7
+ base.validates :city, presence: true, if: :require_city?
8
+ base.validates :country, presence: true, if: :require_country?
9
+ end
10
+
11
+ # override to return false instead of true
12
+ # in case no country
13
+ def require_zipcode?
14
+ country ? country.zipcode_required? : false
15
+ end
16
+
17
+ def require_address1?
18
+ false
19
+ end
20
+
21
+ def require_city?
22
+ false
23
+ end
24
+
25
+ def require_country?
26
+ false
6
27
  end
7
28
  end
8
29
  end
9
30
 
10
- Spree::Address.prepend(SpreeCmCommissioner::AddressDecorator) unless Spree::Address.included_modules.include?(SpreeCmCommissioner::AddressDecorator)
31
+ if Spree::Address.included_modules.exclude?(SpreeCmCommissioner::AddressDecorator)
32
+ keys = %i[address1 city country]
33
+
34
+ Spree::Address._validators.reject! { |key, _| keys.include?(key) }
35
+ Spree::Address._validate_callbacks.each do |c|
36
+ next unless c.filter.respond_to?(:attributes)
37
+
38
+ c.filter.attributes.delete(:address1)
39
+ c.filter.attributes.delete(:city)
40
+ c.filter.attributes.delete(:country)
41
+ end
42
+
43
+ Spree::Address.prepend(SpreeCmCommissioner::AddressDecorator)
44
+ end
@@ -0,0 +1,43 @@
1
+ module SpreeCmCommissioner
2
+ class Block < Base
3
+ enum block_type: {
4
+ seat: 1,
5
+ sleeping_seat: 2,
6
+ driver: 3,
7
+ bathroom: 4,
8
+ other: 0
9
+ }
10
+
11
+ belongs_to :seat_section, class_name: 'SpreeCmCommissioner::SeatSection', optional: true
12
+ belongs_to :seat_layout, class_name: 'SpreeCmCommissioner::SeatLayout', optional: false
13
+
14
+ validates :label, presence: true, if: :label_required?
15
+
16
+ has_many :variant_blocks, class_name: 'SpreeCmCommissioner::VariantBlock', dependent: :destroy
17
+ has_many :variants, class_name: 'Spree::Variant', through: :variant_blocks
18
+
19
+ validates :width, presence: true, numericality: { greater_than: 0 }
20
+ validates :height, presence: true, numericality: { greater_than: 0 }
21
+ validates :x, presence: true, numericality: true
22
+ validates :y, presence: true, numericality: true
23
+ validates :rotation, presence: true, numericality: { greater_than_or_equal_to: -360, less_than_or_equal_to: 360 }
24
+
25
+ before_validation :assign_layout_from_section, if: -> { seat_layout.nil? && seat_section.present? }
26
+
27
+ def label_required?
28
+ seatable? || block_type.to_s == 'other'
29
+ end
30
+
31
+ def self.seatable?(block_type)
32
+ block_type.to_s.in?(%w[sleeping_seat seat])
33
+ end
34
+
35
+ def seatable?
36
+ self.class.seatable?(block_type)
37
+ end
38
+
39
+ def assign_layout_from_section
40
+ self.seat_layout = seat_section.seat_layout
41
+ end
42
+ end
43
+ end
@@ -24,10 +24,17 @@ module SpreeCmCommissioner
24
24
  scope :none_bib, -> { where(bib_prefix: [nil, '']) }
25
25
 
26
26
  belongs_to :line_item, class_name: 'Spree::LineItem'
27
+
27
28
  has_one :variant, class_name: 'Spree::Variant', through: :line_item
29
+
28
30
  belongs_to :user, class_name: 'Spree::User'
29
31
  belongs_to :occupation, class_name: 'Spree::Taxon'
30
32
  belongs_to :nationality, class_name: 'Spree::Taxon'
33
+ belongs_to :block, class_name: 'SpreeCmCommissioner::Block', optional: true
34
+ belongs_to :saved_guest, class_name: 'SpreeCmCommissioner::SavedGuest', optional: true
35
+
36
+ has_one :reserved_block, class_name: 'SpreeCmCommissioner::ReservedBlock'
37
+
31
38
  scope :checked_ins, -> { joins(:check_in) }
32
39
  scope :no_show, -> { left_outer_joins(:check_in).where(cm_check_ins: { id: nil }) }
33
40
 
@@ -48,9 +55,15 @@ module SpreeCmCommissioner
48
55
 
49
56
  before_create :generate_bib, if: -> { line_item.reload && variant.bib_pre_generation_on_create? }
50
57
 
58
+ after_create :preload_order_block_ids, if: -> { block_id.present? }
59
+ after_update :preload_order_block_ids, if: :saved_change_to_block_id?
60
+ after_destroy :preload_order_block_ids
61
+
51
62
  validates :seat_number, uniqueness: { scope: :event_id }, allow_nil: true, if: -> { event_id.present? }
52
63
  validates :bib_index, uniqueness: true, allow_nil: true
53
64
 
65
+ validate :validate_block, if: :should_validate_block?
66
+
54
67
  self.whitelisted_ransackable_associations = %w[id_card event line_item]
55
68
  self.whitelisted_ransackable_attributes = %w[first_name last_name phone_number gender contact occupation_id card_type created_at check_in_status]
56
69
 
@@ -283,6 +296,28 @@ module SpreeCmCommissioner
283
296
  filled_bib_number
284
297
  end
285
298
  end
299
+
300
+ # validate only if block_id is present AND (new record OR block_id is changing)
301
+ # goal: don't bother re-validating when block_id stays the same
302
+ def should_validate_block?
303
+ block_id.present? && (new_record? || will_save_change_to_block_id?)
304
+ end
305
+
306
+ def validate_block
307
+ return if block_id.blank?
308
+ return if line_item.blank? || line_item.variant.blank?
309
+ return if line_item.variant.variant_blocks.exists?(block_id: block_id)
310
+
311
+ errors.add(:block, "does not exist in variant #{line_item.variant_id}")
312
+ end
313
+
314
+ def preload_order_block_ids
315
+ return if line_item.blank?
316
+ return if line_item.order.blank?
317
+
318
+ block_ids = line_item.order.blocks.pluck(:id)
319
+ line_item.order.update(preload_block_ids: block_ids)
320
+ end
286
321
  end
287
322
  end
288
323
  # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ class Inventory
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor :variant_id, :inventory_date, :quantity_available, :max_capacity, :product_type
6
+
7
+ validates :variant_id, presence: true
8
+ validates :quantity_available, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
9
+ validates :max_capacity, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
10
+ end
11
+ end