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,69 @@
1
+ module SpreeCmCommissioner
2
+ class InventoryItem < Base
3
+ include SpreeCmCommissioner::ProductType
4
+
5
+ # Association
6
+ belongs_to :variant, class_name: 'Spree::Variant', optional: false
7
+ has_many :prices, class_name: 'Spree::Price', inverse_of: :inventory_item
8
+
9
+ # Validation
10
+ validates :quantity_available, numericality: { greater_than_or_equal_to: 0 }
11
+ validates :max_capacity, numericality: { greater_than_or_equal_to: 0 } # Originally inventory of each variant.
12
+ validates :inventory_date, presence: true, if: :permanent_stock?
13
+ validates :variant_id, uniqueness: { scope: :inventory_date, message: -> (object, _data) { "The variant is taken on #{object.inventory_date}" } }
14
+
15
+ # Scope
16
+ scope :active, -> { where(inventory_date: nil).or(where('inventory_date >= ?', Time.zone.today)) }
17
+
18
+ before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
19
+
20
+ # This method is only used when admin update stock
21
+ def adjust_quantity!(quantity)
22
+ with_lock do
23
+ self.max_capacity = [max_capacity + quantity, 0].max
24
+ self.quantity_available = [quantity_available + quantity, 0].max
25
+ save!
26
+
27
+ # When user has been searched or booked a product, it has cached the quantity in redis,
28
+ # So we need to update redis cache if inventory key has been created in redis
29
+ adjust_quantity_in_redis(quantity)
30
+ end
31
+ end
32
+
33
+ def redis_key
34
+ "inventory:#{id}"
35
+ end
36
+
37
+ def adjust_quantity_in_redis(quantity)
38
+ SpreeCmCommissioner.redis_pool.with do |redis|
39
+ cached_quantity_available = redis.get(redis_key)
40
+ # ignore if redis doesn't exist
41
+ return if cached_quantity_available.nil? # rubocop:disable Lint/NonLocalExitFromIterator
42
+
43
+ script = <<~LUA
44
+ local key = KEYS[1]
45
+ local increment = tonumber(ARGV[1])
46
+ local current = tonumber(redis.call('GET', key) or 0)
47
+ local new_value = current + increment
48
+ if new_value < 0 then
49
+ new_value = 0
50
+ end
51
+ redis.call('SET', key, new_value)
52
+ return new_value
53
+ LUA
54
+
55
+ redis.eval(script, keys: [redis_key], argv: [quantity])
56
+ end
57
+ end
58
+
59
+ def active?
60
+ inventory_date.nil? || inventory_date >= Time.zone.today
61
+ end
62
+
63
+ def redis_expired_in
64
+ expired_in = 31_536_000 # 1 year for normal stock
65
+ expired_in = Time.parse(inventory_date.to_s).end_of_day.to_i - Time.zone.now.to_i if inventory_date.present?
66
+ [expired_in, 0].max
67
+ end
68
+ end
69
+ end
@@ -1,6 +1,6 @@
1
1
  module SpreeCmCommissioner
2
2
  module LineItemDecorator
3
- def self.prepended(base) # rubocop:disable Metrics/MethodLength
3
+ def self.prepended(base) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
4
4
  include_modules(base)
5
5
 
6
6
  base.belongs_to :accepter, class_name: 'Spree::User', optional: true
@@ -10,31 +10,32 @@ module SpreeCmCommissioner
10
10
  base.has_one :google_wallet, class_name: 'SpreeCmCommissioner::GoogleWallet', through: :product
11
11
 
12
12
  base.has_many :option_types, through: :product
13
+
14
+ base.has_many :inventory_items, lambda { |line_item|
15
+ where(inventory_date: nil).or(where(inventory_date: line_item.date_range))
16
+ }, through: :variant
17
+
13
18
  base.has_many :taxons, class_name: 'Spree::Taxon', through: :product
14
19
  base.has_many :guests, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
