spree_cm_commissioner 2.0.0 → 2.0.1.pre.pre

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 (215) 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 +32 -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/organizer/invite_guests_controller.rb +1 -1
  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/guests_controller.rb +13 -13
  13. data/app/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +2 -2
  14. data/app/factory/spree_cm_commissioner/invite_guest_claimed_telegram_message_factory.rb +81 -23
  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/create_ticket.rb +5 -6
  18. data/app/interactors/spree_cm_commissioner/ensure_correct_product_type.rb +40 -0
  19. data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +25 -0
  20. data/app/interactors/spree_cm_commissioner/pin_code_sender.rb +1 -0
  21. data/app/interactors/spree_cm_commissioner/sms.rb +1 -1
  22. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +44 -0
  23. data/app/interactors/spree_cm_commissioner/stock/inventory_items_adjuster.rb +13 -0
  24. data/app/interactors/spree_cm_commissioner/stock/inventory_items_generator.rb +15 -0
  25. data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +75 -0
  26. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +32 -0
  27. data/app/interactors/spree_cm_commissioner/user_id_token_authenticator.rb +3 -7
  28. data/app/interactors/spree_cm_commissioner/user_id_token_checker.rb +3 -11
  29. data/app/interactors/spree_cm_commissioner/user_identity_checker.rb +6 -12
  30. data/app/interactors/spree_cm_commissioner/user_registration_with_id_token.rb +1 -7
  31. data/app/jobs/spree_cm_commissioner/application_job.rb +20 -0
  32. data/app/jobs/spree_cm_commissioner/application_unique_job.rb +20 -0
  33. data/app/jobs/spree_cm_commissioner/ensure_correct_product_type_job.rb +7 -0
  34. data/app/jobs/spree_cm_commissioner/inventory_item_syncer_job.rb +7 -0
  35. data/app/jobs/spree_cm_commissioner/sms_pin_code_job.rb +1 -1
  36. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +11 -0
  37. data/app/jobs/spree_cm_commissioner/stock/inventory_items_generator_job.rb +11 -0
  38. data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +9 -0
  39. data/app/mailers/spree/order_mailer_decorator.rb +3 -18
  40. data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +9 -15
  41. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +1 -12
  42. data/app/models/concerns/spree_cm_commissioner/order_seatable.rb +44 -0
  43. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +39 -0
  44. data/app/models/concerns/spree_cm_commissioner/product_delegation.rb +1 -3
  45. data/app/models/concerns/spree_cm_commissioner/product_type.rb +10 -0
  46. data/app/models/concerns/spree_cm_commissioner/taxon_kind.rb +1 -1
  47. data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +0 -1
  48. data/app/models/spree_cm_commissioner/block.rb +23 -0
  49. data/app/models/spree_cm_commissioner/dynamic_field.rb +0 -20
  50. data/app/models/spree_cm_commissioner/guest.rb +18 -56
  51. data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +1 -41
  52. data/app/models/spree_cm_commissioner/inventory.rb +11 -0
  53. data/app/models/spree_cm_commissioner/inventory_item.rb +69 -0
  54. data/app/models/spree_cm_commissioner/line_item_decorator.rb +46 -78
  55. data/app/models/spree_cm_commissioner/notification_taxon.rb +1 -1
  56. data/app/models/spree_cm_commissioner/option_type_decorator.rb +1 -11
  57. data/app/models/spree_cm_commissioner/order_decorator.rb +30 -1
  58. data/app/models/spree_cm_commissioner/place.rb +1 -4
  59. data/app/models/spree_cm_commissioner/price_decorator.rb +9 -0
  60. data/app/models/spree_cm_commissioner/product_decorator.rb +10 -28
  61. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +41 -0
  62. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +126 -0
  63. data/app/models/spree_cm_commissioner/redis_stock/line_items_cached_inventory_items_builder.rb +36 -0
  64. data/app/models/spree_cm_commissioner/redis_stock/variant_cached_inventory_items_builder.rb +25 -0
  65. data/app/models/spree_cm_commissioner/reserved_block.rb +30 -0
  66. data/app/models/spree_cm_commissioner/seat_layout.rb +20 -0
  67. data/app/models/spree_cm_commissioner/seat_section.rb +16 -0
  68. data/app/models/spree_cm_commissioner/seats/blocks_canceler.rb +30 -0
  69. data/app/models/spree_cm_commissioner/seats/blocks_holder.rb +53 -0
  70. data/app/models/spree_cm_commissioner/seats/blocks_reserver.rb +49 -0
  71. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_on_hold_by_other_guest.rb +4 -0
  72. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_other_guest.rb +4 -0
  73. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_same_guest.rb +4 -0
  74. data/app/models/spree_cm_commissioner/seats/errors/unable_to_save_reserved_block_record.rb +4 -0
  75. data/app/models/spree_cm_commissioner/service_calendar.rb +0 -2
  76. data/app/models/spree_cm_commissioner/state_decorator.rb +0 -1
  77. data/app/models/spree_cm_commissioner/stock/availability_checker.rb +26 -25
  78. data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +2 -1
  79. data/app/models/spree_cm_commissioner/stock/line_item_availability_checker.rb +3 -3
  80. data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +44 -0
  81. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +17 -0
  82. data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -1
  83. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +0 -6
  84. data/app/models/spree_cm_commissioner/trip.rb +8 -10
  85. data/app/models/spree_cm_commissioner/trip_connection.rb +5 -5
  86. data/app/models/spree_cm_commissioner/trip_stop.rb +6 -25
  87. data/app/models/spree_cm_commissioner/user_identity_provider.rb +4 -26
  88. data/app/models/spree_cm_commissioner/variant_block.rb +9 -0
  89. data/app/models/spree_cm_commissioner/variant_decorator.rb +37 -47
  90. data/app/models/spree_cm_commissioner/variant_options.rb +0 -23
  91. data/app/models/spree_cm_commissioner/vehicle.rb +9 -14
  92. data/app/models/spree_cm_commissioner/vendor_decorator.rb +11 -17
  93. data/app/models/spree_cm_commissioner/vendor_place.rb +9 -3
  94. data/app/queries/spree_cm_commissioner/trip_query.rb +70 -44
  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/variant_request_schema.rb +19 -0
  98. data/app/serializers/spree/v2/storefront/accommodation_serializer.rb +2 -0
  99. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +1 -0
  100. data/app/serializers/spree/v2/tenant/user_serializer.rb +0 -1
  101. data/app/serializers/spree_cm_commissioner/v2/storefront/dynamic_field_serializer.rb +1 -1
  102. data/app/services/spree_cm_commissioner/user_authenticator.rb +1 -1
  103. data/app/views/spree/admin/inventory_items/show.html.erb +72 -0
  104. data/app/views/spree/admin/stock_managements/_variant_stock_items.html.erb +7 -2
  105. data/app/views/spree/admin/stock_managements/calendar.html.erb +56 -0
  106. data/app/views/spree/admin/stock_managements/index.html.erb +55 -6
  107. data/app/views/spree/admin/tenant_vendors/index.html.erb +2 -9
  108. data/app/views/spree/admin/tenants/_form.html.erb +0 -9
  109. data/app/views/spree/admin/tenants/edit.html.erb +1 -2
  110. data/app/views/spree/admin/tenants/index.html.erb +1 -7
  111. data/app/views/spree/admin/vendors/_form.html.erb +0 -14
  112. data/app/views/spree/order_mailer/confirm_email.html.erb +16 -27
  113. data/app/views/spree_cm_commissioner/layouts/order_mailer.html.erb +1 -5
  114. data/app/views/spree_cm_commissioner/order_mailer/_mailer_stylesheets.html.erb +4 -41
  115. data/config/initializers/paper_trail.rb +1 -0
  116. data/config/initializers/spree_permitted_attributes.rb +5 -0
  117. data/config/locales/en.yml +1 -5
  118. data/config/routes.rb +21 -2
  119. data/db/migrate/20240202080634_update_counter_cache_of_vehicle_type.rb +3 -1
  120. data/db/migrate/20250304293518_create_cm_inventory_items.rb +21 -0
  121. data/db/migrate/20250429094228_add_lock_version_to_cm_inventory_items.rb +5 -0
  122. data/db/migrate/20250502025848_add_index_to_spree_products.rb +5 -0
  123. data/db/migrate/20250502030001_add_product_type_to_spree_variants.rb +5 -0
  124. data/db/migrate/20250502030002_add_product_type_to_spree_line_items.rb +5 -0
  125. data/db/migrate/20250603035256_add_inventory_item_to_spree_prices.rb +7 -0
  126. data/db/migrate/20250619073724_drop_table_cm_line_item_seats.rb +5 -0
  127. data/db/migrate/20250619073812_drop_table_cm_vehicle_seats.rb +5 -0
  128. data/db/migrate/20250619073844_drop_table_cm_vehicle_types.rb +9 -0
  129. data/db/migrate/20250619073957_drop_table_cm_option_value_vehicle_types.rb +5 -0
  130. data/db/migrate/20250619082354_remove_unnecessary_fields_from_cm_places.rb +9 -0
  131. data/db/migrate/20250619082736_remove_route_type_from_spree_products.rb +5 -0
  132. data/db/migrate/20250619083055_remove_unnecessary_fields_from_spree_taxons.rb +5 -0
  133. data/db/migrate/20250620083055_remove_variant_id_from_cm_trips.rb +5 -0
  134. data/db/migrate/20250620090000_update_cm_trip_connections_to_use_cm_trips.rb +6 -0
  135. data/db/migrate/20250620090001_create_cm_seat_layouts.rb +17 -0
  136. data/db/migrate/20250620090002_create_cm_seat_sections.rb +18 -0
  137. data/db/migrate/20250620090003_create_cm_blocks.rb +18 -0
  138. data/db/migrate/20250624091005_create_cm_reserved_blocks.rb +29 -0
  139. data/db/migrate/20250626083642_create_cm_variant_blocks.rb +24 -0
  140. data/db/migrate/20250627023314_add_block_id_to_cm_guests.rb +13 -0
  141. data/db/migrate/20250716022821_add_location_reference_to_cm_vendor_places.rb +5 -0
  142. data/db/migrate/20250716031743_drop_table_cm_vendor_stops.rb +5 -0
  143. data/db/migrate/20250717023824_add_vendor_reference_to_cm_trips.rb +5 -0
  144. data/db/migrate/20250717041414_add_location_place_reference_to_cm_trip_stops.rb +5 -0
  145. data/db/migrate/20250717042539_rename_cm_trip_stops_stop_id_column_to_stop_place_id.rb +7 -0
  146. data/db/migrate/20250717042707_rename_cm_trips_origin_and_destination_to_origin_place_and_destination_place.rb +11 -0
  147. data/docker-compose.yml +1 -1
  148. data/lib/cm_app_logger.rb +11 -4
  149. data/lib/generators/spree_cm_commissioner/install/install_generator.rb +14 -11
  150. data/lib/generators/spree_cm_commissioner/install/templates/app/javascript/{spree_cm_commissioner → spree_dashboard/spree_cm_commissioner}/utilities.js +4 -0
  151. data/lib/spree_cm_commissioner/cached_inventory_item.rb +23 -0
  152. data/lib/spree_cm_commissioner/calendar_event.rb +11 -1
  153. data/lib/spree_cm_commissioner/test_helper/factories/block_factory.rb +15 -0
  154. data/lib/spree_cm_commissioner/test_helper/factories/guest_factory.rb +10 -0
  155. data/lib/spree_cm_commissioner/test_helper/factories/homepage_section_relatable_factory.rb +1 -1
  156. data/lib/spree_cm_commissioner/test_helper/factories/inventory_item_factory.rb +9 -0
  157. data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +1 -1
  158. data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +6 -30
  159. data/lib/spree_cm_commissioner/test_helper/factories/order_factory.rb +0 -36
  160. data/lib/spree_cm_commissioner/test_helper/factories/product_factory.rb +18 -34
  161. data/lib/spree_cm_commissioner/test_helper/factories/reserved_block_factory.rb +27 -0
  162. data/lib/spree_cm_commissioner/test_helper/factories/seat_layout_factory.rb +11 -0
  163. data/lib/spree_cm_commissioner/test_helper/factories/seat_section_factory.rb +16 -0
  164. data/lib/spree_cm_commissioner/test_helper/factories/stock_location_factory.rb +2 -2
  165. data/lib/spree_cm_commissioner/test_helper/factories/trip_connection_factory.rb +6 -0
  166. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +11 -3
  167. data/lib/spree_cm_commissioner/test_helper/factories/trip_stop_factory.rb +10 -0
  168. data/lib/spree_cm_commissioner/test_helper/factories/user_identity_provider_factory.rb +1 -1
  169. data/lib/spree_cm_commissioner/test_helper/factories/variant_block_factory.rb +7 -0
  170. data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +41 -19
  171. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_factory.rb +1 -1
  172. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +6 -1
  173. data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +13 -1
  174. data/lib/spree_cm_commissioner/trip_query_result.rb +8 -0
  175. data/lib/spree_cm_commissioner/trip_result.rb +5 -3
  176. data/lib/spree_cm_commissioner/version.rb +1 -1
  177. data/lib/spree_cm_commissioner.rb +35 -1
  178. data/lib/tasks/create_default_non_permanent_inventory_items.rake +16 -0
  179. data/lib/tasks/ensure_correct_product_type.rake +7 -0
  180. data/lib/tasks/generate_inventory_items.rake +7 -0
  181. data/spree_cm_commissioner.gemspec +7 -0
  182. metadata +141 -38
  183. data/app/assets/images/mailer/mail.png +0 -0
  184. data/app/assets/images/mailer/tenant_phone.png +0 -0
  185. data/app/assets/images/mailer/tenant_user.png +0 -0
  186. data/app/controllers/concerns/spree_cm_commissioner/transit/taxon_bitwise.rb +0 -44
  187. data/app/finders/spree_cm_commissioner/line_items/find_by_variant_decorator.rb +0 -20
  188. data/app/models/spree_cm_commissioner/branch.rb +0 -12
  189. data/app/models/spree_cm_commissioner/line_item_seat.rb +0 -10
  190. data/app/models/spree_cm_commissioner/option_value_vehicle_type.rb +0 -8
  191. data/app/models/spree_cm_commissioner/stop.rb +0 -23
  192. data/app/models/spree_cm_commissioner/vehicle_seat.rb +0 -11
  193. data/app/models/spree_cm_commissioner/vehicle_type.rb +0 -76
  194. data/app/models/spree_cm_commissioner/vendor_stop.rb +0 -10
  195. data/app/queries/spree_cm_commissioner/trip_search_query.rb +0 -76
  196. data/app/queries/spree_cm_commissioner/variant_availability/non_permanent_stock_query.rb +0 -45
  197. data/app/queries/spree_cm_commissioner/variant_availability/permanent_stock_query.rb +0 -55
  198. data/app/queries/spree_cm_commissioner/vendor_stop_place_query.rb +0 -54
  199. data/app/services/spree_cm_commissioner/vehicle_option_value_creator.rb +0 -11
  200. data/app/views/spree_cm_commissioner/order_mailer/tenant/_customer_info.html.erb +0 -42
  201. data/app/views/spree_cm_commissioner/order_mailer/tenant/_footer.html.erb +0 -25
  202. data/app/views/spree_cm_commissioner/order_mailer/tenant/_greeting.html.erb +0 -19
  203. data/app/views/spree_cm_commissioner/order_mailer/tenant/_support_contact.html.erb +0 -33
  204. data/db/migrate/20250709073455_add_email_fields_to_spree_vendors.rb +0 -6
  205. data/db/migrate/20250715103333_remove_indexes_from_cm_user_identity_providers.rb +0 -13
  206. data/db/migrate/20250718071620_add_data_fill_stage_to_dynamic_fields.rb +0 -5
  207. data/lib/spree_cm_commissioner/test_helper/factories/branch_factory.rb +0 -12
  208. data/lib/spree_cm_commissioner/test_helper/factories/departure_time_option_type_factory.rb +0 -8
  209. data/lib/spree_cm_commissioner/test_helper/factories/duration_option_type_factory.rb +0 -8
  210. data/lib/spree_cm_commissioner/test_helper/factories/line_item_seat_factory.rb +0 -7
  211. data/lib/spree_cm_commissioner/test_helper/factories/stop_factory.rb +0 -14
  212. data/lib/spree_cm_commissioner/test_helper/factories/transit_place_factory.rb +0 -8
  213. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_option_type_factory.rb +0 -8
  214. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_type_factory.rb +0 -96
  215. 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
