spree_cm_commissioner 2.0.3.pre.pre4 → 2.0.3.pre.pre5

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 (202) 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/operator/event_charts_controller.rb +8 -2
  10. data/app/controllers/spree/api/v2/storefront/accommodations/variants_controller.rb +42 -0
  11. data/app/controllers/spree/api/v2/storefront/accommodations_controller.rb +14 -31
  12. data/app/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +2 -2
  13. data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +60 -0
  14. data/app/finders/spree_cm_commissioner/accommodations/find.rb +37 -0
  15. data/app/finders/spree_cm_commissioner/accommodations/find_variant.rb +32 -0
  16. data/app/interactors/spree_cm_commissioner/ensure_correct_product_type.rb +40 -0
  17. data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +25 -0
  18. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +44 -0
  19. data/app/interactors/spree_cm_commissioner/stock/inventory_items_adjuster.rb +13 -0
  20. data/app/interactors/spree_cm_commissioner/stock/inventory_items_generator.rb +15 -0
  21. data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +75 -0
  22. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +32 -0
  23. data/app/interactors/spree_cm_commissioner/taxi_trip_creator.rb +66 -0
  24. data/app/interactors/spree_cm_commissioner/transit/draft_order_creator.rb +119 -0
  25. data/app/interactors/spree_cm_commissioner/variant_block_updater.rb +62 -0
  26. data/app/interactors/spree_cm_commissioner/vehicle_updater.rb +41 -0
  27. data/app/jobs/spree_cm_commissioner/application_job.rb +20 -0
  28. data/app/jobs/spree_cm_commissioner/application_unique_job.rb +20 -0
  29. data/app/jobs/spree_cm_commissioner/ensure_correct_product_type_job.rb +7 -0
  30. data/app/jobs/spree_cm_commissioner/inventory_item_syncer_job.rb +7 -0
  31. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +11 -0
  32. data/app/jobs/spree_cm_commissioner/stock/inventory_items_generator_job.rb +11 -0
  33. data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +9 -0
  34. data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +18 -15
  35. data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +29 -15
  36. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +89 -0
  37. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +2 -12
  38. data/app/models/concerns/spree_cm_commissioner/order_seatable.rb +44 -0
  39. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +39 -0
  40. data/app/models/concerns/spree_cm_commissioner/product_delegation.rb +1 -3
  41. data/app/models/concerns/spree_cm_commissioner/product_type.rb +10 -0
  42. data/app/models/concerns/spree_cm_commissioner/route_type.rb +1 -1
  43. data/app/models/concerns/spree_cm_commissioner/taxon_kind.rb +1 -1
  44. data/app/models/concerns/spree_cm_commissioner/vehicle_type.rb +11 -0
  45. data/app/models/spree_cm_commissioner/address_decorator.rb +22 -2
  46. data/app/models/spree_cm_commissioner/block.rb +43 -0
  47. data/app/models/spree_cm_commissioner/guest.rb +35 -0
  48. data/app/models/spree_cm_commissioner/inventory.rb +11 -0
  49. data/app/models/spree_cm_commissioner/inventory_item.rb +69 -0
  50. data/app/models/spree_cm_commissioner/line_item_decorator.rb +47 -78
  51. data/app/models/spree_cm_commissioner/notification_taxon.rb +1 -1
  52. data/app/models/spree_cm_commissioner/option_type_decorator.rb +1 -11
  53. data/app/models/spree_cm_commissioner/order_decorator.rb +31 -1
  54. data/app/models/spree_cm_commissioner/place.rb +1 -4
  55. data/app/models/spree_cm_commissioner/price_decorator.rb +9 -0
  56. data/app/models/spree_cm_commissioner/product_decorator.rb +10 -6
  57. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +41 -0
  58. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +126 -0
  59. data/app/models/spree_cm_commissioner/redis_stock/line_items_cached_inventory_items_builder.rb +36 -0
  60. data/app/models/spree_cm_commissioner/redis_stock/variant_cached_inventory_items_builder.rb +25 -0
  61. data/app/models/spree_cm_commissioner/reserved_block.rb +30 -0
  62. data/app/models/spree_cm_commissioner/saved_guest.rb +13 -0
  63. data/app/models/spree_cm_commissioner/seat_layout.rb +20 -0
  64. data/app/models/spree_cm_commissioner/seat_section.rb +16 -0
  65. data/app/models/spree_cm_commissioner/seats/blocks_canceler.rb +30 -0
  66. data/app/models/spree_cm_commissioner/seats/blocks_holder.rb +53 -0
  67. data/app/models/spree_cm_commissioner/seats/blocks_reserver.rb +49 -0
  68. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_on_hold_by_other_guest.rb +4 -0
  69. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_other_guest.rb +4 -0
  70. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_same_guest.rb +4 -0
  71. data/app/models/spree_cm_commissioner/seats/errors/unable_to_save_reserved_block_record.rb +4 -0
  72. data/app/models/spree_cm_commissioner/service_calendar.rb +0 -2
  73. data/app/models/spree_cm_commissioner/state_decorator.rb +0 -1
  74. data/app/models/spree_cm_commissioner/stock/availability_checker.rb +26 -25
  75. data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +2 -1
  76. data/app/models/spree_cm_commissioner/stock/line_item_availability_checker.rb +3 -3
  77. data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +44 -0
  78. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +17 -0
  79. data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -1
  80. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +0 -6
  81. data/app/models/spree_cm_commissioner/trip.rb +17 -10
  82. data/app/models/spree_cm_commissioner/trip_connection.rb +5 -5
  83. data/app/models/spree_cm_commissioner/trip_stop.rb +10 -21
  84. data/app/models/spree_cm_commissioner/variant_block.rb +17 -0
  85. data/app/models/spree_cm_commissioner/variant_decorator.rb +37 -47
  86. data/app/models/spree_cm_commissioner/variant_options.rb +0 -23
  87. data/app/models/spree_cm_commissioner/vehicle.rb +11 -16
  88. data/app/models/spree_cm_commissioner/vendor_decorator.rb +11 -14
  89. data/app/models/spree_cm_commissioner/vendor_place.rb +13 -3
  90. data/app/queries/spree_cm_commissioner/trip_query.rb +110 -131
  91. data/app/request_schemas/spree_cm_commissioner/accommodation_request_schema.rb +3 -0
  92. data/app/request_schemas/spree_cm_commissioner/application_request_schema.rb +1 -1
  93. data/app/request_schemas/spree_cm_commissioner/trip_search_request_schema.rb +20 -0
  94. data/app/request_schemas/spree_cm_commissioner/variant_request_schema.rb +19 -0
  95. data/app/serializers/spree/v2/storefront/accommodation_serializer.rb +2 -0
  96. data/app/serializers/spree/v2/storefront/amenity_serializer.rb +15 -0
  97. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +1 -0
  98. data/app/serializers/spree/v2/storefront/trip_query_result_serializer.rb +14 -0
  99. data/app/serializers/spree/v2/storefront/trip_result_serializer.rb +16 -0
  100. data/app/serializers/spree/v2/storefront/trip_vehicle_serializer.rb +11 -0
  101. data/app/serializers/spree/v2/storefront/trip_vendor_serializer.rb +13 -0
  102. data/app/views/spree/admin/inventory_items/show.html.erb +72 -0
  103. data/app/views/spree/admin/stock_managements/_variant_stock_items.html.erb +7 -2
  104. data/app/views/spree/admin/stock_managements/calendar.html.erb +56 -0
  105. data/app/views/spree/admin/stock_managements/index.html.erb +55 -6
  106. data/config/initializers/paper_trail.rb +1 -0
  107. data/config/initializers/spree_permitted_attributes.rb +5 -0
  108. data/config/routes.rb +24 -2
  109. data/db/migrate/20240202080634_update_counter_cache_of_vehicle_type.rb +3 -1
  110. data/db/migrate/20250122000000_move_route_type_from_vehicles_to_trips.rb +7 -0
  111. data/db/migrate/20250304293518_create_cm_inventory_items.rb +21 -0
  112. data/db/migrate/20250429094228_add_lock_version_to_cm_inventory_items.rb +5 -0
  113. data/db/migrate/20250502025848_add_index_to_spree_products.rb +5 -0
  114. data/db/migrate/20250502030001_add_product_type_to_spree_variants.rb +5 -0
  115. data/db/migrate/20250502030002_add_product_type_to_spree_line_items.rb +5 -0
  116. data/db/migrate/20250603035256_add_inventory_item_to_spree_prices.rb +7 -0
  117. data/db/migrate/20250619073724_drop_table_cm_line_item_seats.rb +5 -0
  118. data/db/migrate/20250619073812_drop_table_cm_vehicle_seats.rb +5 -0
  119. data/db/migrate/20250619073844_drop_table_cm_vehicle_types.rb +9 -0
  120. data/db/migrate/20250619073957_drop_table_cm_option_value_vehicle_types.rb +5 -0
  121. data/db/migrate/20250619082354_remove_unnecessary_fields_from_cm_places.rb +9 -0
  122. data/db/migrate/20250619082736_remove_route_type_from_spree_products.rb +5 -0
  123. data/db/migrate/20250619083055_remove_unnecessary_fields_from_spree_taxons.rb +5 -0
  124. data/db/migrate/20250620083055_remove_variant_id_from_cm_trips.rb +5 -0
  125. data/db/migrate/20250620090000_update_cm_trip_connections_to_use_cm_trips.rb +6 -0
  126. data/db/migrate/20250620090001_create_cm_seat_layouts.rb +17 -0
  127. data/db/migrate/20250620090002_create_cm_seat_sections.rb +18 -0
  128. data/db/migrate/20250620090003_create_cm_blocks.rb +18 -0
  129. data/db/migrate/20250624091005_create_cm_reserved_blocks.rb +29 -0
  130. data/db/migrate/20250626083642_create_cm_variant_blocks.rb +24 -0
  131. data/db/migrate/20250627023314_add_block_id_to_cm_guests.rb +13 -0
  132. data/db/migrate/20250716022821_add_location_reference_to_cm_vendor_places.rb +5 -0
  133. data/db/migrate/20250716031743_drop_table_cm_vendor_stops.rb +5 -0
  134. data/db/migrate/20250717023824_add_vendor_reference_to_cm_trips.rb +5 -0
  135. data/db/migrate/20250717041414_add_location_place_reference_to_cm_trip_stops.rb +5 -0
  136. data/db/migrate/20250717042539_rename_cm_trip_stops_stop_id_column_to_stop_place_id.rb +7 -0
  137. data/db/migrate/20250717042707_rename_cm_trips_origin_and_destination_to_origin_place_and_destination_place.rb +11 -0
  138. data/db/migrate/20250721080738_add_vehicle_type_to_cm_vehicles.rb +7 -0
  139. data/db/migrate/20250725092713_add_block_type_to_cm_blocks.rb +5 -0
  140. data/db/migrate/20250731062816_add_departure_time_and_arrival_time_to_trip_stop.rb +6 -0
  141. data/db/migrate/20250807033035_create_spree_cm_commissioner_saved_guests.rb +23 -0
  142. data/db/migrate/20250808054835_add_saved_guest_reference_to_cm_blocks.rb +5 -0
  143. data/docker-compose.yml +1 -1
  144. data/lib/cm_app_logger.rb +11 -4
  145. data/lib/generators/spree_cm_commissioner/install/install_generator.rb +14 -11
  146. data/lib/generators/spree_cm_commissioner/install/templates/app/javascript/{spree_cm_commissioner → spree_dashboard/spree_cm_commissioner}/utilities.js +4 -0
  147. data/lib/spree_cm_commissioner/cached_inventory_item.rb +23 -0
  148. data/lib/spree_cm_commissioner/calendar_event.rb +11 -1
  149. data/lib/spree_cm_commissioner/test_helper/factories/block_factory.rb +16 -0
  150. data/lib/spree_cm_commissioner/test_helper/factories/guest_factory.rb +10 -0
  151. data/lib/spree_cm_commissioner/test_helper/factories/homepage_section_relatable_factory.rb +1 -1
  152. data/lib/spree_cm_commissioner/test_helper/factories/inventory_item_factory.rb +9 -0
  153. data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +1 -1
  154. data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +6 -30
  155. data/lib/spree_cm_commissioner/test_helper/factories/order_factory.rb +0 -36
  156. data/lib/spree_cm_commissioner/test_helper/factories/product_factory.rb +18 -34
  157. data/lib/spree_cm_commissioner/test_helper/factories/reserved_block_factory.rb +27 -0
  158. data/lib/spree_cm_commissioner/test_helper/factories/seat_layout_factory.rb +11 -0
  159. data/lib/spree_cm_commissioner/test_helper/factories/seat_section_factory.rb +16 -0
  160. data/lib/spree_cm_commissioner/test_helper/factories/stock_location_factory.rb +2 -2
  161. data/lib/spree_cm_commissioner/test_helper/factories/trip_connection_factory.rb +6 -0
  162. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +11 -3
  163. data/lib/spree_cm_commissioner/test_helper/factories/trip_stop_factory.rb +13 -0
  164. data/lib/spree_cm_commissioner/test_helper/factories/variant_block_factory.rb +7 -0
  165. data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +41 -19
  166. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_factory.rb +1 -1
  167. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +6 -1
  168. data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +13 -1
  169. data/lib/spree_cm_commissioner/transit/leg.rb +23 -0
  170. data/lib/spree_cm_commissioner/transit/seat_selection.rb +19 -0
  171. data/lib/spree_cm_commissioner/trip_query_result.rb +16 -0
  172. data/lib/spree_cm_commissioner/trip_result.rb +12 -10
  173. data/lib/spree_cm_commissioner/version.rb +1 -1
  174. data/lib/spree_cm_commissioner.rb +37 -1
  175. data/lib/tasks/create_default_non_permanent_inventory_items.rake +16 -0
  176. data/lib/tasks/ensure_correct_product_type.rake +7 -0
  177. data/lib/tasks/generate_inventory_items.rake +7 -0
  178. data/spree_cm_commissioner.gemspec +7 -0
  179. metadata +161 -26
  180. data/app/controllers/concerns/spree_cm_commissioner/transit/taxon_bitwise.rb +0 -44
  181. data/app/finders/spree_cm_commissioner/line_items/find_by_variant_decorator.rb +0 -20
  182. data/app/models/spree_cm_commissioner/branch.rb +0 -12
  183. data/app/models/spree_cm_commissioner/line_item_seat.rb +0 -10
  184. data/app/models/spree_cm_commissioner/option_value_vehicle_type.rb +0 -8
  185. data/app/models/spree_cm_commissioner/stop.rb +0 -23
  186. data/app/models/spree_cm_commissioner/vehicle_seat.rb +0 -11
  187. data/app/models/spree_cm_commissioner/vehicle_type.rb +0 -76
  188. data/app/models/spree_cm_commissioner/vendor_stop.rb +0 -10
  189. data/app/queries/spree_cm_commissioner/trip_search_query.rb +0 -76
  190. data/app/queries/spree_cm_commissioner/variant_availability/non_permanent_stock_query.rb +0 -45
  191. data/app/queries/spree_cm_commissioner/variant_availability/permanent_stock_query.rb +0 -55
  192. data/app/queries/spree_cm_commissioner/vendor_stop_place_query.rb +0 -54
  193. data/app/services/spree_cm_commissioner/vehicle_option_value_creator.rb +0 -11
  194. data/lib/spree_cm_commissioner/test_helper/factories/branch_factory.rb +0 -12
  195. data/lib/spree_cm_commissioner/test_helper/factories/departure_time_option_type_factory.rb +0 -8
  196. data/lib/spree_cm_commissioner/test_helper/factories/duration_option_type_factory.rb +0 -8
  197. data/lib/spree_cm_commissioner/test_helper/factories/line_item_seat_factory.rb +0 -7
  198. data/lib/spree_cm_commissioner/test_helper/factories/stop_factory.rb +0 -14
  199. data/lib/spree_cm_commissioner/test_helper/factories/transit_place_factory.rb +0 -8
  200. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_option_type_factory.rb +0 -8
  201. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_type_factory.rb +0 -96
  202. data/lib/spree_cm_commissioner/trip_seat_layout_result.rb +0 -11