20
+ base.has_many :guests_with_blocks, -> { where.not(block_id: nil) }, class_name: 'SpreeCmCommissioner::Guest'
21
+ base.has_many :reserved_blocks, through: :guests, class_name: 'SpreeCmCommissioner::ReservedBlock'
15
22
  base.has_many :pending_guests, pending_guests_query, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
16
23
  base.has_many :product_completion_steps, class_name: 'SpreeCmCommissioner::ProductCompletionStep', through: :product
17
- base.has_many :line_item_seats, class_name: 'SpreeCmCommissioner::LineItemSeat', dependent: :destroy
18
24
 
19
25
  base.before_save :update_vendor_id
20
- base.before_save :update_quantity, if: :transit?
21
-
22
- base.validate :validate_seats_reservation, if: :transit?
23
26
 
24
27
  base.before_create :add_due_date, if: :subscription?
28
+ base.before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
25
29
 
26
30
  base.validate :ensure_not_exceed_max_quantity_per_order, if: -> { variant&.max_quantity_per_order.present? }
27
31
 
28
32
  base.whitelisted_ransackable_associations |= %w[guests order]
29
33
  base.whitelisted_ransackable_attributes |= %w[number to_date from_date vendor_id]
30
34
 
31
- base.delegate :delivery_required?, :permanent_stock?, :high_demand, :transit?,
35
+ base.delegate :delivery_required?, :high_demand,
32
36
  to: :variant
33
- base.delegate :discontinue_on, :product_type, :accommodation?, :service?, :ecommerce?, :need_confirmation,
34
- to: :product
35
37
 
36
38
  base.accepts_nested_attributes_for :guests, allow_destroy: true
37
- base.accepts_nested_attributes_for :line_item_seats, allow_destroy: true
38
39
 
39
40
  def base.json_api_columns
40
41
  json_api_columns = column_names.reject { |c| c.match(/_id$|id|preferences|(.*)password|(.*)token|(.*)api_key/) }
@@ -42,6 +43,10 @@ module SpreeCmCommissioner
42
43
  json_api_columns << :vendor_id
43
44
  end
44
45
 
46
+ def discontinue_on
47
+ variant.discontinue_on || product.discontinue_on
48
+ end
49
+
45
50
  def base.search_by_qr_data!(data)
46
51
  matches = data.match(/(R\d+)-([A-Za-z0-9_\-]+)-(L\d+)/)&.captures
47
52
 
@@ -60,6 +65,8 @@ module SpreeCmCommissioner
60
65
  base.include SpreeCmCommissioner::LineItemDurationable
61
66
  base.include SpreeCmCommissioner::LineItemsFilterScope
62
67
  base.include SpreeCmCommissioner::LineItemGuestsConcern
68
+ base.include SpreeCmCommissioner::LineItemTransitable
69
+ base.include SpreeCmCommissioner::ProductType
63
70
  base.include SpreeCmCommissioner::ProductDelegation
64
71
  base.include SpreeCmCommissioner::KycBitwise
65
72
  end
@@ -75,24 +82,39 @@ module SpreeCmCommissioner
75
82
  }
76
83
  end
77
84
 
78
- def reservation?
79
- date_present? && !subscription?
80
- end
85
+ # override to calculate price per date for permanent stock
86
+ def update_price_from_modifier(currency, opts)
87
+ self.currency = currency
81
88
 
82
- # date_unit could be number of nights, or days or hours depending on usecases
83
- # For example:
84
- # - accomodation uses number of nights.
85
- # - appointment uses number of hours, etc.
86
- #
87
- # override
88
- def amount
89
- base_price = price * quantity
89
+ # these fields can be null during this process below.
90
+ # it is needed for permanent_stock?, date_range to work.
91
+ self.product_type = variant.product_type
92
+ self.from_date = opts.delete(:from_date)&.to_datetime
93
+ self.to_date = opts.delete(:to_date)&.to_datetime
90
94
 