@@ -1,6 +1,6 @@
1
1
  module SpreeCmCommissioner
2
2
  class SmsPinCodeJob < SpreeCmCommissioner::SmsJob
3
- # options = { to: xxxx, body: xxxx }
3
+ # options = { from: xxx, to: xxxx, body: xxxx }
4
4
  def perform(options)
5
5
  SpreeCmCommissioner::Sms.call(options)
6
6
  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
@@ -1,5 +1,6 @@
1
1
  module Spree
2
2
  module OrderMailerDecorator
3
+ # overrided
3
4
  def cancel_email(order, resend: false)
4
5
  @order = order.respond_to?(:id) ? order : Spree::Order.find(order)
5
6
  return false if @order.email.blank?
@@ -11,34 +12,18 @@ module Spree
11
12
  @order = order.respond_to?(:id) ? order : Spree::Order.find(order)
12
13
  return false if @order.email.blank?
13
14
 
14
- @tenant = @order.tenant
15
- if @tenant.present?
16
- @brand_color = @tenant.preferences[:brand_primary_color]
17
- @vendor_logo_url = @tenant.active_vendor&.logo&.original_url
18
- end
19
-
20
15
  @current_store = @order.store
21
16
  @product_type = @order.products.first&.product_type || 'accommodation'
22
17
 