@@ -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
@@ -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,9 @@ 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
+ base.has_many :saved_guests, -> { distinct }, through: :guests, class_name: 'SpreeCmCommissioner::SavedGuest'
47
+ base.has_many :blocks, through: :guests, class_name: 'SpreeCmCommissioner::Block', source: :block
48
+ base.has_many :reserved_blocks, through: :guests, class_name: 'SpreeCmCommissioner::ReservedBlock'
45
49
  base.has_many :guest_card_classes, class_name: 'SpreeCmCommissioner::GuestCardClass', through: :variants
46
50
 
47
51
  base.delegate :customer, to: :user, allow_nil: true
@@ -58,6 +62,13 @@ module SpreeCmCommissioner
58
62
  end
59
63
  end
60
64
 
65
+ # override
66
+ # spree use this method to check stock availability & consider whether :order can continue to next state.
67
+ def insufficient_stock_lines
68
+ checker = SpreeCmCommissioner::Stock::OrderAvailabilityChecker.new(self)
69
+ checker.insufficient_stock_lines
70
+ end
71
+
61
72
  def ticket_seller_user?
62
73
  return false if user.nil?
63
74
 
@@ -106,7 +117,14 @@ module SpreeCmCommissioner
106
117
  end
107
118
 
108
119
  def mark_as_archive
