spree_cm_commissioner 1.18.1 → 2.0.0.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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +0 -3
  3. data/.github/workflows/test_and_build_gem.yml +16 -2
  4. data/.gitignore +2 -1
  5. data/Gemfile.lock +28 -1
  6. data/Rakefile +33 -4
  7. data/app/assets/javascripts/spree_cm_commissioner/backend.js +0 -1
  8. data/app/assets/stylesheets/spree_cm_commissioner/backend/calendar.scss +8 -11
  9. data/app/assets/stylesheets/spree_cm_commissioner/backend/commissioner_admin.css.scss +0 -1
  10. data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +1 -2
  11. data/app/controllers/spree/admin/inventory_items_controller.rb +83 -0
  12. data/app/controllers/spree/admin/stock_managements_controller.rb +63 -1
  13. data/app/controllers/spree/admin/tenants_controller.rb +0 -8
  14. data/app/controllers/spree/api/v2/storefront/accommodations/variants_controller.rb +42 -0
  15. data/app/controllers/spree/api/v2/storefront/accommodations_controller.rb +14 -31
  16. data/app/controllers/spree/api/v2/storefront/guests_controller.rb +5 -31
  17. data/app/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +2 -2
  18. data/app/controllers/spree_cm_commissioner/admin/variants_controller_decorator.rb +1 -1
  19. data/app/controllers/spree_cm_commissioner/well_known_controller.rb +18 -20
  20. data/app/finders/spree_cm_commissioner/accommodations/find.rb +37 -0
  21. data/app/finders/spree_cm_commissioner/accommodations/find_variant.rb +32 -0
  22. data/app/interactors/spree_cm_commissioner/ensure_correct_product_type.rb +40 -0
  23. data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +25 -0
  24. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +44 -0
  25. data/app/interactors/spree_cm_commissioner/stock/inventory_items_adjuster.rb +13 -0
  26. data/app/interactors/spree_cm_commissioner/stock/inventory_items_generator.rb +15 -0
  27. data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +75 -0
  28. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +32 -0
  29. data/app/interactors/spree_cm_commissioner/vattanac_bank_initiator.rb +1 -5
  30. data/app/jobs/spree_cm_commissioner/application_job.rb +20 -0
  31. data/app/jobs/spree_cm_commissioner/application_unique_job.rb +20 -0
  32. data/app/jobs/spree_cm_commissioner/ensure_correct_product_type_job.rb +7 -0
  33. data/app/jobs/spree_cm_commissioner/inventory_item_syncer_job.rb +7 -0
  34. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +11 -0
  35. data/app/jobs/spree_cm_commissioner/stock/inventory_items_generator_job.rb +11 -0
  36. data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +9 -0
  37. data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +9 -15
  38. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +1 -13
  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/taxon_kind.rb +1 -1
  44. data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +0 -4
  45. data/app/models/spree_cm_commissioner/block.rb +23 -0
  46. data/app/models/spree_cm_commissioner/dynamic_field.rb +3 -15
  47. data/app/models/spree_cm_commissioner/dynamic_field_option.rb +1 -5
  48. data/app/models/spree_cm_commissioner/guest.rb +19 -1
  49. data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +3 -21
  50. data/app/models/spree_cm_commissioner/inventory.rb +11 -0
  51. data/app/models/spree_cm_commissioner/inventory_item.rb +69 -0
  52. data/app/models/spree_cm_commissioner/invite_team.rb +1 -3
  53. data/app/models/spree_cm_commissioner/line_item_decorator.rb +46 -78
  54. data/app/models/spree_cm_commissioner/notification_taxon.rb +1 -1
  55. data/app/models/spree_cm_commissioner/option_type_decorator.rb +1 -11
  56. data/app/models/spree_cm_commissioner/order_decorator.rb +30 -1
  57. data/app/models/spree_cm_commissioner/place.rb +1 -3
  58. data/app/models/spree_cm_commissioner/price_decorator.rb +9 -0
  59. data/app/models/spree_cm_commissioner/product_decorator.rb +10 -4
  60. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +41 -0
  61. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +126 -0
  62. data/app/models/spree_cm_commissioner/redis_stock/line_items_cached_inventory_items_builder.rb +36 -0
  63. data/app/models/spree_cm_commissioner/redis_stock/variant_cached_inventory_items_builder.rb +25 -0
  64. data/app/models/spree_cm_commissioner/reserved_block.rb +30 -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 -2
  82. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +0 -6
  83. data/app/models/spree_cm_commissioner/trip.rb +20 -6
  84. data/app/models/spree_cm_commissioner/trip_connection.rb +5 -5
  85. data/app/models/spree_cm_commissioner/trip_stop.rb +2 -3
  86. data/app/models/spree_cm_commissioner/user_taxon.rb +0 -1
  87. data/app/models/spree_cm_commissioner/variant_block.rb +9 -0
  88. data/app/models/spree_cm_commissioner/variant_decorator.rb +36 -47
  89. data/app/models/spree_cm_commissioner/variant_options.rb +0 -23
  90. data/app/models/spree_cm_commissioner/vehicle.rb +9 -14
  91. data/app/models/spree_cm_commissioner/vendor_decorator.rb +7 -9
  92. data/app/models/spree_cm_commissioner/vendor_place.rb +6 -5
  93. data/app/models/spree_cm_commissioner/vendor_stop.rb +1 -2
  94. data/app/overrides/spree/admin/variants/_form/kyc_field.html.erb.deface +2 -2
  95. data/app/queries/spree_cm_commissioner/guest_searcher_query.rb +3 -45
  96. data/app/queries/spree_cm_commissioner/trip_query.rb +23 -26
  97. data/app/request_schemas/spree_cm_commissioner/accommodation_request_schema.rb +3 -0
  98. data/app/request_schemas/spree_cm_commissioner/application_request_schema.rb +1 -1
  99. data/app/request_schemas/spree_cm_commissioner/variant_request_schema.rb +19 -0
  100. data/app/serializers/spree/v2/storefront/accommodation_serializer.rb +2 -0
  101. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +1 -0
  102. data/app/serializers/spree_cm_commissioner/v2/operator/dashboard_crew_event_serializer.rb +1 -4
  103. data/app/serializers/spree_cm_commissioner/v2/storefront/dynamic_field_option_serializer.rb +3 -1
  104. data/app/serializers/spree_cm_commissioner/v2/storefront/dynamic_field_serializer.rb +3 -1
  105. data/app/serializers/spree_cm_commissioner/v2/storefront/guest_dynamic_field_serializer.rb +1 -2
  106. data/app/services/spree_cm_commissioner/organizer/export_guest_csv_service.rb +15 -23
  107. data/app/views/spree/admin/inventory_items/show.html.erb +72 -0
  108. data/app/views/spree/admin/stock_managements/_variant_stock_items.html.erb +7 -2
  109. data/app/views/spree/admin/stock_managements/calendar.html.erb +56 -0
  110. data/app/views/spree/admin/stock_managements/index.html.erb +55 -6
  111. data/app/views/spree/admin/tenants/_form.html.erb +42 -109
  112. data/config/initializers/paper_trail.rb +1 -0
  113. data/config/initializers/spree_permitted_attributes.rb +5 -2
  114. data/config/locales/en.yml +0 -4
  115. data/config/routes.rb +22 -4
  116. data/db/migrate/20240202080634_update_counter_cache_of_vehicle_type.rb +3 -1
  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/docker-compose.yml +1 -1
  139. data/lib/cm_app_logger.rb +11 -4
  140. data/lib/generators/spree_cm_commissioner/install/install_generator.rb +14 -11
  141. data/lib/generators/spree_cm_commissioner/install/templates/app/javascript/{spree_cm_commissioner → spree_dashboard/spree_cm_commissioner}/utilities.js +4 -0
  142. data/lib/spree_cm_commissioner/cached_inventory_item.rb +23 -0
  143. data/lib/spree_cm_commissioner/calendar_event.rb +11 -1
  144. data/lib/spree_cm_commissioner/test_helper/factories/block_factory.rb +9 -0
  145. data/lib/spree_cm_commissioner/test_helper/factories/guest_factory.rb +10 -0
  146. data/lib/spree_cm_commissioner/test_helper/factories/homepage_section_relatable_factory.rb +1 -1
  147. data/lib/spree_cm_commissioner/test_helper/factories/inventory_item_factory.rb +9 -0
  148. data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +1 -1
  149. data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +6 -30
  150. data/lib/spree_cm_commissioner/test_helper/factories/order_factory.rb +0 -36
  151. data/lib/spree_cm_commissioner/test_helper/factories/product_factory.rb +18 -34
  152. data/lib/spree_cm_commissioner/test_helper/factories/reserved_block_factory.rb +27 -0
  153. data/lib/spree_cm_commissioner/test_helper/factories/seat_layout_factory.rb +8 -0
  154. data/lib/spree_cm_commissioner/test_helper/factories/seat_section_factory.rb +8 -0
  155. data/lib/spree_cm_commissioner/test_helper/factories/stock_location_factory.rb +2 -2
  156. data/lib/spree_cm_commissioner/test_helper/factories/trip_connection_factory.rb +6 -0
  157. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +10 -3
  158. data/lib/spree_cm_commissioner/test_helper/factories/variant_block_factory.rb +7 -0
  159. data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +41 -19
  160. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_factory.rb +1 -1
  161. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +1 -1
  162. data/lib/spree_cm_commissioner/version.rb +1 -1
  163. data/lib/spree_cm_commissioner.rb +35 -1
  164. data/lib/tasks/create_default_non_permanent_inventory_items.rake +16 -0
  165. data/lib/tasks/ensure_correct_product_type.rake +7 -0
  166. data/lib/tasks/generate_inventory_items.rake +7 -0
  167. data/spree_cm_commissioner.gemspec +7 -0
  168. metadata +134 -39
  169. data/app/assets/images/cm-hangmeas-checkout_image.svg +0 -63
  170. data/app/assets/images/cm-hangmeas-failed.svg +0 -56
  171. data/app/assets/images/cm-hangmeas-loader.svg +0 -50
  172. data/app/assets/images/cm-hangmeas-success.svg +0 -51
  173. data/app/assets/javascripts/spree_cm_commissioner/tenant_payment_icon_fields.js +0 -65
  174. data/app/assets/stylesheets/spree_cm_commissioner/backend/tenant_payment_icon_fields.scss +0 -60
  175. data/app/controllers/concerns/spree_cm_commissioner/transit/taxon_bitwise.rb +0 -44
  176. data/app/finders/spree_cm_commissioner/line_items/find_by_variant_decorator.rb +0 -20
  177. data/app/interactors/spree_cm_commissioner/universal_link_fetcher.rb +0 -54
  178. data/app/models/concerns/spree_cm_commissioner/event_check_in_flowable.rb +0 -30
  179. data/app/models/spree_cm_commissioner/branch.rb +0 -12
  180. data/app/models/spree_cm_commissioner/line_item_seat.rb +0 -10
  181. data/app/models/spree_cm_commissioner/option_value_vehicle_type.rb +0 -8
  182. data/app/models/spree_cm_commissioner/stop.rb +0 -23
  183. data/app/models/spree_cm_commissioner/vehicle_seat.rb +0 -11
  184. data/app/models/spree_cm_commissioner/vehicle_type.rb +0 -76
  185. data/app/overrides/spree/admin/taxons/_form/check_in_flows.html.erb.deface +0 -18
  186. data/app/queries/spree_cm_commissioner/trip_search_query.rb +0 -76
  187. data/app/queries/spree_cm_commissioner/variant_availability/non_permanent_stock_query.rb +0 -45
  188. data/app/queries/spree_cm_commissioner/variant_availability/permanent_stock_query.rb +0 -55
  189. data/app/services/spree_cm_commissioner/vehicle_option_value_creator.rb +0 -11
  190. data/db/migrate/20250616084219_add_description_to_cm_vendor_place.rb +0 -5
  191. data/db/migrate/20250701093203_add_configurations_to_cm_dynamic_field.rb +0 -6
  192. data/db/migrate/20250702091305_add_dynamic_field_option_to_guest_dynamic_field.rb +0 -5
  193. data/db/migrate/20250702091935_add_status_to_dynamic_field_option.rb +0 -5
  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