23
- subject = resend ? "[#{Spree.t(:resend).upcase}] " : ''
18
+ subject = (resend ? "[#{Spree.t(:resend).upcase}] " : '')
24
19
  subject += "#{@current_store&.name} Booking Confirmation ##{@order.number}"
25
20
 
26
- mail(to: @order.email, from: from_email_address, subject: subject, store_url: @current_store.url) do |format|
21
+ mail(to: @order.email, from: from_address, subject: subject, store_url: @current_store.url) do |format|
27
22
  format.html { render layout: 'spree_cm_commissioner/layouts/order_mailer' }
28
23
  format.text
29
24
  end
30
25
  end
31
26
 
32
- private
33
-
34
- def from_email_address
35
- if @order.tenant.nil?
36
- from_address
37
- else
38
- @order.tenant.active_vendor&.from_email
39
- end
40
- end
41
-
42
27
  def ticket_email(guest, email)
43
28
  @guest = guest
44
29
  @event = @guest.event
@@ -18,29 +18,23 @@ 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
26
 
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
27
+ (from_date.to_date..to_date.to_date).to_a
30
28
  end
31
29
 
32
- def date_range_including_checkout
33
- return [] unless date_present?
34
-
35
- (from_date.to_date..to_date.to_date).to_a
30
+ def checkin_date
31
+ from_date&.to_date
36
32
  end