109
- update(archived_at: Time.current)
120
+ blocks_canceled = begin
121
+ cancel_blocks!
122
+ true
123
+ rescue StandardError
124
+ false
125
+ end
126
+
127
+ update(archived_at: Time.current) if blocks_canceled
110
128
  end
111
129
 
112
130
  # overrided
@@ -199,6 +217,18 @@ module SpreeCmCommissioner
199
217
 
200
218
  private
201
219
 
220
+ def unstock_inventory_in_redis!
221
+ CmAppLogger.log(label: "#{self.class.name}#unstock_inventory_in_redis!", data: { order_id: id }) do
222
+ SpreeCmCommissioner::RedisStock::InventoryUpdater.new(line_item_ids).unstock!
223
+ end
224
+ end
225
+
226
+ def restock_inventory_in_redis!
227
+ CmAppLogger.log(label: "#{self.class.name}#restock_inventory_in_redis!", data: { order_id: id }) do
228
+ SpreeCmCommissioner::RedisStock::InventoryUpdater.new(line_item_ids).restock!
229
+ end
230
+ end
231
+
202
232
  # override :spree_api
203
233
  def webhook_payload_body
204
234
  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?
@@ -17,7 +15,6 @@ module SpreeCmCommissioner
17
15
  has_many :products, through: :product_places