@@ -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
@@ -5,10 +5,6 @@ module SpreeCmCommissioner
5
5
  included do
6
6
  preference :assetlinks, :string, default: ''
7
7
  preference :apple_app_site_association, :string, default: ''
8
- preference :payment_checkout_image, :string, default: ''
9
- preference :payment_failed_image, :string, default: ''
10
- preference :payment_success_image, :string, default: ''
11
- preference :payment_loader, :string, default: ''
12
8
  end
13
9
  end
14
10
  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
@@ -6,27 +6,15 @@ module SpreeCmCommissioner
6
6
  has_many :guest_dynamic_fields, class_name: 'SpreeCmCommissioner::GuestDynamicField', dependent: :destroy
7
7
  has_many :product_dynamic_fields, class_name: 'SpreeCmCommissioner::ProductDynamicField', dependent: :destroy
8
8
 
9
- enum data_type: { text: 0, number: 1, boolean: 2, checkbox: 3, radio: 4, selection: 5, textarea: 6 }
9
+ enum data_type: { string: 0, integer: 1, boolean: 2, checkbox: 3, radio: 4 }
10
10
 
11
11
  accepts_nested_attributes_for :dynamic_field_options, allow_destroy: true, reject_if: :all_blank
12
12
 
13
13
  validates :label, presence: true