37
33
 
38
- def date_range
39
- if accommodation?
40
- date_range_excluding_checkout
41
- else
42
- date_range_including_checkout
43
- end
34
+ def checkout_date
35
+ return to_date ? to_date.to_date - 1.day : nil if accommodation?
36
+
37
+ to_date&.to_date
44
38
  end
45
39
 
46
40
  private
@@ -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,7 @@ 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'
56
45
  }.freeze
57
46
 
58
47
  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
@@ -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 }
7
7
  end
8
8
  end
9
9
  end
@@ -9,7 +9,6 @@ module SpreeCmCommissioner
9
9
  preference :payment_failed_image, :string, default: ''
10
10
  preference :payment_success_image, :string, default: ''
11
11
  preference :payment_loader, :string, default: ''
12
- preference :brand_primary_color, :string, default: ''
13
12
  end
14
13
  end
15
14
  end
@@ -0,0 +1,23 @@
1
+ module SpreeCmCommissioner
2
+ class Block < Base
3
+ belongs_to :seat_section, class_name: 'SpreeCmCommissioner::SeatSection', optional: true
4
+ belongs_to :seat_layout, class_name: 'SpreeCmCommissioner::SeatLayout', optional: false
5
+
6
+ validates :label, presence: true
7
+
8
+ has_many :variant_blocks, class_name: 'SpreeCmCommissioner::VariantBlock', dependent: :destroy
9
+ has_many :variants, class_name: 'Spree::Variant', through: :variant_blocks
10
+
11
+ validates :width, presence: true, numericality: { greater_than: 0 }
12
+ validates :height, presence: true, numericality: { greater_than: 0 }
13
+ validates :x, presence: true, numericality: true
14
+ validates :y, presence: true, numericality: true
15
+ validates :rotation, presence: true, numericality: { greater_than_or_equal_to: -360, less_than_or_equal_to: 360 }
16
+
17
+ before_validation :assign_layout_from_section, if: -> { seat_layout.nil? && seat_section.present? }
18
+
19
+ def assign_layout_from_section
20
+ self.seat_layout = seat_section.seat_layout
21
+ end
22
+ end
23
+ end
@@ -9,14 +9,12 @@ module SpreeCmCommissioner
9
9
  has_many :product_dynamic_fields, class_name: 'SpreeCmCommissioner::ProductDynamicField', dependent: :destroy
