spree_cm_commissioner 2.0.3.pre.pre4 → 2.0.3.pre.pre6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test_and_build_gem.yml +16 -2
- data/.gitignore +2 -1
- data/Gemfile.lock +28 -1
- data/Rakefile +33 -4
- data/app/assets/stylesheets/spree_cm_commissioner/backend/calendar.scss +8 -11
- data/app/controllers/spree/admin/inventory_items_controller.rb +83 -0
- data/app/controllers/spree/admin/stock_managements_controller.rb +63 -1
- data/app/controllers/spree/api/v2/storefront/accommodations/variants_controller.rb +42 -0
- data/app/controllers/spree/api/v2/storefront/accommodations_controller.rb +14 -31
- data/app/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +2 -2
- data/app/controllers/spree/api/v2/storefront/trip_places_controller.rb +29 -0
- data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +62 -0
- data/app/controllers/spree_cm_commissioner/admin/products_controller_decorator.rb +14 -0
- data/app/finders/spree_cm_commissioner/accommodations/find.rb +37 -0
- data/app/finders/spree_cm_commissioner/accommodations/find_variant.rb +32 -0
- data/app/interactors/spree_cm_commissioner/ensure_correct_product_type.rb +40 -0
- data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +25 -0
- data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +44 -0
- data/app/interactors/spree_cm_commissioner/stock/inventory_items_adjuster.rb +13 -0
- data/app/interactors/spree_cm_commissioner/stock/inventory_items_generator.rb +15 -0
- data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +75 -0
- data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +32 -0
- data/app/interactors/spree_cm_commissioner/taxi_trip_creator.rb +66 -0
- data/app/interactors/spree_cm_commissioner/transit/draft_order_creator.rb +132 -0
- data/app/interactors/spree_cm_commissioner/variant_block_updater.rb +62 -0
- data/app/interactors/spree_cm_commissioner/vehicle_updater.rb +41 -0
- data/app/jobs/spree_cm_commissioner/application_job.rb +20 -0
- data/app/jobs/spree_cm_commissioner/application_unique_job.rb +20 -0
- data/app/jobs/spree_cm_commissioner/ensure_correct_product_type_job.rb +7 -0
- data/app/jobs/spree_cm_commissioner/inventory_item_syncer_job.rb +7 -0
- data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +11 -0
- data/app/jobs/spree_cm_commissioner/stock/inventory_items_generator_job.rb +11 -0
- data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +9 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +18 -15
- data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +29 -15
- data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +89 -0
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +2 -12
- data/app/models/concerns/spree_cm_commissioner/order_seatable.rb +44 -0
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +39 -0
- data/app/models/concerns/spree_cm_commissioner/product_delegation.rb +1 -3
- data/app/models/concerns/spree_cm_commissioner/product_type.rb +10 -0
- data/app/models/concerns/spree_cm_commissioner/route_type.rb +1 -1
- data/app/models/concerns/spree_cm_commissioner/taxon_kind.rb +1 -1
- data/app/models/concerns/spree_cm_commissioner/vehicle_type.rb +11 -0
- data/app/models/spree_cm_commissioner/address_decorator.rb +36 -2
- data/app/models/spree_cm_commissioner/block.rb +43 -0
- data/app/models/spree_cm_commissioner/guest.rb +35 -0
- data/app/models/spree_cm_commissioner/inventory.rb +11 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +69 -0
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +47 -78
- data/app/models/spree_cm_commissioner/notification_taxon.rb +1 -1
- data/app/models/spree_cm_commissioner/option_type_decorator.rb +1 -11
- data/app/models/spree_cm_commissioner/order/address_book_decorator.rb +30 -12
- data/app/models/spree_cm_commissioner/order_decorator.rb +38 -1
- data/app/models/spree_cm_commissioner/place.rb +3 -4
- data/app/models/spree_cm_commissioner/price_decorator.rb +9 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +39 -6
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +41 -0
- data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +126 -0
- data/app/models/spree_cm_commissioner/redis_stock/line_items_cached_inventory_items_builder.rb +36 -0
- data/app/models/spree_cm_commissioner/redis_stock/variant_cached_inventory_items_builder.rb +25 -0
- data/app/models/spree_cm_commissioner/reserved_block.rb +30 -0
- data/app/models/spree_cm_commissioner/saved_guest.rb +15 -0
- data/app/models/spree_cm_commissioner/seat_layout.rb +20 -0
- data/app/models/spree_cm_commissioner/seat_section.rb +16 -0
- data/app/models/spree_cm_commissioner/seats/blocks_canceler.rb +30 -0
- data/app/models/spree_cm_commissioner/seats/blocks_holder.rb +53 -0
- data/app/models/spree_cm_commissioner/seats/blocks_reserver.rb +49 -0
- data/app/models/spree_cm_commissioner/seats/errors/blocks_are_on_hold_by_other_guest.rb +4 -0
- data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_other_guest.rb +4 -0
- data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_same_guest.rb +4 -0
- data/app/models/spree_cm_commissioner/seats/errors/unable_to_save_reserved_block_record.rb +4 -0
- data/app/models/spree_cm_commissioner/service_calendar.rb +0 -2
- data/app/models/spree_cm_commissioner/state_decorator.rb +0 -1
- data/app/models/spree_cm_commissioner/stock/availability_checker.rb +26 -25
- data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +2 -1
- data/app/models/spree_cm_commissioner/stock/line_item_availability_checker.rb +3 -3
- data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +44 -0
- data/app/models/spree_cm_commissioner/stock_item_decorator.rb +17 -0
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -1
- data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +0 -6
- data/app/models/spree_cm_commissioner/trip.rb +19 -10
- data/app/models/spree_cm_commissioner/trip_connection.rb +5 -5
- data/app/models/spree_cm_commissioner/trip_stop.rb +10 -21
- data/app/models/spree_cm_commissioner/variant_block.rb +17 -0
- data/app/models/spree_cm_commissioner/variant_decorator.rb +37 -47
- data/app/models/spree_cm_commissioner/variant_options.rb +0 -23
- data/app/models/spree_cm_commissioner/vehicle.rb +11 -16
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +14 -14
- data/app/models/spree_cm_commissioner/vendor_place.rb +13 -3
- data/app/overrides/spree/admin/products/_form/industry_taxons.html.erb.deface +26 -0
- data/app/overrides/spree/admin/products/_form/taxons.html.erb.deface +26 -0
- data/app/queries/spree_cm_commissioner/trip_query.rb +110 -131
- data/app/request_schemas/spree_cm_commissioner/accommodation_request_schema.rb +3 -0
- data/app/request_schemas/spree_cm_commissioner/application_request_schema.rb +1 -1
- data/app/request_schemas/spree_cm_commissioner/trip_search_request_schema.rb +20 -0
- data/app/request_schemas/spree_cm_commissioner/variant_request_schema.rb +19 -0
- data/app/serializers/spree/v2/storefront/accommodation_serializer.rb +2 -0
- data/app/serializers/spree/v2/storefront/amenity_serializer.rb +15 -0
- data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +1 -0
- data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +2 -0
- data/app/serializers/spree/v2/storefront/trip_place_serializer.rb +10 -0
- data/app/serializers/spree/v2/storefront/trip_query_result_serializer.rb +14 -0
- data/app/serializers/spree/v2/storefront/trip_result_serializer.rb +17 -0
- data/app/serializers/spree/v2/storefront/trip_vehicle_serializer.rb +11 -0
- data/app/serializers/spree/v2/storefront/trip_vendor_serializer.rb +13 -0
- data/app/views/spree/admin/inventory_items/show.html.erb +72 -0
- data/app/views/spree/admin/stock_managements/_variant_stock_items.html.erb +7 -2
- data/app/views/spree/admin/stock_managements/calendar.html.erb +56 -0
- data/app/views/spree/admin/stock_managements/index.html.erb +55 -6
- data/config/initializers/paper_trail.rb +1 -0
- data/config/initializers/spree_permitted_attributes.rb +5 -0
- data/config/routes.rb +24 -2
- data/db/migrate/20240202080634_update_counter_cache_of_vehicle_type.rb +3 -1
- data/db/migrate/20250122000000_move_route_type_from_vehicles_to_trips.rb +7 -0
- data/db/migrate/20250304293518_create_cm_inventory_items.rb +21 -0
- data/db/migrate/20250429094228_add_lock_version_to_cm_inventory_items.rb +5 -0
- data/db/migrate/20250502025848_add_index_to_spree_products.rb +5 -0
- data/db/migrate/20250502030001_add_product_type_to_spree_variants.rb +5 -0
- data/db/migrate/20250502030002_add_product_type_to_spree_line_items.rb +5 -0
- data/db/migrate/20250603035256_add_inventory_item_to_spree_prices.rb +7 -0
- data/db/migrate/20250619073724_drop_table_cm_line_item_seats.rb +5 -0
- data/db/migrate/20250619073812_drop_table_cm_vehicle_seats.rb +5 -0
- data/db/migrate/20250619073844_drop_table_cm_vehicle_types.rb +9 -0
- data/db/migrate/20250619073957_drop_table_cm_option_value_vehicle_types.rb +5 -0
- data/db/migrate/20250619082354_remove_unnecessary_fields_from_cm_places.rb +9 -0
- data/db/migrate/20250619082736_remove_route_type_from_spree_products.rb +5 -0
- data/db/migrate/20250619083055_remove_unnecessary_fields_from_spree_taxons.rb +5 -0
- data/db/migrate/20250620083055_remove_variant_id_from_cm_trips.rb +5 -0
- data/db/migrate/20250620090000_update_cm_trip_connections_to_use_cm_trips.rb +6 -0
- data/db/migrate/20250620090001_create_cm_seat_layouts.rb +17 -0
- data/db/migrate/20250620090002_create_cm_seat_sections.rb +18 -0
- data/db/migrate/20250620090003_create_cm_blocks.rb +18 -0
- data/db/migrate/20250624091005_create_cm_reserved_blocks.rb +29 -0
- data/db/migrate/20250626083642_create_cm_variant_blocks.rb +24 -0
- data/db/migrate/20250627023314_add_block_id_to_cm_guests.rb +13 -0
- data/db/migrate/20250716022821_add_location_reference_to_cm_vendor_places.rb +5 -0
- data/db/migrate/20250716031743_drop_table_cm_vendor_stops.rb +5 -0
- data/db/migrate/20250717023824_add_vendor_reference_to_cm_trips.rb +5 -0
- data/db/migrate/20250717041414_add_location_place_reference_to_cm_trip_stops.rb +5 -0
- data/db/migrate/20250717042539_rename_cm_trip_stops_stop_id_column_to_stop_place_id.rb +7 -0
- data/db/migrate/20250717042707_rename_cm_trips_origin_and_destination_to_origin_place_and_destination_place.rb +11 -0
- data/db/migrate/20250721080738_add_vehicle_type_to_cm_vehicles.rb +7 -0
- data/db/migrate/20250725092713_add_block_type_to_cm_blocks.rb +5 -0
- data/db/migrate/20250731062816_add_departure_time_and_arrival_time_to_trip_stop.rb +6 -0
- data/db/migrate/20250807033035_create_spree_cm_commissioner_saved_guests.rb +23 -0
- data/db/migrate/20250808054835_add_saved_guest_reference_to_cm_blocks.rb +5 -0
- data/db/migrate/20250812015522_remove_unique_indexes_from_cm_user_identity_providers.rb +8 -0
- data/db/migrate/20250814073854_add_call_to_action_to_spree_products.rb +5 -0
- data/docker-compose.yml +1 -1
- data/lib/cm_app_logger.rb +11 -4
- data/lib/generators/spree_cm_commissioner/install/install_generator.rb +14 -11
- data/lib/generators/spree_cm_commissioner/install/templates/app/javascript/{spree_cm_commissioner → spree_dashboard/spree_cm_commissioner}/utilities.js +4 -0
- data/lib/spree_cm_commissioner/cached_inventory_item.rb +23 -0
- data/lib/spree_cm_commissioner/calendar_event.rb +11 -1
- data/lib/spree_cm_commissioner/test_helper/factories/block_factory.rb +16 -0
- data/lib/spree_cm_commissioner/test_helper/factories/guest_factory.rb +10 -0
- data/lib/spree_cm_commissioner/test_helper/factories/homepage_section_relatable_factory.rb +1 -1
- data/lib/spree_cm_commissioner/test_helper/factories/inventory_item_factory.rb +9 -0
- data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +1 -1
- data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +6 -30
- data/lib/spree_cm_commissioner/test_helper/factories/order_factory.rb +0 -36
- data/lib/spree_cm_commissioner/test_helper/factories/product_factory.rb +18 -34
- data/lib/spree_cm_commissioner/test_helper/factories/reserved_block_factory.rb +27 -0
- data/lib/spree_cm_commissioner/test_helper/factories/seat_layout_factory.rb +11 -0
- data/lib/spree_cm_commissioner/test_helper/factories/seat_section_factory.rb +16 -0
- data/lib/spree_cm_commissioner/test_helper/factories/stock_location_factory.rb +2 -2
- data/lib/spree_cm_commissioner/test_helper/factories/trip_connection_factory.rb +6 -0
- data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +11 -3
- data/lib/spree_cm_commissioner/test_helper/factories/trip_stop_factory.rb +13 -0
- data/lib/spree_cm_commissioner/test_helper/factories/variant_block_factory.rb +7 -0
- data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +41 -19
- data/lib/spree_cm_commissioner/test_helper/factories/vehicle_factory.rb +1 -1
- data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +6 -1
- data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +13 -1
- data/lib/spree_cm_commissioner/transit/leg.rb +37 -0
- data/lib/spree_cm_commissioner/transit/seat_selection.rb +27 -0
- data/lib/spree_cm_commissioner/trip_query_result.rb +50 -0
- data/lib/spree_cm_commissioner/trip_result.rb +19 -10
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +37 -1
- data/lib/tasks/create_default_non_permanent_inventory_items.rake +16 -0
- data/lib/tasks/ensure_correct_product_type.rake +7 -0
- data/lib/tasks/generate_inventory_items.rake +7 -0
- data/spree_cm_commissioner.gemspec +7 -0
- metadata +167 -26
- data/app/controllers/concerns/spree_cm_commissioner/transit/taxon_bitwise.rb +0 -44
- data/app/finders/spree_cm_commissioner/line_items/find_by_variant_decorator.rb +0 -20
- data/app/models/spree_cm_commissioner/branch.rb +0 -12
- data/app/models/spree_cm_commissioner/line_item_seat.rb +0 -10
- data/app/models/spree_cm_commissioner/option_value_vehicle_type.rb +0 -8
- data/app/models/spree_cm_commissioner/stop.rb +0 -23
- data/app/models/spree_cm_commissioner/vehicle_seat.rb +0 -11
- data/app/models/spree_cm_commissioner/vehicle_type.rb +0 -76
- data/app/models/spree_cm_commissioner/vendor_stop.rb +0 -10
- data/app/queries/spree_cm_commissioner/trip_search_query.rb +0 -76
- data/app/queries/spree_cm_commissioner/variant_availability/non_permanent_stock_query.rb +0 -45
- data/app/queries/spree_cm_commissioner/variant_availability/permanent_stock_query.rb +0 -55
- data/app/queries/spree_cm_commissioner/vendor_stop_place_query.rb +0 -54
- data/app/services/spree_cm_commissioner/vehicle_option_value_creator.rb +0 -11
- data/lib/spree_cm_commissioner/test_helper/factories/branch_factory.rb +0 -12
- data/lib/spree_cm_commissioner/test_helper/factories/departure_time_option_type_factory.rb +0 -8
- data/lib/spree_cm_commissioner/test_helper/factories/duration_option_type_factory.rb +0 -8
- data/lib/spree_cm_commissioner/test_helper/factories/line_item_seat_factory.rb +0 -7
- data/lib/spree_cm_commissioner/test_helper/factories/stop_factory.rb +0 -14
- data/lib/spree_cm_commissioner/test_helper/factories/transit_place_factory.rb +0 -8
- data/lib/spree_cm_commissioner/test_helper/factories/vehicle_option_type_factory.rb +0 -8
- data/lib/spree_cm_commissioner/test_helper/factories/vehicle_type_factory.rb +0 -96
- data/lib/spree_cm_commissioner/trip_seat_layout_result.rb +0 -11
@@ -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
|
@@ -18,29 +18,32 @@ module SpreeCmCommissioner
|
|
18
18
|
def date_unit
|
19
19
|
return nil unless permanent_stock?
|
20
20
|
|
21
|
-
|
21
|
+
date_range.size
|
22
22
|
end
|
23
23
|
|
24
|
-
def
|
24
|
+
def date_range
|
25
25
|
return [] unless date_present?
|
26
|
+
return [] unless permanent_stock?
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
28
|
+
# For transit products (e.g., bus, train), only return the departure day,
|
29
|
+
# as the booking represents a single trip rather than a duration.
|
30
|
+
return [from_date.to_date] if transit?
|
31
31
|
|
32
|
-
|
33
|
-
return
|
32
|
+
# For products with a stay or usage period (e.g., hotel, car rental, activities),
|
33
|
+
# return the full date range from start to end, inclusive.
|
34
|
+
return (from_date.to_date..to_date.to_date).to_a if service? || accommodation?
|
34
35
|
|
35
|
-
|
36
|
+
[]
|
36
37
|
end
|
37
38
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
def checkin_date
|
40
|
+
from_date&.to_datetime
|
41
|
+
end
|
42
|
+
|
43
|
+
def checkout_date
|
44
|
+
return to_date ? to_date.to_datetime + 1.day : nil if accommodation?
|
45
|
+
|
46
|
+
to_date&.to_datetime
|
44
47
|
end
|
45
48
|
|
46
49
|
private
|
@@ -1,13 +1,21 @@
|
|
1
|
+
# Number of guests are depend on the variant, but user is allowed to input custom number of guests
|
2
|
+
# which will be stored in public_metadata.
|
3
|
+
#
|
4
|
+
# metadata {
|
5
|
+
# 'number_of_adults': 2,
|
6
|
+
# 'number_of_kids': 1,
|
7
|
+
# }
|
1
8
|
module SpreeCmCommissioner
|
2
9
|
module LineItemGuestsConcern
|
3
10
|
extend ActiveSupport::Concern
|
4
11
|
|
5
12
|
included do
|
6
|
-
validate :validate_total_adults, if: -> {
|
7
|
-
validate :validate_total_kids, if: -> {
|
13
|
+
validate :validate_total_adults, if: -> { valid_integer_metadata?('number_of_adults') }
|
14
|
+
validate :validate_total_kids, if: -> { valid_integer_metadata?('number_of_kids') }
|
8
15
|
end
|
9
16
|
|
10
|
-
def
|
17
|
+
def number_of_adults = public_metadata['number_of_adults']&.to_i || (variant.number_of_adults * quantity)
|
18
|
+
def number_of_kids = public_metadata['number_of_kids']&.to_i || (variant.number_of_kids * quantity)
|
11
19
|
def number_of_guests = number_of_adults + number_of_kids
|
12
20
|
|
13
21
|
def allowed_extra_adults = variant.allowed_extra_adults * quantity
|
@@ -16,8 +24,7 @@ module SpreeCmCommissioner
|
|
16
24
|
def allowed_total_adults = (variant.number_of_adults * quantity) + allowed_extra_adults
|
17
25
|
def allowed_total_kids = (variant.number_of_kids * quantity) + allowed_extra_kids
|
18
26
|
|
19
|
-
def
|
20
|
-
def number_of_kids = public_metadata[:number_of_kids] || (variant.number_of_kids * quantity)
|
27
|
+
def remaining_total_guests = [number_of_guests - guests.size, 0].max
|
21
28
|
|
22
29
|
def generate_remaining_guests
|
23
30
|
return if remaining_total_guests.zero?
|
@@ -28,15 +35,17 @@ module SpreeCmCommissioner
|
|
28
35
|
end
|
29
36
|
|
30
37
|
def extra_adults
|
38
|
+
return 0 unless valid_integer_metadata?('number_of_adults')
|
31
39
|
return 0 unless extra_adults?
|
32
40
|
|
33
|
-
public_metadata[
|
41
|
+
public_metadata['number_of_adults'].to_i - (variant.number_of_adults * quantity)
|
34
42
|
end
|
35
43
|
|
36
44
|
def extra_kids
|
45
|
+
return 0 unless valid_integer_metadata?('number_of_kids')
|
37
46
|
return 0 unless extra_kids?
|
38
47
|
|
39
|
-
public_metadata[
|
48
|
+
public_metadata['number_of_kids'].to_i - (variant.number_of_kids * quantity)
|
40
49
|
end
|
41
50
|
|
42
51
|
def guest_options
|
@@ -55,25 +64,30 @@ module SpreeCmCommissioner
|
|
55
64
|
end
|
56
65
|
|
57
66
|
def extra_adults?
|
58
|
-
|
67
|
+
return false unless valid_integer_metadata?('number_of_adults')
|
68
|
+
|
69
|
+
public_metadata['number_of_adults'].to_i > variant.number_of_adults * quantity
|
59
70
|
end
|
60
71
|
|
61
72
|
def extra_kids?
|
62
|
-
|
73
|
+
return false unless valid_integer_metadata?('number_of_kids')
|
74
|
+
|
75
|
+
public_metadata['number_of_kids'].to_i > variant.number_of_kids * quantity
|
63
76
|
end
|
64
77
|
|
65
78
|
private
|
66
79
|
|
67
|
-
def
|
68
|
-
|
80
|
+
def valid_integer_metadata?(key)
|
81
|
+
value = public_metadata[key.to_s]
|
82
|
+
value.present? && value.to_s.match?(/\A-?\d+\z/)
|
83
|
+
end
|
69
84
|
|
70
|
-
|
85
|
+
def validate_total_adults
|
86
|
+
errors.add(:quantity, 'exceed_total_adults') if public_metadata['number_of_adults'].to_i > allowed_total_adults
|
71
87
|
end
|
72
88
|
|
73
89
|
def validate_total_kids
|
74
|
-
|
75
|
-
|
76
|
-
errors.add(:quantity, 'exceed_total_kids')
|
90
|
+
errors.add(:quantity, 'exceed_total_kids') if public_metadata['number_of_kids'].to_i > allowed_total_kids
|
77
91
|
end
|
78
92
|
end
|
79
93
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# This concern allow line item to have trip-related metadata.
|
2
|
+
#
|
3
|
+
# Metadata structure:
|
4
|
+
# {
|
5
|
+
# 'direction': 'outbound' or 'inbound',
|
6
|
+
# 'trip_id': 123,
|
7
|
+
# 'boarding_trip_stop_id': 456,
|
8
|
+
# 'drop_off_trip_stop_id': 789
|
9
|
+
# }
|
10
|
+
#
|
11
|
+
# Direction:
|
12
|
+
# - "outbound": The trip is moving away from the origin (e.g., A → B → C → D).
|
13
|
+
# This term is more flexible than "departure" and applies to both direct and connected trips.
|
14
|
+
# - "inbound": The trip is returning toward the origin (e.g., D → C → B → A).
|
15
|
+
# This term is more flexible than "return" and applies to both direct and connected trips.
|
16
|
+
module SpreeCmCommissioner
|
17
|
+
module LineItemTransitable
|
18
|
+
extend ActiveSupport::Concern
|
19
|
+
|
20
|
+
DIRECTION = %w[outbound inbound].freeze
|
21
|
+
|
22
|
+
TripKey = Struct.new(:trip_id, :direction, :boarding_trip_stop_id, :drop_off_trip_stop_id)
|
23
|
+
|
24
|
+
included do
|
25
|
+
validates :direction, inclusion: { in: DIRECTION }, allow_nil: true
|
26
|
+
validates :trip_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
27
|
+
validates :boarding_trip_stop_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
28
|
+
validates :drop_off_trip_stop_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
29
|
+
|
30
|
+
# Groups LineItems by trip fields. Some fields can be null,
|
31
|
+
# so use it with caution. Output:
|
32
|
+
# {
|
33
|
+
# $<TripKey:> => [<LineItem id:1>]
|
34
|
+
# $<TripKey:> => [<LineItem id:2>]
|
35
|
+
# $<TripKey:> => [<LineItem id:3>, <LineItem id:4>]
|
36
|
+
# }
|
37
|
+
def self.group_by_trip_key
|
38
|
+
all.group_by do |line_item|
|
39
|
+
TripKey.new(
|
40
|
+
line_item.trip_id,
|
41
|
+
line_item.direction,
|
42
|
+
line_item.boarding_trip_stop_id,
|
43
|
+
line_item.drop_off_trip_stop_id
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def outbound? = direction == 'outbound'
|
50
|
+
def inbound? = direction == 'inbound'
|
51
|
+
|
52
|
+
def direction = private_metadata['direction']
|
53
|
+
def trip_id = private_metadata['trip_id']&.to_i
|
54
|
+
def boarding_trip_stop_id = private_metadata['boarding_trip_stop_id']&.to_i
|
55
|
+
def drop_off_trip_stop_id = private_metadata['drop_off_trip_stop_id']&.to_i
|
56
|
+
|
57
|
+
def direction=(value)
|
58
|
+
if value.nil?
|
59
|
+
private_metadata.delete('direction')
|
60
|
+
else
|
61
|
+
private_metadata['direction'] = value.to_s
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def trip_id=(value)
|
66
|
+
if value.nil?
|
67
|
+
private_metadata.delete('trip_id')
|
68
|
+
else
|
69
|
+
private_metadata['trip_id'] = value.to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def boarding_trip_stop_id=(value)
|
74
|
+
if value.nil?
|
75
|
+
private_metadata.delete('boarding_trip_stop_id')
|
76
|
+
else
|
77
|
+
private_metadata['boarding_trip_stop_id'] = value.to_s
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def drop_off_trip_stop_id=(value)
|
82
|
+
if value.nil?
|
83
|
+
private_metadata.delete('drop_off_trip_stop_id')
|
84
|
+
else
|
85
|
+
private_metadata['drop_off_trip_stop_id'] = value.to_s
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -14,13 +14,6 @@ module SpreeCmCommissioner
|
|
14
14
|
state_selection
|
15
15
|
payment_option
|
16
16
|
delivery_option
|
17
|
-
amenity
|
18
|
-
departure_time
|
19
|
-
duration
|
20
|
-
vehicle_id
|
21
|
-
origin
|
22
|
-
destination
|
23
|
-
place_id
|
24
17
|
].freeze
|
25
18
|
|
26
19
|
RESERVED_OPTIONS = {
|
@@ -48,11 +41,8 @@ module SpreeCmCommissioner
|
|
48
41
|
'bib-display-prefix' => 'boolean',
|
49
42
|
'bib-pre-generation-on-create' => 'boolean',
|
50
43
|
'seat-number-positions' => 'array',
|
51
|
-
'
|
52
|
-
'
|
53
|
-
'departure-time' => 'time',
|
54
|
-
'vehicle' => 'vehicle_id',
|
55
|
-
'allow-seat-selection' => 'boolean'
|
44
|
+
'seat-type' => 'string',
|
45
|
+
'intercity-taxi' => 'string'
|
56
46
|
}.freeze
|
57
47
|
|
58
48
|
included do
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
module OrderSeatable
|
3
|
+
def should_manage_blocks?
|
4
|
+
preload_block_ids.any?
|
5
|
+
end
|
6
|
+
|
7
|
+
def hold_blocks!
|
8
|
+
return unless should_manage_blocks?
|
9
|
+
|
10
|
+
CmAppLogger.log(label: "#{self.class.name}#hold_blocks!", data: { order_id: id }) do
|
11
|
+
SpreeCmCommissioner::Seats::BlocksHolder.new(line_item_ids: line_item_ids, hold_by: user).hold_blocks!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def cancel_blocks!
|
16
|
+
return unless should_manage_blocks?
|
17
|
+
|
18
|
+
CmAppLogger.log(label: "#{self.class.name}#cancel_blocks!", data: { order_id: id }) do
|
19
|
+
SpreeCmCommissioner::Seats::BlocksCanceler.new(order_id: id, cancel_by: user).cancel_blocks!
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def reserve_blocks!
|
24
|
+
return unless should_manage_blocks?
|
25
|
+
|
26
|
+
CmAppLogger.log(label: "#{self.class.name}#reserve_blocks!", data: { order_id: id }) do
|
27
|
+
SpreeCmCommissioner::Seats::BlocksReserver.new(line_item_ids: line_item_ids, reserve_by: user).reserve_blocks!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Calling `.block_ids` directly can cause many slow database queries (N+1 problem)
|
32
|
+
# every time `.should_manage_blocks?` or `.preload_block_ids` runs.
|
33
|
+
# To avoid this, we store a precomputed list of block IDs in `private_metadata`.
|
34
|
+
# This list is updated whenever a guest’s block is saved or destroy.
|
35
|
+
def preload_block_ids=(preload_block_ids = [])
|
36
|
+
self.private_metadata ||= {}
|
37
|
+
self.private_metadata['preload_block_ids'] = preload_block_ids
|
38
|
+
end
|
39
|
+
|
40
|
+
def preload_block_ids
|
41
|
+
self.private_metadata&.fetch('preload_block_ids', []) || []
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -5,6 +5,8 @@ module SpreeCmCommissioner
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
+
state_machine.before_transition to: :payment, do: :hold_blocks!
|
9
|
+
|
8
10
|
state_machine.before_transition to: :complete, do: :request, if: :need_confirmation?
|
9
11
|
state_machine.before_transition to: :complete, do: :generate_bib_number
|
10
12
|
|
@@ -14,10 +16,14 @@ module SpreeCmCommissioner
|
|
14
16
|
state_machine.after_transition to: :complete, do: :send_order_complete_telegram_alert_to_vendors, unless: :need_confirmation?
|
15
17
|
state_machine.after_transition to: :complete, do: :send_order_complete_telegram_alert_to_store, unless: :need_confirmation?
|
16
18
|
|
19
|
+
state_machine.around_transition to: :complete, do: :unstock_inventory!
|
20
|
+
|
17
21
|
state_machine.after_transition to: :complete, do: :send_transaction_email_to_user, if: :user_has_email?
|
18
22
|
state_machine.after_transition to: :resumed, do: :precalculate_conversion
|
23
|
+
state_machine.around_transition to: :resumed, do: :unstock_inventory!
|
19
24
|
|
20
25
|
state_machine.after_transition to: :canceled, do: :precalculate_conversion
|
26
|
+
state_machine.after_transition to: :canceled, do: :restock_inventory!
|
21
27
|
|
22
28
|
scope :accepted, -> { where(request_state: 'accepted') }
|
23
29
|
|
@@ -67,6 +73,39 @@ module SpreeCmCommissioner
|
|
67
73
|
end
|
68
74
|
end
|
69
75
|
|
76
|
+
def unstock_inventory!
|
77
|
+
ActiveRecord::Base.transaction do
|
78
|
+
yield # Equal to block.call
|
79
|
+
|
80
|
+
# After the transition is complete, the following code will execute first before proceeding to other `after_transition` callbacks.
|
81
|
+
# This ensures that if `reserve_blocks!` or `unstock_inventory_in_redis!` fails, the state will be rolled back,
|
82
|
+
# and neither the `finalize!` method nor any notifications will be triggered.
|
83
|
+
# The payment will be reversed in vPago gem, and `Spree::Checkout::Complete` will be called, which checks `order.reload.complete?`.
|
84
|
+
# This is critical because if the order state is complete, the payment will be marked as paid.
|
85
|
+
|
86
|
+
reserve_blocks!
|
87
|
+
unstock_inventory_in_redis!
|
88
|
+
|
89
|
+
# We rollback only order state, and we keep payment state as it is.
|
90
|
+
# We implement payment in vPago gem, and it will be reversed in the gem.
|
91
|
+
# Some bank has api for refund, but some don't have the api to refund yet. So we keep the payment state as it is and refund manually.
|
92
|
+
end
|
93
|
+
rescue StandardError => e
|
94
|
+
CmAppLogger.log(
|
95
|
+
label: 'SpreeCmCommissioner::OrderStateMachine#reserve_inventory failed',
|
96
|
+
data: { order_id: id, error: e.message, type: e.class.name, backtrace: e.backtrace&.first(5)&.join("\n") }
|
97
|
+
)
|
98
|
+
|
99
|
+
raise e
|
100
|
+
end
|
101
|
+
|
102
|
+
def restock_inventory!
|
103
|
+
ActiveRecord::Base.transaction do
|
104
|
+
cancel_blocks!
|
105
|
+
restock_inventory_in_redis!
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
70
109
|
def generate_bib_number
|
71
110
|
line_items.find_each(&:generate_remaining_guests)
|
72
111
|
|
@@ -3,12 +3,10 @@ module SpreeCmCommissioner
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
delegate :
|
7
|
-
:subscribable?,
|
6
|
+
delegate :subscribable?,
|
8
7
|
:allowed_upload_later?,
|
9
8
|
:need_confirmation?, :need_confirmation,
|
10
9
|
:allow_anonymous_booking,
|
11
|
-
:accommodation?, :service?, :ecommerce?,
|
12
10
|
:allow_self_check_in,
|
13
11
|
:allow_self_check_in?,
|
14
12
|
:required_self_check_in_location,
|
@@ -6,10 +6,20 @@ module SpreeCmCommissioner
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
PRODUCT_TYPES = %i[accommodation service ecommerce transit].freeze
|
9
|
+
PERMANENT_STOCK_PRODUCT_TYPES = %w[accommodation service transit].freeze
|
10
|
+
PRE_INVENTORY_DAYS = { 'transit' => 90, 'accommodation' => 365, 'service' => 30 }.freeze
|
9
11
|
|
10
12
|
included do
|
11
13
|
enum product_type: PRODUCT_TYPES if table_exists? && column_names.include?('product_type')
|
12
14
|
enum primary_product_type: PRODUCT_TYPES if table_exists? && column_names.include?('primary_product_type')
|
13
15
|
end
|
16
|
+
|
17
|
+
def permanent_stock?
|
18
|
+
PERMANENT_STOCK_PRODUCT_TYPES.include?(product_type)
|
19
|
+
end
|
20
|
+
|
21
|
+
def pre_inventory_days
|
22
|
+
PRE_INVENTORY_DAYS[product_type]
|
23
|
+
end
|
14
24
|
end
|
15
25
|
end
|
@@ -2,7 +2,7 @@ module SpreeCmCommissioner
|
|
2
2
|
module RouteType
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
ROUTE_TYPES = %i[
|
5
|
+
ROUTE_TYPES = %i[bus intercity_taxi airport_shuttle rails ferry].freeze
|
6
6
|
|
7
7
|
included do
|
8
8
|
enum route_type: ROUTE_TYPES if table_exists? && column_names.include?('route_type')
|
@@ -3,7 +3,7 @@ module SpreeCmCommissioner
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
enum kind: { category: 0, cms: 1, event: 2, occupation: 3, nationality: 4, organization: 5, transit: 6 }
|
6
|
+
enum kind: { category: 0, cms: 1, event: 2, occupation: 3, nationality: 4, organization: 5, transit: 6, industry: 7 }
|
7
7
|
end
|
8
8
|
end
|
9
9
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
module VehicleType
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
VEHICLE_TYPES = %i[suv sedan minivan van sleeping_bus luxury_van air_bus].freeze
|
6
|
+
|
7
|
+
included do
|
8
|
+
enum vehicle_type: VEHICLE_TYPES if table_exists? && column_names.include?('vehicle_type')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -1,10 +1,44 @@
|
|
1
1
|
module SpreeCmCommissioner
|
2
2
|
module AddressDecorator
|
3
3
|
def self.prepended(base)
|
4
|
-
# Field is not mandatory
|
5
4
|
base.enum gender: { :not_selected => 0, :male => 1, :female => 2, :other => 3 }
|
5
|
+
|
6
|
+
base.validates :address1, presence: true, if: :require_address1?
|
7
|
+
base.validates :city, presence: true, if: :require_city?
|
8
|
+
base.validates :country, presence: true, if: :require_country?
|
9
|
+
end
|
10
|
+
|
11
|
+
# override to return false instead of true
|
12
|
+
# in case no country
|
13
|
+
def require_zipcode?
|
14
|
+
country ? country.zipcode_required? : false
|
15
|
+
end
|
16
|
+
|
17
|
+
def require_address1?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
def require_city?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def require_country?
|
26
|
+
false
|
6
27
|
end
|
7
28
|
end
|
8
29
|
end
|
9
30
|
|
10
|
-
|
31
|
+
if Spree::Address.included_modules.exclude?(SpreeCmCommissioner::AddressDecorator)
|
32
|
+
keys = %i[address1 city country]
|
33
|
+
|
34
|
+
Spree::Address._validators.reject! { |key, _| keys.include?(key) }
|
35
|
+
Spree::Address._validate_callbacks.each do |c|
|
36
|
+
next unless c.filter.respond_to?(:attributes)
|
37
|
+
|
38
|
+
c.filter.attributes.delete(:address1)
|
39
|
+
c.filter.attributes.delete(:city)
|
40
|
+
c.filter.attributes.delete(:country)
|
41
|
+
end
|
42
|
+
|
43
|
+
Spree::Address.prepend(SpreeCmCommissioner::AddressDecorator)
|
44
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
class Block < Base
|
3
|
+
enum block_type: {
|
4
|
+
seat: 1,
|
5
|
+
sleeping_seat: 2,
|
6
|
+
driver: 3,
|
7
|
+
bathroom: 4,
|
8
|
+
other: 0
|
9
|
+
}
|
10
|
+
|
11
|
+
belongs_to :seat_section, class_name: 'SpreeCmCommissioner::SeatSection', optional: true
|
12
|
+
belongs_to :seat_layout, class_name: 'SpreeCmCommissioner::SeatLayout', optional: false
|
13
|
+
|
14
|
+
validates :label, presence: true, if: :label_required?
|
15
|
+
|
16
|
+
has_many :variant_blocks, class_name: 'SpreeCmCommissioner::VariantBlock', dependent: :destroy
|
17
|
+
has_many :variants, class_name: 'Spree::Variant', through: :variant_blocks
|
18
|
+
|
19
|
+
validates :width, presence: true, numericality: { greater_than: 0 }
|
20
|
+
validates :height, presence: true, numericality: { greater_than: 0 }
|
21
|
+
validates :x, presence: true, numericality: true
|
22
|
+
validates :y, presence: true, numericality: true
|
23
|
+
validates :rotation, presence: true, numericality: { greater_than_or_equal_to: -360, less_than_or_equal_to: 360 }
|
24
|
+
|
25
|
+
before_validation :assign_layout_from_section, if: -> { seat_layout.nil? && seat_section.present? }
|
26
|
+
|
27
|
+
def label_required?
|
28
|
+
seatable? || block_type.to_s == 'other'
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.seatable?(block_type)
|
32
|
+
block_type.to_s.in?(%w[sleeping_seat seat])
|
33
|
+
end
|
34
|
+
|
35
|
+
def seatable?
|
36
|
+
self.class.seatable?(block_type)
|
37
|
+
end
|
38
|
+
|
39
|
+
def assign_layout_from_section
|
40
|
+
self.seat_layout = seat_section.seat_layout
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -24,10 +24,17 @@ module SpreeCmCommissioner
|
|
24
24
|
scope :none_bib, -> { where(bib_prefix: [nil, '']) }
|
25
25
|
|
26
26
|
belongs_to :line_item, class_name: 'Spree::LineItem'
|
27
|
+
|
27
28
|
has_one :variant, class_name: 'Spree::Variant', through: :line_item
|
29
|
+
|
28
30
|
belongs_to :user, class_name: 'Spree::User'
|
29
31
|
belongs_to :occupation, class_name: 'Spree::Taxon'
|
30
32
|
belongs_to :nationality, class_name: 'Spree::Taxon'
|
33
|
+
belongs_to :block, class_name: 'SpreeCmCommissioner::Block', optional: true
|
34
|
+
belongs_to :saved_guest, class_name: 'SpreeCmCommissioner::SavedGuest', optional: true
|
35
|
+
|
36
|
+
has_one :reserved_block, class_name: 'SpreeCmCommissioner::ReservedBlock'
|
37
|
+
|
31
38
|
scope :checked_ins, -> { joins(:check_in) }
|
32
39
|
scope :no_show, -> { left_outer_joins(:check_in).where(cm_check_ins: { id: nil }) }
|
33
40
|
|
@@ -48,9 +55,15 @@ module SpreeCmCommissioner
|
|
48
55
|
|
49
56
|
before_create :generate_bib, if: -> { line_item.reload && variant.bib_pre_generation_on_create? }
|
50
57
|
|
58
|
+
after_create :preload_order_block_ids, if: -> { block_id.present? }
|
59
|
+
after_update :preload_order_block_ids, if: :saved_change_to_block_id?
|
60
|
+
after_destroy :preload_order_block_ids
|
61
|
+
|
51
62
|
validates :seat_number, uniqueness: { scope: :event_id }, allow_nil: true, if: -> { event_id.present? }
|
52
63
|
validates :bib_index, uniqueness: true, allow_nil: true
|
53
64
|
|
65
|
+
validate :validate_block, if: :should_validate_block?
|
66
|
+
|
54
67
|
self.whitelisted_ransackable_associations = %w[id_card event line_item]
|
55
68
|
self.whitelisted_ransackable_attributes = %w[first_name last_name phone_number gender contact occupation_id card_type created_at check_in_status]
|
56
69
|
|
@@ -283,6 +296,28 @@ module SpreeCmCommissioner
|
|
283
296
|
filled_bib_number
|
284
297
|
end
|
285
298
|
end
|
299
|
+
|
300
|
+
# validate only if block_id is present AND (new record OR block_id is changing)
|
301
|
+
# goal: don't bother re-validating when block_id stays the same
|
302
|
+
def should_validate_block?
|
303
|
+
block_id.present? && (new_record? || will_save_change_to_block_id?)
|
304
|
+
end
|
305
|
+
|
306
|
+
def validate_block
|
307
|
+
return if block_id.blank?
|
308
|
+
return if line_item.blank? || line_item.variant.blank?
|
309
|
+
return if line_item.variant.variant_blocks.exists?(block_id: block_id)
|
310
|
+
|
311
|
+
errors.add(:block, "does not exist in variant #{line_item.variant_id}")
|
312
|
+
end
|
313
|
+
|
314
|
+
def preload_order_block_ids
|
315
|
+
return if line_item.blank?
|
316
|
+
return if line_item.order.blank?
|
317
|
+
|
318
|
+
block_ids = line_item.order.blocks.pluck(:id)
|
319
|
+
line_item.order.update(preload_block_ids: block_ids)
|
320
|
+
end
|
286
321
|
end
|
287
322
|
end
|
288
323
|
# rubocop:enable Metrics/ClassLength
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
class Inventory
|
3
|
+
include ActiveModel::Model
|
4
|
+
|
5
|
+
attr_accessor :variant_id, :inventory_date, :quantity_available, :max_capacity, :product_type
|
6
|
+
|
7
|
+
validates :variant_id, presence: true
|
8
|
+
validates :quantity_available, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
|
9
|
+
validates :max_capacity, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
|
10
|
+
end
|
11
|
+
end
|