14
14
  validates :data_type, presence: true
15
- validates :configurations, presence: true, allow_blank: false
16
15
 
17
- def requires_dynamic_field_options?
18
- checkbox? || radio? || selection?
19
- end
20
-
21
- def multiple_select
22
- return nil unless selection?
23
-
24
- configurations['multiple_select'] == true
25
- end
26
-
27
- def multiple_select=(value)
28
- self.configurations ||= {}
29
- self.configurations['multiple_select'] = ActiveModel::Type::Boolean.new.cast(value)
16
+ def selection?
17
+ checkbox? || radio?
30
18
  end
31
19
  end
32
20
  end
@@ -1,12 +1,8 @@
1
1
  module SpreeCmCommissioner
2
2
  class DynamicFieldOption < SpreeCmCommissioner::Base
3
3
  belongs_to :dynamic_field, class_name: 'SpreeCmCommissioner::DynamicField', optional: false
4
- has_many :guest_dynamic_fields, class_name: 'SpreeCmCommissioner::GuestDynamicField', dependent: :restrict_with_error
5
-
6
- enum status: { active: 0, draft: 1, archived: 2 }
7
4
 
8
5
  validates :value, presence: true, uniqueness: { scope: :dynamic_field_id }
9
- validates :status, presence: true
10
6
  validate :parent_field_must_be_selection_type