10
10
 
11
11
  enum data_type: { text: 0, number: 1, boolean: 2, checkbox: 3, radio: 4, selection: 5, textarea: 6 }
12
- enum data_fill_stage: { pre_registration: 0, post_registration: 1, during_check_in: 2 }
13
12
 
14
13
  accepts_nested_attributes_for :dynamic_field_options, allow_destroy: true, reject_if: :all_blank
15
14
 
16
15
  validates :label, presence: true
17
16
  validates :data_type, presence: true
18
17
  validates :configurations, presence: true, allow_blank: false
19
- validate :validate_data_fill_stage_phase
20
18
 
21
19
  def requires_dynamic_field_options?
22
20
  checkbox? || radio? || selection?
@@ -32,23 +30,5 @@ module SpreeCmCommissioner
32
30
  self.configurations ||= {}
33
31
  self.configurations['multiple_select'] = ActiveModel::Type::Boolean.new.cast(value)
34
32
  end
35
-
36
- def required_at_stage?(stage)
37
- data_fill_stage == stage.to_s
38
- end
39
-
40
- def display_label
41
- label + " (#{data_fill_stage.humanize})"
42
- end
43
-
44
- private
45
-
46
- def validate_data_fill_stage_phase
47
- return if data_fill_stage.blank?
48
-
49
- return if data_fill_stage.in?(self.class.data_fill_stages.keys)
50
-
51
- errors.add(:data_fill_stage, "must be one of: #{self.class.data_fill_stages.keys.join(', ')}")
52
- end
53
33
  end