18
16
 
19
17
  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
18
 
22
19
  has_many :taxon_places, class_name: 'SpreeCmCommissioner::TaxonPlace', dependent: :destroy
23
20
  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
@@ -64,20 +64,17 @@ module SpreeCmCommissioner
64
64
  base.before_validation :set_event_id
65
65
 
66
66
  base.validate :validate_event_taxons, if: -> { taxons.event.present? }
67
-
68
67
  base.validate :validate_product_date, if: -> { available_on.present? && discontinue_on.present? }
69
-
68
+ base.validate :product_type_unchanged, on: :update
70
69
  base.validates :commission_rate, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true
71
70
 
72
- base.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status vendor_id short_name route_type]
71
+ base.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status vendor_id short_name]
73
72
 
74
73
  base.after_update :update_variants_vendor_id, if: :saved_change_to_vendor_id?
75
74
  base.after_update :sync_event_id_to_children, if: :saved_change_to_event_id?
76
75
 
77
76
  base.enum purchasable_on: { both: 0, web: 1, app: 2 }
78
77
 
79
- base.accepts_nested_attributes_for :trip, allow_destroy: true
80
-
81
78
  base.multi_tenant :tenant, class_name: 'SpreeCmCommissioner::Tenant'
82
79
  base.before_save :set_tenant