11
7
 
12
8
  private
@@ -14,7 +10,7 @@ module SpreeCmCommissioner
14
10
  def parent_field_must_be_selection_type
15
11
  return unless dynamic_field
16
12
 
17
- return if dynamic_field.requires_dynamic_field_options?
13
+ return if dynamic_field.selection?
18
14
 
19
15
  errors.add(:dynamic_field, 'must be a selection type field')
20
16
  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,10 +54,14 @@ 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
 
54
- self.whitelisted_ransackable_associations = %w[id_card event line_item]
64
+ self.whitelisted_ransackable_associations = %w[id_card event]
55
65
  self.whitelisted_ransackable_attributes = %w[first_name last_name phone_number gender contact occupation_id card_type created_at check_in_status]
56
66
 
57
67
  def self.csv_importable_columns
@@ -227,6 +237,14 @@ module SpreeCmCommissioner
227
237
  filled_bib_number
228
238
  end
229
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
230
248
  end
231
249
  end
232
250
  # rubocop:enable Metrics/ClassLength
@@ -2,11 +2,9 @@ module SpreeCmCommissioner
2
2
  class GuestDynamicField < SpreeCmCommissioner::Base
3
3
  belongs_to :guest, class_name: 'SpreeCmCommissioner::Guest', optional: false
4
4
  belongs_to :dynamic_field, class_name: 'SpreeCmCommissioner::DynamicField', optional: false
5
- belongs_to :dynamic_field_option, class_name: 'SpreeCmCommissioner::DynamicFieldOption', optional: true
6
5
 
7
6
  validates :value, presence: true
8
7
  validate :validate_value_format, if: -> { value.present? && dynamic_field.present? }
9
- validate :validate_option_reference, if: -> { dynamic_field_option.present? }
10
8
 
11
9
  private
12
10
 
@@ -14,30 +12,14 @@ module SpreeCmCommissioner
14
12
  return if dynamic_field.blank?
15
13
 
16
14
  case dynamic_field.data_type.to_s
17
- when 'number'
15
+ when 'integer'
18
16
  errors.add(:value, 'must be a number') unless value.to_s.match?(/\A\d+\z/)
19
17
  when 'boolean'
20
- errors.add(:value, 'must be true or false') unless %w[true false].include?(value.to_s.downcase)
21
- when 'checkbox', 'radio', 'selection'
22
- validate_option_value
23
- end
24
- end
25
-
26
- def validate_option_value
27
- if dynamic_field_option.present?
28
- self.value ||= dynamic_field_option.value
29
- else
18
+ errors.add(:value, 'must be true or false') unless %w[true false 1 0].include?(value.to_s.downcase)
19
+ when 'checkbox', 'radio'
30
20
  valid_options = dynamic_field.dynamic_field_options.pluck(:id).map(&:to_s)
31
21
  errors.add(:value, 'is not a valid option') unless valid_options.include?(value)
32
22
  end
33
23
  end