54
34
  end
@@ -24,10 +24,16 @@ 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
+
35
+ has_one :reserved_block, class_name: 'SpreeCmCommissioner::ReservedBlock'
36
+
31
37
  scope :checked_ins, -> { joins(:check_in) }
32
38
  scope :no_show, -> { left_outer_joins(:check_in).where(cm_check_ins: { id: nil }) }
33
39
 
@@ -48,6 +54,10 @@ module SpreeCmCommissioner
48
54
 
49
55
  before_create :generate_bib, if: -> { line_item.reload && variant.bib_pre_generation_on_create? }
50
56
 
57
+ after_create :preload_order_block_ids, if: -> { block_id.present? }
58
+ after_update :preload_order_block_ids, if: :saved_change_to_block_id?
59
+ after_destroy :preload_order_block_ids
60
+
51
61
  validates :seat_number, uniqueness: { scope: :event_id }, allow_nil: true, if: -> { event_id.present? }
52
62
  validates :bib_index, uniqueness: true, allow_nil: true
53
63
 
@@ -213,62 +223,6 @@ module SpreeCmCommissioner
213
223
  save!
214
224
  end
215
225
 
216
- def missing_dynamic_fields_for_stage(stage)
217
- required_fields = line_item.product.dynamic_fields.where(data_fill_stage: stage)
218
- filled_ids = guest_dynamic_fields.map(&:dynamic_field_id)
219
-
220
- required_fields.reject { |field| filled_ids.include?(field.id) }
221
- end
222
-
223
- def all_required_dynamic_fields_completed?(stage)
224
- missing_dynamic_fields_for_stage(stage).blank?
225
- end
226
-
227
- def validate_dynamic_fields_for_phase(phase)
228
- required_fields = line_item.product.dynamic_fields.where(data_fill_stage: phase)
229
- filled_ids = guest_dynamic_fields.map(&:dynamic_field_id)
230
-
231
- required_fields.each do |field|
232
- errors.add(:base, "#{field.label} is required for #{phase} phase") unless filled_ids.include?(field.id)
233
- end
234
- end
235
-
236
- def pre_registration_completed?
237
- return true unless line_item.product.dynamic_fields.pre_registration.any?
238
-
239
- pre_registration_fields_completed?
240
- end
241
-
242
- def post_registration_completed?
243
- return true unless line_item.product.dynamic_fields.post_registration.any?
244
-
245
- post_registration_fields_completed?
246
- end
247
-
248
- def check_in_completed?
249
- return true unless line_item.product.dynamic_fields.during_check_in.any?
250
-
251
- check_in_fields_completed?
252
- end
253
-
254
- def current_collection_phase
255
- if check_in.present?
256
- :during_check_in
257
- elsif line_item.order.completed?
258
- :post_registration
259
- else
260
- :pre_registration
261
- end
262
- end
263
-
264
- def phase_completion_status
265
- {
266
- pre_registration: pre_registration_completed?,
267
- post_registration: post_registration_completed?,
268
- during_check_in: check_in_completed?
269
- }
270
- end
271
-
272
226
  # bib_number: 345, bib_prefix: 5KM, bib_zerofill: 5 => return 5KM00345