83
80
  end
@@ -136,6 +133,13 @@ module SpreeCmCommissioner
136
133
 
137
134
  errors.add(:discontinue_on, 'must be after the available on date')
138
135
  end
136
+
137
+ def product_type_unchanged
138
+ return if product_type_was.nil?
139
+ return unless product_type_changed?
140
+
141
+ errors.add(:product_type, 'cannot be changed once set')
142
+ end
139
143
  end
140
144
  end
141
145
 
@@ -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
@@ -0,0 +1,126 @@
1
+ module SpreeCmCommissioner
2
+ module RedisStock
3
+ class InventoryUpdater
4
+ class UnableToRestock < StandardError; end
5
+ class UnableToUnstock < StandardError; end
6
+
7
+ def initialize(line_item_ids)
8
+ @line_item_ids = line_item_ids
9
+ end
10
+
11
+ def unstock!
12
+ keys, quantities, inventory_ids = extract_inventory_data
13
+
14
+ raise UnableToUnstock, Spree.t(:insufficient_stock_lines_present) unless unstock(keys, quantities)
15
+
16
+ inventory_id_and_quantities = inventory_ids.map.with_index do |inventory_id, i|
17
+ { inventory_id: inventory_id, quantity: -quantities[i] }
18
+ end
19
+
20
+ schedule_sync_inventory(inventory_id_and_quantities)
21
+ end
22
+
23
+ def restock!
24
+ keys, quantities, inventory_ids = extract_inventory_data
25
+
26
+ raise UnableToRestock unless restock(keys, quantities)
27
+
28
+ inventory_id_and_quantities = inventory_ids.map.with_index do |inventory_id, i|
29
+ { inventory_id: inventory_id, quantity: quantities[i] }
30
+ end
31
+
32
+ schedule_sync_inventory(inventory_id_and_quantities)
33
+ end
34
+
35
+ private
36
+
37
+ # only unstock or restock when variants is available, track inventory, not backorderable & not need_confirmation.
38
+ # otherwise, we can ignore them.
39
+ def line_items
40
+ return @line_items if defined?(@line_items)
41
+
42
+ @line_items = Spree::LineItem.where(id: @line_item_ids).includes(variant: :stock_items)
43
+ @line_items = @line_items.map do |line_item|
44
+ next unless line_item.variant.available?
45
+ next unless line_item.variant.should_track_inventory?
46
+ next if line_item.variant.backorderable?
47
+ next if line_item.variant.need_confirmation?
48
+
49
+ line_item
50
+ end.compact
51
+ end
52
+
53
+ def unstock(keys, quantities)
54
+ SpreeCmCommissioner.redis_pool.with do |redis|
55
+ redis.eval(unstock_redis_script, keys: keys, argv: quantities)
56
+ end.positive?
57
+ end
58
+
59
+ def restock(keys, quantities)
60
+ SpreeCmCommissioner.redis_pool.with do |redis|
61
+ redis.eval(restock_redis_script, keys: keys, argv: quantities)
62
+ end.positive?
63
+ end
64
+
65
+ # Return: [CachedInventoryItem(...), CachedInventoryItem(...)]
66
+ def cached_inventory_items
67
+ @cached_inventory_items ||= SpreeCmCommissioner::RedisStock::LineItemsCachedInventoryItemsBuilder.new(line_item_ids: line_items.pluck(:id))
68
+ .call.values.flatten
69
+ end
70
+
71
+ def extract_inventory_data
72
+ keys = []
73
+ quantities = []
74
+ inventory_ids = []
75
+
76
+ cached_inventory_items.each do |cached_inventory_item|
77
+ keys << cached_inventory_item.inventory_key
78
+ quantities << line_items.find { |item| item.variant_id == cached_inventory_item.variant_id }.quantity
79
+ inventory_ids << cached_inventory_item.inventory_item_id
80
+ end
81
+
82
+ [keys, quantities, inventory_ids]
83
+ end
84
+
85
+ def unstock_redis_script
86
+ <<~LUA
87
+ local keys = KEYS
88
+ local quantities = ARGV
89
+
90
+ -- Check availability first
91
+ for i, key in ipairs(keys) do
92
+ local current = tonumber(redis.call('GET', key) or 0)
93
+ if current - tonumber(quantities[i]) < 0 then
94
+ return 0
95
+ end
96
+ end
97
+
98
+ -- Apply updates
99
+ for i, key in ipairs(keys) do
100
+ redis.call('DECRBY', key, tonumber(quantities[i]))
101
+ end
102
+
103
+ return 1
104
+ LUA
105
+ end
106
+
107
+ def restock_redis_script
108
+ <<~LUA
109
+ local keys = KEYS
110
+ local quantities = ARGV
111
+
112
+ -- Apply restock updates
113
+ for i, key in ipairs(keys) do
114
+ redis.call('INCRBY', key, tonumber(quantities[i]))
115
+ end
116
+
117
+ return 1
118
+ LUA
119
+ end
120
+
121
+ def schedule_sync_inventory(inventory_id_and_quantities)
122
+ SpreeCmCommissioner::InventoryItemSyncerJob.perform_later(inventory_id_and_quantities:)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,36 @@
1
+ module SpreeCmCommissioner
2
+ module RedisStock
3
+ class LineItemsCachedInventoryItemsBuilder
4
+ attr_reader :line_item_ids
5
+
6
+ def initialize(line_item_ids:)
7
+ @line_item_ids = line_item_ids
8
+ end
9
+
10
+ # return list of inventory items group by :line_item_id:
11
+ # {
12
+ # 1: [ CachedInventoryItem(...), CachedInventoryItem(...) ],
13
+ # 2: [ CachedInventoryItem(...), CachedInventoryItem(...) ],
14
+ # }
15
+ def call
16
+ cached_inventory_items.group_by do |cached_inventory_item|
17
+ line_item = line_items.find { |item| item.variant_id == cached_inventory_item.variant_id }
18
+ line_item.id
19
+ end
20
+ end
21
+
22
+ def cached_inventory_items
23
+ @cached_inventory_items ||= SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(inventory_items)
24
+ .call
25
+ end
26
+
27
+ def inventory_items
28
+ @inventory_items ||= line_items.flat_map(&:inventory_items)
29
+ end
30
+
31
+ def line_items
32
+ @line_items ||= Spree::LineItem.where(id: line_item_ids).includes(:inventory_items)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ module SpreeCmCommissioner
2
+ module RedisStock
3
+ class VariantCachedInventoryItemsBuilder
4
+ attr_reader :variant_id, :dates
5
+
6
+ def initialize(variant_id:, dates: [])
7
+ @variant_id = variant_id
8
+ @dates = dates
9
+ end
10
+
11
+ # output: [ CachedInventoryItem(...), CachedInventoryItem(...) ]
12
+ def call
13
+ ::SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(inventory_items).call
14
+ end
15
+
16
+ def inventory_items
17
+ variant = Spree::Variant.find(variant_id)
18
+
19
+ inventory_items = variant.inventory_items
20
+ inventory_items = inventory_items.where(inventory_date: dates) if variant.permanent_stock?
21
+ inventory_items
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ module SpreeCmCommissioner
2
+ class ReservedBlock < Base
3
+ has_paper_trail
4
+
5
+ belongs_to :updated_by, optional: true, class_name: 'Spree::User'
6
+ belongs_to :guest, optional: true, class_name: 'SpreeCmCommissioner::Guest'
7
+ belongs_to :inventory_item, class_name: 'SpreeCmCommissioner::InventoryItem'
8
+ belongs_to :block, class_name: 'SpreeCmCommissioner::Block'
9
+
10
+ enum status: {
11
+ reserved: 0, # confirmed reservation
12
+ on_hold: 1, # temporary hold
13
+ canceled: 2
14
+ }
15
+
16
+ validates :status, presence: true
17
+ validates :expired_at, presence: true, if: -> { on_hold? }
18
+
19
+ scope :active_on_hold, -> { on_hold.where('expired_at IS NULL OR expired_at > ?', Time.current) }
20
+ scope :active, -> { where('expired_at IS NULL OR expired_at > ?', Time.current) }
21
+
22
+ def expired?
23
+ expired_at.present? && expired_at <= Time.current
24
+ end
25
+
26
+ def active_on_hold?
27
+ on_hold? && !expired?
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ # Reusable guest profile, can be linked to a user or be nil for anonymous checkouts.
2
+ # Orders associate to saved_guest through guests. If an order later associates to a user, the saved_guest is assigned to that user.
3
+ module SpreeCmCommissioner
4
+ class SavedGuest < Base
5
+ enum gender: { :other => 0, :male => 1, :female => 2 }
6
+
7
+ belongs_to :user, class_name: 'Spree::User', optional: true
8
+ belongs_to :occupation, class_name: 'Spree::Taxon', optional: true
9
+ belongs_to :nationality, class_name: 'Spree::Taxon', optional: true
10
+
11
+ has_many :guests, class_name: 'SpreeCmCommissioner::Guest', dependent: :nullify
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module SpreeCmCommissioner
2
+ class SeatLayout < Base
3
+ has_many :seat_sections, class_name: 'SpreeCmCommissioner::SeatSection', dependent: :destroy
4
+
5
+ has_many :top_level_blocks, -> { where(seat_section_id: nil) }, class_name: 'SpreeCmCommissioner::Block', dependent: :destroy
6
+ has_many :section_blocks, -> { where.not(seat_section_id: nil) }, class_name: 'SpreeCmCommissioner::Block'
7
+
8
+ belongs_to :layoutable, polymorphic: true
9
+
10
+ enum status: {
11
+ inactive: 0,
12
+ active: 1,
13
+ archived: 2
14
+ }
15
+
16
+ validates :name, presence: true
17
+
18
+ accepts_nested_attributes_for :seat_sections, :top_level_blocks
19
+ end
20
+ end