34
-
35
- def validate_option_reference
36
- return if dynamic_field.blank? || dynamic_field_option.blank?
37
-
38
- return if dynamic_field_option.dynamic_field_id == dynamic_field_id
39
-
40
- errors.add(:dynamic_field_option, 'must belong to the same dynamic field')
41
- end
42
24
  end
43
25
  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
@@ -1,7 +1,5 @@
1
1
  module SpreeCmCommissioner
2
2
  class InviteTeam < SpreeCmCommissioner::Base
3
- validates :email, presence: true, uniqueness: { scope: :vendor_id, message: I18n.t('invite.already_invited') }
4
-
5
3
  belongs_to :user, class_name: 'Spree::User', optional: true
6
4
  belongs_to :inviter, class_name: 'Spree::User'
7
5
  belongs_to :vendor, class_name: 'Spree::Vendor'
@@ -36,7 +34,7 @@ module SpreeCmCommissioner
36
34
  end
37
35
 
38
36
  def invitation_link
39
- "#{ENV.fetch('ORGANIZER_URL')}/invite_teams/#{token}?utm_source=email"
37
+ "http://#{ENV.fetch('APP_HOST')}/organizer/invite_teams/#{token}?utm_source=email"
40
38
  end
41
39
 
42
40
  def url_valid?
@@ -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,7 @@ module SpreeCmCommissioner
60
65
  base.include SpreeCmCommissioner::LineItemDurationable
61
66
  base.include SpreeCmCommissioner::LineItemsFilterScope
62
67
  base.include SpreeCmCommissioner::LineItemGuestsConcern
68
+ base.include SpreeCmCommissioner::ProductType
63
69
  base.include SpreeCmCommissioner::ProductDelegation
64
70
  base.include SpreeCmCommissioner::KycBitwise
65
71
  end
@@ -75,24 +81,39 @@ module SpreeCmCommissioner
75
81
  }
76
82
  end
77
83
 
78
- def reservation?
79
- date_present? && !subscription?
80
- end
84
+ # override to calculate price per date for permanent stock
85
+ def update_price_from_modifier(currency, opts)
86
+ self.currency = currency
81
87
 
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
88
+ # these fields can be null during this process below.
89
+ # it is needed for permanent_stock?, date_range to work.
90
+ self.product_type = variant.product_type
91
+ self.from_date = opts.delete(:from_date)&.to_date
92
+ self.to_date = opts.delete(:to_date)&.to_date
90
93
 
91
- if permanent_stock? && date_unit.present?
92
- base_price * date_unit
93
- else
94
- base_price
95
- end
94
+ base_price = variant.price_in(currency)
95
+
96
+ self.price = if permanent_stock?
97
+ date_range.map do |date|
98
+ selected_price = variant.price_for_date(date: date, currency: currency) || base_price
99
+ (selected_price.amount || 0) + variant.price_modifier_amount_in(currency, opts.merge(date: date))
100
+ end.sum
101
+ else
102
+ (base_price.amount || 0) + variant.price_modifier_amount_in(currency, opts)
103
+ end
104
+ end
105
+
106
+ # override to calculate price per date for permanent stock
107
+ def update_price
108
+ base_price = variant.price_in(order.currency)
109
+ self.price = if permanent_stock?
110
+ date_range.map do |date|
111
+ selected_price = variant.price_for_date(date: date, currency: order.currency) || base_price
112
+ selected_price.amount || 0
113
+ end.sum
114
+ else
115
+ base_price.amount || 0
116
+ end
96
117
  end
97
118
 
98
119
  def allowed_self_check_in?
@@ -179,8 +200,6 @@ module SpreeCmCommissioner
179
200
 
180
201
  # override
181
202
  def sufficient_stock?
182
- return transit_sufficient_stock? if variant.product.product_type == 'transit'
183
-
184
203
  SpreeCmCommissioner::Stock::LineItemAvailabilityChecker.new(self).can_supply?(quantity)
185
204
  end
186
205
 
@@ -199,18 +218,6 @@ module SpreeCmCommissioner
199
218
  errors.add(:quantity, 'exceeded_max_quantity_per_order') if quantity > variant.max_quantity_per_order
200
219
  end
201
220
 
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
221
  def update_vendor_id
215
222
  self.vendor_id = variant.vendor_id
216
223
  end
@@ -233,45 +240,6 @@ module SpreeCmCommissioner
233
240
 
234
241
  from_date + variant.month.month + day.days
235
242
  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
243
  end
276
244
  end
277
245
 
@@ -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