91
- if permanent_stock? && date_unit.present?
92
- base_price * date_unit
93
- else
94
- base_price
95
- end
95
+ base_price = variant.price_in(currency)
96
+
97
+ self.price = if permanent_stock?
98
+ date_range.map do |date|
99
+ selected_price = variant.price_for_date(date: date, currency: currency) || base_price
100
+ (selected_price.amount || 0) + variant.price_modifier_amount_in(currency, opts.merge(date: date))
101
+ end.sum
102
+ else
103
+ (base_price.amount || 0) + variant.price_modifier_amount_in(currency, opts)
104
+ end
105
+ end
106
+
107
+ # override to calculate price per date for permanent stock
108
+ def update_price
109
+ base_price = variant.price_in(order.currency)
110
+ self.price = if permanent_stock?
111
+ date_range.map do |date|
112
+ selected_price = variant.price_for_date(date: date, currency: order.currency) || base_price
113
+ selected_price.amount || 0
114
+ end.sum
115
+ else
116
+ base_price.amount || 0
117
+ end
96
118
  end
97
119
 
98
120
  def allowed_self_check_in?
@@ -179,8 +201,6 @@ module SpreeCmCommissioner
179
201
 
180
202
  # override
181
203
  def sufficient_stock?
182
- return transit_sufficient_stock? if variant.product.product_type == 'transit'
183
-
184
204
  SpreeCmCommissioner::Stock::LineItemAvailabilityChecker.new(self).can_supply?(quantity)
185
205
  end
186
206
 
@@ -199,18 +219,6 @@ module SpreeCmCommissioner
199
219
  errors.add(:quantity, 'exceeded_max_quantity_per_order') if quantity > variant.max_quantity_per_order
200
220
  end
201
221
 
202
- def transit_sufficient_stock?
203
- return selected_seats_available? if reservation_trip.allow_seat_selection
204
-
205
- seat_quantity_available?(reservation_trip)
206
- end
207
-
208
- def update_quantity
209
- return if line_item_seats.blank?
210
-
211
- self.quantity = line_item_seats.size
212
- end
213
-
214
222
  def update_vendor_id
215
223
  self.vendor_id = variant.vendor_id
216
224
  end
@@ -233,45 +241,6 @@ module SpreeCmCommissioner
233
241
 
234
242
  from_date + variant.month.month + day.days
235
243
  end
236
-
237
- def validate_seats_reservation
238
- if reservation_trip.allow_seat_selection && !selected_seats_available?
239
- errors.add(:base, :some_seats_are_booked, message: 'Some seats are already booked')
240
- elsif !reservation_trip.allow_seat_selection && !seat_quantity_available?(reservation_trip)
241
- errors.add(:quantity, :exceeded_available_quantity, message: 'exceeded available quantity')
242
- end
243
- end
244
-
245
- def selected_seats_available?
246
- selected_seat_ids = line_item_seats.map(&:seat_id)
247
- !selected_seat_ids_occupied?(selected_seat_ids)
248
- end
249
-
250
- def seat_quantity_available?(trip)
251
- booked_quantity = Spree::LineItem.joins(:order)
252
- .where(variant_id: variant_id, date: date, spree_orders: { state: 'complete' })
253
- .where.not(spree_line_items: { id: id })
254
- .sum(:quantity)
255
- remaining_quantity = trip.vehicle.number_of_seats - booked_quantity
256
- remaining_quantity >= quantity
257
- end
258
-
259
- def reservation_trip
260
- return @trip if defined? @trip
261
-
262
- route = Spree::Variant.find_by(id: variant_id).product
263
- @trip = route.trip
264
- end
265
-
266
- def selected_seat_ids_occupied?(selected_seat_ids)
267
- # check to see if there are any selected_ids exist in the line_item_seats and belongs to completed order
268
- SpreeCmCommissioner::LineItemSeat.joins(line_item: :order)
269
- .where(seat_id: selected_seat_ids, date: date, variant_id: variant_id, spree_orders: { state: 'complete',
270
- canceled_at: nil
271
- }
272
- )
273
- .present?
274
- end
275
244
  end