273
227
  # bib_number: 345, bib_prefix: 5KM, bib_zerofill: 2 => return 5KM345
274
228
  def formatted_bib_number
@@ -283,6 +237,14 @@ module SpreeCmCommissioner
283
237
  filled_bib_number
284
238
  end
285
239
  end
240
+
241
+ def preload_order_block_ids
242
+ return if line_item.blank?
243
+ return if line_item.order.blank?
244
+
245
+ block_ids = line_item.order.blocks.pluck(:id)
246
+ line_item.order.update(preload_block_ids: block_ids)
247
+ end
286
248
  end
287
249
  end
288
250
  # rubocop:enable Metrics/ClassLength
@@ -7,7 +7,6 @@ module SpreeCmCommissioner
7
7
  validates :value, presence: true
8
8
  validate :validate_value_format, if: -> { value.present? && dynamic_field.present? }
9
9
  validate :validate_option_reference, if: -> { dynamic_field_option.present? }
10
- validate :check_required_fields_based_on_phase
11
10
 
12
11
  private
13
12
 
@@ -35,49 +34,10 @@ module SpreeCmCommissioner
35
34
 
36
35
  def validate_option_reference
37
36
  return if dynamic_field.blank? || dynamic_field_option.blank?
37
+
38
38
  return if dynamic_field_option.dynamic_field_id == dynamic_field_id
39
39
 
40
40
  errors.add(:dynamic_field_option, 'must belong to the same dynamic field')
41
41
  end
42
-
43
- def before_checkin_phase?
44
- guest.present? && guest.line_item.present? && guest.check_in.nil?
45
- end
46
-
47
- def after_purchase_phase?
48
- guest.present? && guest.line_item.present? && guest.line_item.completed? && guest.check_in.nil?
49
- end
50
-
51
- def during_checkin_phase?
52
- guest.present? && guest.check_in.present?
53
- end
54
-
55
- def pre_registration_required?
56
- dynamic_field.data_fill_stage.to_sym == :pre_registration &&
57
- guest.line_item.order.completed? && guest.check_in.blank?
58
- end
59
-
60
- def post_registration_required?
61
- dynamic_field.data_fill_stage.to_sym == :post_registration &&
62
- guest.line_item.order.completed? && guest.check_in.blank?
63
- end
64
-
65
- def during_check_in_required?
66
- dynamic_field.data_fill_stage.to_sym == :during_check_in &&
67
- guest.check_in.present?
68
- end
69
-
70
- def check_required_fields_based_on_phase
71
- return if dynamic_field.blank? || value.present?
72
-
73
- case dynamic_field.data_fill_stage.to_sym
74
- when :pre_registration
75
- errors.add(:value, I18n.t('guest_dynamic_field.errors.pre_registration_required')) if pre_registration_required?
76
- when :post_registration
77
- errors.add(:value, I18n.t('guest_dynamic_field.errors.post_registration_required')) if post_registration_required?
78
- when :during_check_in
79
- errors.add(:value, I18n.t('guest_dynamic_field.errors.during_check_in_required')) if during_check_in_required?
80
- end
81
- end
82
42
  end
83
43
  end
@@ -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
@@ -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