276
245
  end
277
246
 
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- class NotificationTaxon < SpreeCmCommissioner::Base
2
+ class NotificationTaxon < Base
3
3
  belongs_to :customer_notification, class_name: 'SpreeCmCommissioner::CustomerNotification'
4
4
  belongs_to :taxon, class_name: 'Spree::Taxon'
5
5
  end
@@ -6,7 +6,7 @@ module SpreeCmCommissioner
6
6
  base.include SpreeCmCommissioner::ParameterizeName
7
7
  base.include SpreeCmCommissioner::OptionTypeAttrType
8
8
 
9
- base.enum kind: %i[variant product vendor vehicle_type transit]
9
+ base.enum kind: %i[variant product vendor vehicle]
10
10
 
11
11
  base.validates :name, presence: true
12
12
 
@@ -19,16 +19,6 @@ module SpreeCmCommissioner
19
19
  base.scope :promoted, -> { where(promoted: true) }
20
20
  base.whitelisted_ransackable_attributes = %w[kind]
21
21
 
22
- def base.amenities
23
- Spree::OptionType.where(kind: 'vehicle_type', name: 'amenities', presentation: 'Amenities', attr_type: 'amenity').first_or_create
24
- end
25
-
26
- def base.vehicle
27
- Spree::OptionType.where(presentation: 'vehicle', attr_type: 'vehicle_id', kind: 'variant',
28
- name: 'vehicle'
29
- ).first_or_create
30
- end
31
-
32
22
  def base.rules_option_type
33
23
  Spree::OptionType.find_by(name: RULES_OPTION_TYPE_NAME)
34
24
  end
@@ -5,30 +5,48 @@
5
5
  module SpreeCmCommissioner
6
6
  module Order
7
7
  module AddressBookDecorator
8
- def guest_order? = user.blank?
9
-
10
8
  # override
11
9
  def bill_address_attributes=(attributes)
12
- attributes[:id] = bill_address&.id if attributes[:id].blank?
13
- return if guest_order? && !basic_info_included?(attributes)
10
+ attributes[:id] = bill_address_id if attributes[:id].blank?
11
+ attributes[:phone] = intel_phone_number if intel_phone_number.present?
14
12
 
15
- super
13
+ self.bill_address = assign_address_attributes(attributes)
14
+ user.bill_address = bill_address if user && user.bill_address.nil?
16
15
  end
17
16
 
18
17
  # override
19
18
  def ship_address_attributes=(attributes)
20
- attributes[:id] = bill_address&.id if attributes[:id].blank?
21
- return if guest_order? && !basic_info_included?(attributes)
19
+ attributes[:id] = ship_address_id if attributes[:id].blank?
20
+ attributes[:phone] = intel_phone_number if intel_phone_number.present?
22
21
 
23
- super
22
+ self.ship_address = assign_address_attributes(attributes)
23
+ user.ship_address = ship_address if user && user.ship_address.nil?
24
24
  end
25
25
 
26
- private
26
+ def assign_address_attributes(attributes = {})
27
+ return if attributes.blank?
28
+
29
+ attributes.transform_values! { |v| v == '' ? nil : v }
30
+ attributes = attributes.to_h.symbolize_keys
27
31
 
28
- def basic_info_included?(attributes)
29
- return true if attributes[:id].present?
32
+ existing_address = attributes[:id].present? ? find_existing_address(attributes[:id]) : nil
33
+
34
+ if existing_address&.editable?
35
+ existing_address.assign_attributes(attributes)
36
+ existing_address
37
+ else
38
+ ::Spree::Address.new(attributes.except(:id, :updated_at, :created_at))
39
+ end
40
+ end
30
41
 
31
- %i[firstname lastname phone].all? { |key| attributes[key].present? }
42
+ def find_existing_address(id)
43
+ if user.present?
44
+ ::Spree::Address.where('(user_id = ? OR user_id IS NULL) AND deleted_at IS NULL', user.id)
45
+ .find_by(id: id)
46
+ else
47
+ ::Spree::Address.where(user_id: nil, deleted_at: nil)
48
+ .find_by(id: id)
49
+ end
32
50
  end
33
51
  end
34
52
  end
@@ -2,6 +2,7 @@ module SpreeCmCommissioner
2
2
  module OrderDecorator
3
3
  def self.prepended(base) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
4
4
  base.include SpreeCmCommissioner::PhoneNumberSanitizer
5
+ base.include SpreeCmCommissioner::OrderSeatable
5
6
  base.include SpreeCmCommissioner::OrderStateMachine
6
7
 
7
8
  base.scope :subscription, -> { where.not(subscription_id: nil) }
@@ -42,6 +43,14 @@ module SpreeCmCommissioner
42
43
  base.has_many :vendors, through: :products, class_name: 'Spree::Vendor'
43
44
  base.has_many :taxons, through: :products, class_name: 'Spree::Taxon'
44
45
  base.has_many :guests, through: :line_items, class_name: 'SpreeCmCommissioner::Guest'
46
+
47
+ base.has_many :saved_guests, -> { distinct.reorder('cm_saved_guests.created_at DESC') },
48
+ through: :guests,
49
+ source: :saved_guest,
50
+ class_name: 'SpreeCmCommissioner::SavedGuest'
51
+
52
+ base.has_many :blocks, through: :guests, class_name: 'SpreeCmCommissioner::Block', source: :block
53
+ base.has_many :reserved_blocks, through: :guests, class_name: 'SpreeCmCommissioner::ReservedBlock'
45
54
  base.has_many :guest_card_classes, class_name: 'SpreeCmCommissioner::GuestCardClass', through: :variants
46
55
 
47
56
  base.delegate :customer, to: :user, allow_nil: true
@@ -49,6 +58,8 @@ module SpreeCmCommissioner
49
58
  base.whitelisted_ransackable_associations |= %w[customer taxon payments guests invoice]
50
59
  base.whitelisted_ransackable_attributes |= %w[intel_phone_number phone_number email number state]
51
60
 
61
+ base.accepts_nested_attributes_for :saved_guests, allow_destroy: true
62
+
52
63
  def base.search_by_qr_data!(data)
53
64
  token = data.match(/^R\d{9,}-([A-Za-z0-9_\-]+)$/)&.captures
54
65
 
@@ -58,6 +69,13 @@ module SpreeCmCommissioner
58
69
  end
59
70
  end
60
71
 
72
+ # override
73
+ # spree use this method to check stock availability & consider whether :order can continue to next state.
74
+ def insufficient_stock_lines
75
+ checker = SpreeCmCommissioner::Stock::OrderAvailabilityChecker.new(self)
76
+ checker.insufficient_stock_lines
77
+ end
78
+
61
79
  def ticket_seller_user?
62
80
  return false if user.nil?
63
81
 
@@ -106,7 +124,14 @@ module SpreeCmCommissioner
106
124
  end
107
125
 
108
126
  def mark_as_archive
109
- update(archived_at: Time.current)
127
+ blocks_canceled = begin
128
+ cancel_blocks!
129
+ true
130
+ rescue StandardError
131
+ false
132
+ end
133
+
134
+ update(archived_at: Time.current) if blocks_canceled
110
135
  end
111
136
 
112
137
  # overrided
@@ -199,6 +224,18 @@ module SpreeCmCommissioner
199
224
 
200
225
  private
201
226
 
227
+ def unstock_inventory_in_redis!
228
+ CmAppLogger.log(label: "#{self.class.name}#unstock_inventory_in_redis!", data: { order_id: id }) do
229
+ SpreeCmCommissioner::RedisStock::InventoryUpdater.new(line_item_ids).unstock!
230
+ end
231
+ end
232
+
233
+ def restock_inventory_in_redis!
234
+ CmAppLogger.log(label: "#{self.class.name}#restock_inventory_in_redis!", data: { order_id: id }) do
235
+ SpreeCmCommissioner::RedisStock::InventoryUpdater.new(line_item_ids).restock!
236
+ end
237
+ end
238
+
202
239
  # override :spree_api
203
240
  def webhook_payload_body
204
241
  resource_serializer.new(
@@ -1,7 +1,5 @@
1
- require_dependency 'spree_cm_commissioner'
2
-
3
1
  module SpreeCmCommissioner
4
- class Place < ApplicationRecord
2
+ class Place < Base
5
3
  acts_as_nested_set
6
4
 
7
5
  validates :reference, presence: true, if: :validate_reference?
@@ -16,8 +14,9 @@ module SpreeCmCommissioner
16
14
  has_many :product_places, class_name: 'SpreeCmCommissioner::ProductPlace', dependent: :destroy
17
15
  has_many :products, through: :product_places
18
16
 
17
+ has_many :location_vendor_places, -> { where(place_type: :location) }, class_name: 'SpreeCmCommissioner::VendorPlace'
18
+
19
19
  has_many :children, -> { order(:lft) }, class_name: 'SpreeCmCommissioner::Place', foreign_key: :parent_id, dependent: :destroy
20
- has_many :vendor_stops, class_name: 'SpreeCmCommissioner::VendorStop', dependent: :destroy, foreign_key: :stop_id
21
20
 
22
21
  has_many :taxon_places, class_name: 'SpreeCmCommissioner::TaxonPlace', dependent: :destroy
23
22
  has_many :taxons, through: :taxon_places
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ module PriceDecorator
3
+ def self.prepended(base)
4
+ base.belongs_to :inventory_item, class_name: 'SpreeCmCommissioner::InventoryItem', inverse_of: :prices, optional: true
5
+ end
6
+ end
7
+ end
8
+
9
+ Spree::Price.prepend(SpreeCmCommissioner::PriceDecorator) unless Spree::Price.included_modules.include?(SpreeCmCommissioner::PriceDecorator)
@@ -6,7 +6,6 @@ module SpreeCmCommissioner
6
6
  base.include SpreeCmCommissioner::KycBitwise
7
7
  base.include SpreeCmCommissioner::Metafield
8
8
  base.include SpreeCmCommissioner::TenantUpdatable
9
- base.include SpreeCmCommissioner::RouteType
10
9
 
11
10
  base.has_many :variant_kind_option_types, -> { where(kind: :variant).order(:position) },
12
11
  through: :product_option_types, source: :option_type
@@ -33,6 +32,7 @@ module SpreeCmCommissioner
33
32
  base.has_one :google_wallet, class_name: 'SpreeCmCommissioner::GoogleWallet', dependent: :destroy
34
33
 
35
34
  base.has_many :complete_line_items, through: :classifications, source: :line_items
35
+ base.has_many :inventory_items, through: :variants
36
36
  base.has_many :guests, through: :line_items
37
37
 
38
38
  base.has_many :product_places, class_name: 'SpreeCmCommissioner::ProductPlace', dependent: :destroy
@@ -48,6 +48,9 @@ module SpreeCmCommissioner
48
48
 
49
49
  base.belongs_to :event, class_name: 'Spree::Taxon', optional: true
50
50
 
51
+ base.has_many :industry_classifications, -> { joins(:taxon).where(spree_taxons: { kind: :industry }) }, class_name: 'Spree::Classification'
52
+ base.has_many :industry_taxons, through: :industry_classifications, source: :taxon
53
+
51
54
  base.scope :min_price, lambda { |vendor|
52
55
  joins(:prices_including_master)
53
56
  .where(vendor_id: vendor.id, product_type: vendor.primary_product_type)
@@ -64,19 +67,26 @@ module SpreeCmCommissioner
64
67
  base.before_validation :set_event_id
65
68
 
66
69
  base.validate :validate_event_taxons, if: -> { taxons.event.present? }
67
-
68
70
  base.validate :validate_product_date, if: -> { available_on.present? && discontinue_on.present? }
69
-
71
+ base.validate :product_type_unchanged, on: :update
70
72
  base.validates :commission_rate, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true
71
73
 
72
- base.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status vendor_id short_name route_type]
74
+ base.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status vendor_id short_name]
73
75
 
74
76
  base.after_update :update_variants_vendor_id, if: :saved_change_to_vendor_id?
75
77
  base.after_update :sync_event_id_to_children, if: :saved_change_to_event_id?
76
78
 
77
79
  base.enum purchasable_on: { both: 0, web: 1, app: 2 }
78
-
79
- base.accepts_nested_attributes_for :trip, allow_destroy: true
80
+ base.enum call_to_action: { buy_now: 0,
81
+ book_now: 1,
82
+ reserve: 2,
83
+ register: 3,
84
+ apply: 4,
85
+ get_ticket: 5,
86
+ secure_your_seat: 6,
87
+ join_the_event: 7,
88
+ request_to_book: 8
89
+ }
80
90
 
81
91
  base.multi_tenant :tenant, class_name: 'SpreeCmCommissioner::Tenant'
82
92
  base.before_save :set_tenant
@@ -86,6 +96,22 @@ module SpreeCmCommissioner
86
96
  "#{Spree::Store.default.formatted_url}/tickets/#{slug}"
87
97
  end
88
98
 
99
+ def action_button
100
+ return :request_to_book if need_confirmation?
101
+ return :reserve if free?
102
+ return call_to_action.to_sym if call_to_action.present?
103
+
104
+ :buy_now
105
+ end
106
+
107
+ def free?
108
+ return false if prices_including_master.empty?
109
+
110
+ prices_including_master.all? do |master_price|
111
+ master_price.price.zero? && (master_price.compare_at_price.nil? || master_price.compare_at_price.zero?)
112
+ end
113
+ end
114
+
89
115
  def dynamic_fields_by_collection_phase
90
116
  {
91
117
  pre_registration: dynamic_fields.pre_registration.order(:position),
@@ -136,6 +162,13 @@ module SpreeCmCommissioner
136
162
 
137
163
  errors.add(:discontinue_on, 'must be after the available on date')
138
164
  end
165
+
166
+ def product_type_unchanged
167
+ return if product_type_was.nil?
168
+ return unless product_type_changed?
169
+
170
+ errors.add(:product_type, 'cannot be changed once set')
171
+ end
139
172
  end
140
173
  end
141
174
 
@@ -0,0 +1,41 @@
1
+ module SpreeCmCommissioner
2
+ module RedisStock
3
+ class CachedInventoryItemsBuilder
4
+ attr_reader :inventory_items
5
+
6
+ def initialize(inventory_items)
7
+ @inventory_items = inventory_items
8
+ end
9
+
10
+ # output: [ CachedInventoryItem(...), CachedInventoryItem(...) ]
11
+ def call
12
+ keys = inventory_items.map { |item| "inventory:#{item.id}" }
13
+ return [] unless keys.any?
14
+
15
+ counts = SpreeCmCommissioner.redis_pool.with { |redis| redis.mget(*keys) }
16
+ inventory_items.map.with_index do |inventory_item, i|
17
+ ::SpreeCmCommissioner::CachedInventoryItem.new(
18
+ inventory_key: keys[i],
19
+ active: inventory_item.active?,
20
+ quantity_available: cache_inventory(keys[i], inventory_item, counts[i]),
21
+ inventory_item_id: inventory_item.id,
22
+ variant_id: inventory_item.variant_id
23
+ )
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def cache_inventory(key, inventory_item, count_in_redis)
30
+ return count_in_redis.to_i if count_in_redis.present?
31
+ return inventory_item.quantity_available unless inventory_item.active?
32
+
33
+ SpreeCmCommissioner.redis_pool.with do |redis|
34
+ redis.set(key, inventory_item.quantity_available, ex: inventory_item.redis_expired_in)
35
+ end
36
+
37
+ inventory_item.quantity_available
38
+ end
39
+ end
40
+ end
41
+ end