spree_cm_commissioner 2.13.7 → 2.14.0
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/Gemfile.lock +1 -1
- data/app/assets/javascripts/spree_cm_commissioner/backend.js +0 -1
- data/app/assets/stylesheets/spree_cm_commissioner/backend/calendar.scss +0 -39
- data/app/controllers/concerns/spree/admin/rates_availability_cells.rb +16 -0
- data/app/controllers/concerns/spree/api/v2/product_list_includes_decorator.rb +1 -0
- data/app/controllers/spree/admin/guests_controller.rb +2 -1
- data/app/controllers/spree/admin/integrations_controller.rb +16 -7
- data/app/controllers/spree/admin/inventory_items_controller.rb +33 -62
- data/app/controllers/spree/admin/product_option_types_controller.rb +88 -0
- data/app/controllers/spree/admin/rate_plans_controller.rb +125 -0
- data/app/controllers/spree/admin/rates_availabilities_controller.rb +142 -0
- data/app/controllers/spree/admin/stock_managements_controller.rb +1 -53
- data/app/controllers/spree/api/switch_cm/base_controller.rb +109 -0
- data/app/controllers/spree/api/switch_cm/tokens_controller.rb +68 -0
- data/app/controllers/spree/api/switch_cm/xml/availabilities_controller.rb +39 -0
- data/app/controllers/spree/api/switch_cm/xml/base_controller.rb +37 -0
- data/app/controllers/spree/api/switch_cm/xml/reservation_summaries_controller.rb +28 -0
- data/app/controllers/spree/api/switch_cm/xml/reservations_controller.rb +57 -0
- data/app/controllers/spree/api/switch_cm/xml/room_rate_availabilities_controller.rb +56 -0
- data/app/controllers/spree/api/switch_cm/xml/room_rates_controller.rb +45 -0
- data/app/controllers/spree/api/v2/operator/guests_controller.rb +1 -1
- data/app/controllers/spree/api/v2/storefront/accommodations/variants_controller.rb +31 -5
- data/app/controllers/spree/api/v2/storefront/inventory_items_controller.rb +2 -2
- data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +0 -1
- data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +1 -1
- data/app/finders/spree_cm_commissioner/accommodations/find.rb +10 -8
- data/app/finders/spree_cm_commissioner/accommodations/find_variant.rb +5 -11
- data/app/finders/spree_cm_commissioner/rates_availabilities/find_bookings.rb +116 -0
- data/app/helpers/spree_cm_commissioner/admin/inventory_items_helper.rb +36 -0
- data/app/helpers/spree_cm_commissioner/admin/rates_availabilities_helper.rb +247 -0
- data/app/interactors/spree_cm_commissioner/intercity_taxi_order_creator.rb +0 -1
- data/app/jobs/spree_cm_commissioner/option_type_variants_public_metadata_updater_job.rb +1 -0
- data/app/jobs/spree_cm_commissioner/option_value_variants_public_metadata_updater_job.rb +1 -0
- data/app/jobs/spree_cm_commissioner/product_variants_public_metadata_updater_job.rb +12 -0
- data/app/models/concerns/spree_cm_commissioner/kyc_bitwise.rb +4 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +1 -10
- data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +33 -3
- data/app/models/concerns/spree_cm_commissioner/line_item_rate_plannable.rb +55 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +0 -16
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +22 -4
- data/app/models/concerns/spree_cm_commissioner/rate_plan_options_concern.rb +62 -0
- data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +12 -0
- data/app/models/concerns/spree_cm_commissioner/variant_rate_plannable.rb +81 -0
- data/app/models/spree_cm_commissioner/address_decorator.rb +2 -0
- data/app/models/spree_cm_commissioner/configuration.rb +0 -6
- data/app/models/spree_cm_commissioner/exports/import_order_guests_csv.rb +2 -1
- data/app/models/spree_cm_commissioner/exports/operator_guest_json_gzip.rb +8 -2
- data/app/models/spree_cm_commissioner/guest.rb +1 -1
- data/app/models/spree_cm_commissioner/integration.rb +15 -0
- data/app/models/spree_cm_commissioner/integrations/switch_cm.rb +96 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +32 -3
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +7 -7
- data/app/models/spree_cm_commissioner/maintenance_tasks/cache_invalidation.rb +20 -4
- data/app/models/spree_cm_commissioner/maintenance_tasks/event.rb +3 -2
- data/app/models/spree_cm_commissioner/option_type_decorator.rb +2 -1
- data/app/models/spree_cm_commissioner/option_value_decorator.rb +3 -0
- data/app/models/spree_cm_commissioner/option_value_rate_plan.rb +12 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/price_decorator.rb +13 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +18 -2
- data/app/models/spree_cm_commissioner/product_option_type_decorator.rb +42 -0
- data/app/models/spree_cm_commissioner/rate_plan.rb +139 -0
- data/app/models/spree_cm_commissioner/rate_plan_options.rb +36 -0
- data/app/models/spree_cm_commissioner/rate_restriction.rb +45 -0
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +5 -2
- data/app/models/spree_cm_commissioner/variant_decorator.rb +36 -5
- data/app/models/spree_cm_commissioner/variant_options.rb +3 -2
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
- data/app/overrides/spree/admin/option_types/_form/checkbox.html.erb.deface +0 -8
- data/app/overrides/spree/admin/option_types/_form/name.html.erb.deface +1 -1
- data/app/overrides/spree/admin/products/_filters/vendor_filter.html.erb.deface +6 -0
- data/app/overrides/spree/admin/shared/_product_tabs/rates_availabilities.html.erb.deface +8 -0
- data/app/overrides/spree/admin/shared/_product_tabs/stock_managements.html.erb.deface +1 -1
- data/app/overrides/spree/admin/variants/index/discontinued_badge.html.erb.deface +13 -0
- data/app/overrides/spree/admin/variants/index/option_types_empty_state_link.html.erb.deface +3 -0
- data/app/overrides/spree/admin/variants/index/option_types_summary.html.erb.deface +24 -0
- data/app/presenters/spree/variants/visible_options_presenter.rb +34 -1
- data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +40 -22
- data/app/queries/spree_cm_commissioner/single_leg_trips_query.rb +28 -14
- data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_update_schema.rb +0 -1
- data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +2 -0
- data/app/serializers/spree/v2/storefront/option_type_serializer_decorator.rb +1 -1
- data/app/serializers/spree/v2/storefront/product_option_type_serializer.rb +15 -0
- data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +15 -0
- data/app/serializers/spree/v2/tenant/line_item_serializer.rb +1 -1
- data/app/serializers/spree/v2/tenant/option_type_serializer.rb +1 -1
- data/app/serializers/spree_cm_commissioner/v2/operator/dynamic_field_option_serializer.rb +9 -0
- data/app/serializers/spree_cm_commissioner/v2/operator/dynamic_field_serializer.rb +11 -0
- data/app/serializers/spree_cm_commissioner/v2/operator/guest_dynamic_field_serializer.rb +14 -0
- data/app/serializers/spree_cm_commissioner/v2/operator/guest_serializer.rb +1 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/accommodation_variant_serializer.rb +44 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/inventory_item_serializer.rb +4 -4
- data/app/serializers/spree_cm_commissioner/v2/storefront/rate_plan_serializer.rb +16 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_item_availability.rb +1 -1
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_item_availability.rb +1 -1
- data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/accept_push.rb +111 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/accommodation_lock.rb +43 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/apply_updates.rb +133 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/availability_snapshot.rb +127 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/inventory_update.rb +35 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/pushed_nights.rb +82 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/unseen_room_nights.rb +49 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/validate_updates.rb +153 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/reservations/feed.rb +209 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/reservations/mark_seen.rb +62 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/reservations/reservation.rb +171 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/room_catalog.rb +88 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_manager.rb +16 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/full_sync_strategy.rb +39 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/incremental_sync_strategy.rb +51 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/availability_parser.rb +103 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/push_response.rb +45 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/reservation_serializer.rb +83 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/response_builder.rb +19 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_item_availability.rb +1 -1
- data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +1 -3
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_by_variant.rb +4 -0
- data/app/services/spree_cm_commissioner/inventory_items/reset.rb +4 -0
- data/app/services/spree_cm_commissioner/rate_plans/save.rb +65 -0
- data/app/services/spree_cm_commissioner/rates_availabilities/bulk_update.rb +225 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/split_line_item.rb +5 -0
- data/app/services/spree_cm_commissioner/room_types/create.rb +87 -0
- data/app/services/spree_cm_commissioner/room_types/update_properties.rb +32 -0
- data/app/services/spree_cm_commissioner/room_types.rb +40 -0
- data/app/services/spree_cm_commissioner/route_prices/update_price_range.rb +3 -1
- data/app/services/spree_cm_commissioner/trips/search.rb +14 -7
- data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +5 -1
- data/app/views/spree/admin/guests/_check_in_status.html.erb +28 -22
- data/app/views/spree/admin/guests/edit.html.erb +29 -26
- data/app/views/spree/admin/integrations/_form.html.erb +4 -0
- data/app/views/spree/admin/integrations/_switch_cm_fields.html.erb +53 -0
- data/app/views/spree/admin/inventory_items/_details.html.erb +56 -0
- data/app/views/spree/admin/product_option_types/index.html.erb +79 -0
- data/app/views/spree/admin/rate_plans/_form.html.erb +134 -0
- data/app/views/spree/admin/rate_plans/_order_form.html.erb +37 -0
- data/app/views/spree/admin/rates_availabilities/_block.html.erb +34 -0
- data/app/views/spree/admin/rates_availabilities/_bookings.html.erb +52 -0
- data/app/views/spree/admin/rates_availabilities/_filters.html.erb +48 -0
- data/app/views/spree/admin/rates_availabilities/_legend.html.erb +27 -0
- data/app/views/spree/admin/rates_availabilities/_list_pages.html.erb +14 -0
- data/app/views/spree/admin/rates_availabilities/_modal_header.html.erb +11 -0
- data/app/views/spree/admin/rates_availabilities/_names.html.erb +62 -0
- data/app/views/spree/admin/rates_availabilities/_script.html.erb +676 -0
- data/app/views/spree/admin/rates_availabilities/_styles.html.erb +159 -0
- data/app/views/spree/admin/rates_availabilities/index.html.erb +112 -0
- data/app/views/spree/admin/shared/sub_menu/_homepage.html.erb +0 -1
- data/app/views/spree/admin/stock_managements/_header.html.erb +0 -14
- data/app/views/spree/admin/stock_managements/_inventory_item_stats.html.erb +28 -0
- data/app/views/spree/admin/stock_managements/index.html.erb +1 -56
- data/config/initializers/spree_permitted_attributes.rb +1 -0
- data/config/locales/en.yml +241 -3
- data/config/locales/km.yml +241 -4
- data/config/routes.rb +36 -17
- data/db/migrate/20260928000000_add_oauth_application_id_to_cm_integrations.rb +5 -0
- data/db/migrate/20260929000000_create_cm_rate_plans.rb +22 -0
- data/db/migrate/20260929000001_add_rate_plan_to_spree_prices.rb +24 -0
- data/db/migrate/20260929000002_add_rate_plan_to_spree_line_items.rb +7 -0
- data/db/migrate/20260929000003_create_cm_option_value_rate_plans.rb +16 -0
- data/db/migrate/20261009000000_create_cm_rate_restrictions.rb +29 -0
- data/db/migrate/20261013000001_add_hidden_and_presentation_to_spree_product_option_types.rb +6 -0
- data/db/migrate/20261013000002_move_hidden_from_spree_option_types_to_product_option_types.rb +36 -0
- data/db/migrate/20261016000001_add_index_to_spree_line_items_number.rb +8 -0
- data/lib/spree_cm_commissioner/booking_dates.rb +43 -0
- data/lib/spree_cm_commissioner/calendar_event.rb +0 -25
- data/lib/spree_cm_commissioner/rate.rb +74 -0
- data/lib/spree_cm_commissioner/rate_closings.rb +48 -0
- data/lib/spree_cm_commissioner/rates_availability_grid.rb +113 -0
- data/lib/spree_cm_commissioner/stay_price.rb +30 -0
- data/lib/spree_cm_commissioner/stay_prices.rb +58 -0
- data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +6 -0
- data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +6 -0
- data/lib/spree_cm_commissioner/test_helper/factories/option_value_rate_plan_factory.rb +6 -0
- data/lib/spree_cm_commissioner/test_helper/factories/rate_plan_factory.rb +14 -0
- data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +9 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +6 -0
- metadata +94 -32
- data/app/assets/javascripts/spree_cm_commissioner/calendar.js +0 -59
- data/app/controllers/spree/admin/calendars/base_controller.rb +0 -27
- data/app/controllers/spree/admin/calendars/orders_controller.rb +0 -27
- data/app/controllers/spree/admin/homepage_feed_controller.rb +0 -36
- data/app/controllers/spree/admin/metafields_controller.rb +0 -69
- data/app/models/concerns/spree_cm_commissioner/metafield.rb +0 -15
- data/app/models/spree_cm_commissioner/homepage_feed.rb +0 -4
- data/app/overrides/spree/admin/shared/_main_menu/sidebar_calendars.html.erb.deface +0 -5
- data/app/overrides/spree/admin/shared/_product_tabs/metafields.html.erb.deface +0 -15
- data/app/serializers/spree_cm_commissioner/v2/storefront/taxon_include_product_serializer.rb +0 -13
- data/app/serializers/spree_cm_commissioner/v2/storefront/vendor_include_product_serializer.rb +0 -14
- data/app/services/spree_cm_commissioner/feed/taxon_include_product.rb +0 -20
- data/app/services/spree_cm_commissioner/feed/taxon_product.rb +0 -54
- data/app/services/spree_cm_commissioner/feed/vendor_include_product.rb +0 -20
- data/app/services/spree_cm_commissioner/feed/vendor_product.rb +0 -52
- data/app/services/spree_cm_commissioner/homepage_data_loader.rb +0 -112
- data/app/services/spree_cm_commissioner/metafields/product_metadata_service.rb +0 -32
- data/app/views/shared/_calendar.html.erb +0 -27
- data/app/views/shared/calendar/_order_popover.html.erb +0 -18
- data/app/views/shared/calendar/_orders_sidebar.html.erb +0 -41
- data/app/views/spree/admin/calendars/orders/index.html.erb +0 -22
- data/app/views/spree/admin/homepage_feed/_form.html.erb +0 -24
- data/app/views/spree/admin/homepage_feed/edit.html.erb +0 -17
- data/app/views/spree/admin/inventory_items/prices.html.erb +0 -43
- data/app/views/spree/admin/inventory_items/stocks.html.erb +0 -34
- data/app/views/spree/admin/metafields/_private_metafields_form.html.erb +0 -71
- data/app/views/spree/admin/metafields/_public_metafields_form.html.erb +0 -64
- data/app/views/spree/admin/metafields/edit.html.erb +0 -27
- data/app/views/spree/admin/shared/sub_menu/_calendar.html.erb +0 -3
- data/app/views/spree/admin/stock_managements/calendar.html.erb +0 -310
|
@@ -31,8 +31,6 @@ module SpreeCmCommissioner
|
|
|
31
31
|
|
|
32
32
|
validate :validate_trip_stops_for_scheduled_trips
|
|
33
33
|
|
|
34
|
-
validates :passenger_count, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
|
35
|
-
|
|
36
34
|
# Groups LineItems by trip fields. Some fields can be null,
|
|
37
35
|
# so use it with caution. Output:
|
|
38
36
|
# {
|
|
@@ -62,8 +60,6 @@ module SpreeCmCommissioner
|
|
|
62
60
|
def drop_off_trip_stop_id = private_metadata['drop_off_trip_stop_id']&.to_i
|
|
63
61
|
def service_origin_id = private_metadata['service_origin_id']&.to_i
|
|
64
62
|
|
|
65
|
-
def passenger_count = public_metadata['passenger_count']&.to_i
|
|
66
|
-
|
|
67
63
|
def pickup_map_place
|
|
68
64
|
pickup_hash = public_metadata&.dig('pickup_map_place')
|
|
69
65
|
return nil if pickup_hash.blank?
|
|
@@ -121,10 +117,6 @@ module SpreeCmCommissioner
|
|
|
121
117
|
set_private_metadata_value('service_origin_id', value)
|
|
122
118
|
end
|
|
123
119
|
|
|
124
|
-
def passenger_count=(value)
|
|
125
|
-
set_public_metadata_value('passenger_count', value)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
120
|
private
|
|
129
121
|
|
|
130
122
|
def resolve_service_origin_id
|
|
@@ -153,14 +145,6 @@ module SpreeCmCommissioner
|
|
|
153
145
|
end
|
|
154
146
|
end
|
|
155
147
|
|
|
156
|
-
def set_public_metadata_value(key, value)
|
|
157
|
-
if value.nil?
|
|
158
|
-
public_metadata.delete(key)
|
|
159
|
-
else
|
|
160
|
-
public_metadata[key] = value.to_s
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
148
|
def set_private_metadata_value(key, value)
|
|
165
149
|
if value.nil?
|
|
166
150
|
private_metadata.delete(key)
|
|
@@ -33,6 +33,7 @@ module SpreeCmCommissioner
|
|
|
33
33
|
'month' => 'integer',
|
|
34
34
|
'number-of-adults' => 'integer',
|
|
35
35
|
'number-of-kids' => 'integer',
|
|
36
|
+
'number-of-guests' => 'integer',
|
|
36
37
|
'kids-age-max' => 'integer',
|
|
37
38
|
'allowed-extra-adults' => 'integer',
|
|
38
39
|
'allowed-extra-kids' => 'integer',
|
|
@@ -49,6 +50,13 @@ module SpreeCmCommissioner
|
|
|
49
50
|
'top-up-amount' => 'integer'
|
|
50
51
|
}.freeze
|
|
51
52
|
|
|
53
|
+
# System option types for rate plan terms. RatePlanOptions reads them by these names.
|
|
54
|
+
RATE_PLAN_RESERVED_OPTIONS = {
|
|
55
|
+
'meal-plan' => 'string',
|
|
56
|
+
'refundable' => 'boolean',
|
|
57
|
+
'free-cancellation-days' => 'integer'
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
52
60
|
included do
|
|
53
61
|
include SpreeCmCommissioner::ParameterizeName
|
|
54
62
|
|
|
@@ -59,6 +67,10 @@ module SpreeCmCommissioner
|
|
|
59
67
|
|
|
60
68
|
before_validation :set_reserved_options_attributes, if: :reserved_option?
|
|
61
69
|
|
|
70
|
+
# reserved_option? reads the saved name, so a new record never matches it; rate plan system types need
|
|
71
|
+
# their kind and attr_type from the start, since RatePlanOptions reads them by name.
|
|
72
|
+
before_validation :set_reserved_options_attributes, if: -> { new_record? && RATE_PLAN_RESERVED_OPTIONS.key?(name) }
|
|
73
|
+
|
|
62
74
|
after_save :sort_date_time_option_values, if: -> { attr_type == 'date' || attr_type == 'time' }
|
|
63
75
|
after_save :update_variants_metadata, if: :saved_change_to_name?
|
|
64
76
|
|
|
@@ -81,9 +93,10 @@ module SpreeCmCommissioner
|
|
|
81
93
|
end
|
|
82
94
|
|
|
83
95
|
def reserved_option?
|
|
84
|
-
|
|
96
|
+
reserved_names = RESERVED_OPTIONS.keys + RATE_PLAN_RESERVED_OPTIONS.keys
|
|
97
|
+
return name_was.in?(reserved_names) if name_changed?
|
|
85
98
|
|
|
86
|
-
name.in?(
|
|
99
|
+
name.in?(reserved_names)
|
|
87
100
|
end
|
|
88
101
|
|
|
89
102
|
def ticket_type?
|
|
@@ -91,8 +104,13 @@ module SpreeCmCommissioner
|
|
|
91
104
|
end
|
|
92
105
|
|
|
93
106
|
def set_reserved_options_attributes
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
if RATE_PLAN_RESERVED_OPTIONS.key?(name)
|
|
108
|
+
self.attr_type = RATE_PLAN_RESERVED_OPTIONS[name]
|
|
109
|
+
self.kind = :rate_plan
|
|
110
|
+
else
|
|
111
|
+
self.attr_type = RESERVED_OPTIONS[name]
|
|
112
|
+
self.kind = :variant
|
|
113
|
+
end
|
|
96
114
|
end
|
|
97
115
|
|
|
98
116
|
def sort_date_time_option_values
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
# Rate plan terms are option values (source of truth) cached as {option_type_name => option_value_name} in
|
|
3
|
+
# the `options` column. Follows VariantOptionsConcern, which caches in public_metadata only because
|
|
4
|
+
# spree_variants is a core table; cm_rate_plans is ours, so it gets a column.
|
|
5
|
+
module RatePlanOptionsConcern
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
# Terms beyond price, e.g. Meal plan: Breakfast, from option types of the `rate_plan` kind.
|
|
10
|
+
has_many :option_value_rate_plans, class_name: 'SpreeCmCommissioner::OptionValueRatePlan', inverse_of: :rate_plan, dependent: :destroy
|
|
11
|
+
has_many :option_values, through: :option_value_rate_plans, class_name: 'Spree::OptionValue'
|
|
12
|
+
|
|
13
|
+
validate :option_values_are_rate_plan_kind
|
|
14
|
+
validate :one_option_value_per_option_type
|
|
15
|
+
validate :free_cancellation_only_when_refundable
|
|
16
|
+
|
|
17
|
+
# Before validation, so validations can read the typed cache instead of re-deriving it from option_values.
|
|
18
|
+
before_validation :cache_options
|
|
19
|
+
|
|
20
|
+
delegate :meal_plan, :refundable?, :free_cancellation_days, to: :options
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Read as RatePlanOptions, stored as a hash, like Route#route_stops.
|
|
24
|
+
def options
|
|
25
|
+
RatePlanOptions.from_hash(read_attribute(:options))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def options=(value)
|
|
29
|
+
self[:options] = value.to_h
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cache_options
|
|
33
|
+
self.options = option_values.each_with_object({}) do |option_value, hash|
|
|
34
|
+
hash[option_value.option_type.name] = option_value.name
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Reloads option values first: callers like OptionValueRatePlan may hold an instance loaded before the change.
|
|
39
|
+
def cache_options!
|
|
40
|
+
option_values.reset
|
|
41
|
+
cache_options
|
|
42
|
+
save!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def option_values_are_rate_plan_kind
|
|
48
|
+
errors.add(:option_values, :invalid_kind) unless option_values.all? { |value| value.option_type&.rate_plan? }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A rate plan can't be both "Room only" and "Breakfast".
|
|
52
|
+
def one_option_value_per_option_type
|
|
53
|
+
errors.add(:option_values, :duplicate_option_type) if option_values.map(&:option_type_id).uniq.size != option_values.size
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def free_cancellation_only_when_refundable
|
|
57
|
+
return if refundable? || free_cancellation_days.nil?
|
|
58
|
+
|
|
59
|
+
errors.add(:option_values, :free_cancellation_not_refundable)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -2,9 +2,21 @@ module SpreeCmCommissioner
|
|
|
2
2
|
module VariantOptionsConcern
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
|
+
# VariantOptions#number_of_guests in SQL (explicit number-of-guests, else adults defaulting
|
|
6
|
+
# to 1 plus kids defaulting to 0), so search matches the capacity the variant reports.
|
|
7
|
+
GUEST_CAPACITY_SQL = <<~SQL.squish.freeze
|
|
8
|
+
COALESCE(
|
|
9
|
+
CAST(spree_variants.public_metadata->'cm_options'->>'number-of-guests' AS INTEGER),
|
|
10
|
+
COALESCE(CAST(spree_variants.public_metadata->'cm_options'->>'number-of-adults' AS INTEGER), #{VariantOptions::DEFAULT_NUMBER_OF_ADULTS}) +
|
|
11
|
+
COALESCE(CAST(spree_variants.public_metadata->'cm_options'->>'number-of-kids' AS INTEGER), 0)
|
|
12
|
+
)
|
|
13
|
+
SQL
|
|
14
|
+
|
|
5
15
|
included do
|
|
6
16
|
before_save :set_options_to_public_metadata
|
|
7
17
|
|
|
18
|
+
scope :with_guest_capacity, -> (number_of_guests) { where("#{GUEST_CAPACITY_SQL} >= ?", number_of_guests) }
|
|
19
|
+
|
|
8
20
|
delegate :location,
|
|
9
21
|
:start_date,
|
|
10
22
|
:start_time,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
# The base price lookups (price_in, default_price, price_for_date) override Spree's, so they stay in VariantDecorator.
|
|
3
|
+
module VariantRatePlannable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
attr_writer :preloaded_stay_prices, :preloaded_rate_closings
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
has_many :active_rate_plans, -> { active.order(:position) }, class_name: 'SpreeCmCommissioner::RatePlan', inverse_of: :variant
|
|
10
|
+
has_many :rate_plans, -> { order(:position) }, class_name: 'SpreeCmCommissioner::RatePlan', inverse_of: :variant, dependent: :destroy
|
|
11
|
+
|
|
12
|
+
# Variants with a rate open on every date, by the rule #rates lists them with: the base rate when there's no
|
|
13
|
+
# active rate plan, else any active one (a derived one also needs the base rate open, RateClosings). Rate-level
|
|
14
|
+
# only; bookable_on_every_date checks the room-nights themselves.
|
|
15
|
+
scope :with_open_rate_on_every_date, lambda { |dates|
|
|
16
|
+
closed = lambda do |rate_condition|
|
|
17
|
+
SpreeCmCommissioner::RateRestriction
|
|
18
|
+
.joins(:inventory_item)
|
|
19
|
+
.where(closed: true, cm_inventory_items: { inventory_date: dates })
|
|
20
|
+
.where('cm_inventory_items.variant_id = spree_variants.id')
|
|
21
|
+
.where(rate_condition)
|
|
22
|
+
.arel.exists
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
active_rate_plans = SpreeCmCommissioner::RatePlan.active.where('cm_rate_plans.variant_id = spree_variants.id')
|
|
26
|
+
base_open = active_rate_plans.arel.exists.not.and(closed.call('cm_rate_restrictions.rate_plan_id IS NULL').not)
|
|
27
|
+
own_or_followed = [
|
|
28
|
+
'cm_rate_restrictions.rate_plan_id = cm_rate_plans.id OR (cm_rate_plans.kind = ? AND cm_rate_restrictions.rate_plan_id IS NULL)',
|
|
29
|
+
SpreeCmCommissioner::RatePlan.kinds[:derived]
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
rate_plan_open = active_rate_plans.where(closed.call(own_or_followed).not).arel.exists
|
|
33
|
+
where(base_open.or(rate_plan_open))
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class_methods do
|
|
38
|
+
# Every variant gets the same batch: one per variant would be a price query per room. Each one only reads its
|
|
39
|
+
# own [variant_id, rate_plan_id] keys, so it never sees another room's prices. Expects active_rate_plans
|
|
40
|
+
# preloaded, or this adds a query per variant.
|
|
41
|
+
def preload_stay_prices(variants, dates:, currency:)
|
|
42
|
+
stay_prices = SpreeCmCommissioner::StayPrices.new(
|
|
43
|
+
variants: variants,
|
|
44
|
+
rate_plans: variants.flat_map(&:active_rate_plans),
|
|
45
|
+
dates: dates,
|
|
46
|
+
currency: currency
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
rate_closings = SpreeCmCommissioner::RateClosings.new(
|
|
50
|
+
variant_ids: variants.map(&:id),
|
|
51
|
+
dates: dates
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
variants.each do |variant|
|
|
55
|
+
variant.preloaded_stay_prices = stay_prices
|
|
56
|
+
variant.preloaded_rate_closings = rate_closings
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The default rate (the variant's own prices) only when there's no active rate plan. A hotel that also wants
|
|
62
|
+
# to sell at the plain price adds a 0% rate plan. Rates closed on any of the dates are left out.
|
|
63
|
+
def rates(dates:, currency:)
|
|
64
|
+
stay_prices = @preloaded_stay_prices if @preloaded_stay_prices&.matches?(dates: dates, currency: currency)
|
|
65
|
+
stay_prices ||= SpreeCmCommissioner::StayPrices.new(variants: [self], rate_plans: active_rate_plans, dates: dates, currency: currency)
|
|
66
|
+
|
|
67
|
+
rate_closings = @preloaded_rate_closings if @preloaded_rate_closings&.matches?(dates: dates)
|
|
68
|
+
rate_closings ||= SpreeCmCommissioner::RateClosings.new(variant_ids: [id], dates: dates)
|
|
69
|
+
|
|
70
|
+
(active_rate_plans.presence || [nil]).filter_map do |rate_plan|
|
|
71
|
+
next if rate_closings.closed?(id, rate_plan)
|
|
72
|
+
|
|
73
|
+
SpreeCmCommissioner::Rate.new(variant: self, rate_plan: rate_plan, dates: dates, currency: currency, stay_prices: stay_prices)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def stay_price(dates:, currency:)
|
|
78
|
+
SpreeCmCommissioner::StayPrices.new(variants: [self], rate_plans: [], dates: dates, currency: currency).for_variant(self)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -6,6 +6,8 @@ module SpreeCmCommissioner
|
|
|
6
6
|
base.validates :address1, presence: true, if: :require_address1?
|
|
7
7
|
base.validates :city, presence: true, if: :require_city?
|
|
8
8
|
base.validates :country, presence: true, if: :require_country?
|
|
9
|
+
# Country is optional, so redefine Spree's country delegation to return nil instead of raising.
|
|
10
|
+
base.delegate :name, :iso3, :iso, :iso_name, to: :country, prefix: true, allow_nil: true
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
# override to return false instead of true
|
|
@@ -8,12 +8,6 @@ module SpreeCmCommissioner
|
|
|
8
8
|
# preference :favorite_number, :integer
|
|
9
9
|
# preference :supported_locales, :array, default: [:en]
|
|
10
10
|
preference :enabled, :boolean, default: true
|
|
11
|
-
preference :displayed_product_taxon_ids, :array, default: []
|
|
12
|
-
preference :top_category_taxon_ids, :array, default: []
|
|
13
|
-
preference :trending_category_taxon_ids, :array, default: []
|
|
14
|
-
preference :featured_brand_taxon_ids, :array, default: []
|
|
15
|
-
preference :featured_vendor_ids, :array, default: []
|
|
16
|
-
preference :cleared_cache_at, :string
|
|
17
11
|
preference :prepaid_topup_min_amount, :decimal, default: 1
|
|
18
12
|
preference :prepaid_topup_max_amount, :decimal, default: 500
|
|
19
13
|
|
|
@@ -19,7 +19,7 @@ module SpreeCmCommissioner
|
|
|
19
19
|
# Lets Excel detect UTF-8 so Khmer names render correctly.
|
|
20
20
|
UTF8_BOM = "\uFEFF".freeze
|
|
21
21
|
ORDER_BATCH_SIZE = 200
|
|
22
|
-
HEADERS = ['Guest', 'Phone Number', 'Order ID', 'Order Status', 'Ticket', 'Order Ticket', 'Guest Ticket'].freeze
|
|
22
|
+
HEADERS = ['Guest', 'Phone Number', 'Order ID', 'Order Status', 'Ticket', 'QR Data', 'Order Ticket', 'Guest Ticket'].freeze
|
|
23
23
|
|
|
24
24
|
store_public_metadata :guests_count, :integer, default: 0
|
|
25
25
|
store_private_metadata :exported_by_id, :integer
|
|
@@ -85,6 +85,7 @@ module SpreeCmCommissioner
|
|
|
85
85
|
order.number,
|
|
86
86
|
order.state&.capitalize,
|
|
87
87
|
line_item.name,
|
|
88
|
+
order.qr_data,
|
|
88
89
|
app_url_helpers.order_page_url(order.qr_data, **app_url_options),
|
|
89
90
|
guest_ticket_link(guest, line_item)
|
|
90
91
|
]
|
|
@@ -3,7 +3,9 @@ module SpreeCmCommissioner
|
|
|
3
3
|
class OperatorGuestJsonGzip < ::SpreeCmCommissioner::Export
|
|
4
4
|
BATCH_INCLUDES = [
|
|
5
5
|
:occupation, :user,
|
|
6
|
-
{ check_ins: %i[check_in_by check_in_session], line_item: [:variant, { order: :bill_address }]
|
|
6
|
+
{ check_ins: %i[check_in_by check_in_session], line_item: [:variant, { order: :bill_address }],
|
|
7
|
+
guest_dynamic_fields: [:dynamic_field_option, { dynamic_field: :dynamic_field_options }]
|
|
8
|
+
}
|
|
7
9
|
].freeze
|
|
8
10
|
|
|
9
11
|
RESOURCE_INCLUDES = [
|
|
@@ -14,7 +16,11 @@ module SpreeCmCommissioner
|
|
|
14
16
|
'line_item',
|
|
15
17
|
'line_item.order',
|
|
16
18
|
'line_item.order.billing_address',
|
|
17
|
-
'line_item.variant'
|
|
19
|
+
'line_item.variant',
|
|
20
|
+
'guest_dynamic_fields',
|
|
21
|
+
'guest_dynamic_fields.dynamic_field_option',
|
|
22
|
+
'guest_dynamic_fields.dynamic_field',
|
|
23
|
+
'guest_dynamic_fields.dynamic_field.dynamic_field_options'
|
|
18
24
|
].freeze
|
|
19
25
|
|
|
20
26
|
# Check-in group scope: one role, one export. `role` is the requesting operator's check-in group
|
|
@@ -83,7 +83,7 @@ module SpreeCmCommissioner
|
|
|
83
83
|
self.whitelisted_ransackable_attributes = %w[first_name last_name phone_number gender contact occupation_id card_type created_at check_in_status]
|
|
84
84
|
|
|
85
85
|
scope :with_dynamic_fields_and_options, lambda {
|
|
86
|
-
includes(guest_dynamic_fields: { dynamic_field: :dynamic_field_options })
|
|
86
|
+
includes(guest_dynamic_fields: [:dynamic_field_option, { dynamic_field: :dynamic_field_options }])
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
def self.csv_importable_columns
|
|
@@ -14,10 +14,25 @@ module SpreeCmCommissioner
|
|
|
14
14
|
validates :incremental_sync_interval_seconds, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 3600 }
|
|
15
15
|
validates :full_sync_interval_hours, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 168 }
|
|
16
16
|
|
|
17
|
+
# Listed from the folder rather than `descendants`, which misses subclasses Zeitwerk hasn't
|
|
18
|
+
# loaded yet in dev and test.
|
|
19
|
+
def self.types
|
|
20
|
+
Dir[SpreeCmCommissioner::Engine.root.join('app/models/spree_cm_commissioner/integrations/*.rb')]
|
|
21
|
+
.map { |path| "SpreeCmCommissioner::Integrations::#{File.basename(path, '.rb').camelize}" }
|
|
22
|
+
.sort
|
|
23
|
+
end
|
|
24
|
+
|
|
17
25
|
def sync_manager
|
|
18
26
|
raise NotImplementedError, 'Subclasses must implement the sync_manager method'
|
|
19
27
|
end
|
|
20
28
|
|
|
29
|
+
# Whether the partner, not BookMe+, sets this variant's per-night stock, prices and closings.
|
|
30
|
+
# When true, stock resets and dashboard edits leave its inventory items alone, and new nights
|
|
31
|
+
# start closed until the partner opens them (InventoryItem). Off by default.
|
|
32
|
+
def inventory_external_wins?(_variant)
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
21
36
|
def restock_external_inventory!(_order, _line_items)
|
|
22
37
|
raise NotImplementedError, 'Subclasses must implement the restock_external_inventory! method'
|
|
23
38
|
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# A hotel connected to switch.cm, which calls our Booking.com-compatible channel-manager API
|
|
2
|
+
# (Spree::Api::SwitchCm) instead of us calling it: it pushes availability, prices and
|
|
3
|
+
# restrictions, and pulls reservations. So there's nothing to pull, hold or unstock externally;
|
|
4
|
+
# polling only runs our own checks (SyncManager).
|
|
5
|
+
#
|
|
6
|
+
# Every switch.cm row shares one Doorkeeper app; the hotels switch.cm may manage are the rows
|
|
7
|
+
# pointing at it. `paused` is the setup state: switch.cm can read the hotel (room list,
|
|
8
|
+
# reservation summary) while it keeps selling on its own counts. `active` with conflict strategy
|
|
9
|
+
# `external_wins` hands the rooms' availability, prices and restrictions over to switch.cm.
|
|
10
|
+
class SpreeCmCommissioner::Integrations::SwitchCm < SpreeCmCommissioner::Integration
|
|
11
|
+
READABLE_STATUSES = %i[active paused].freeze
|
|
12
|
+
|
|
13
|
+
belongs_to :oauth_application, class_name: 'Spree::OauthApplication', optional: false
|
|
14
|
+
|
|
15
|
+
# Reservations completed before this aren't in the reservation feeds: switch.cm imports them
|
|
16
|
+
# through the reservation summary while setting the hotel up.
|
|
17
|
+
store_public_metadata :connected_at, :datetime
|
|
18
|
+
alias stored_connected_at connected_at
|
|
19
|
+
|
|
20
|
+
before_create :set_connected_at
|
|
21
|
+
after_commit :sync_unpushed_nights, on: %i[create update]
|
|
22
|
+
|
|
23
|
+
# Polling runs the hotel's periodic checks (SyncManager), and the incremental one alerts on
|
|
24
|
+
# every run while reservations sit unpulled, so the column's 10-second default would flood
|
|
25
|
+
# Telegram. Full runs daily upkeep.
|
|
26
|
+
attribute :incremental_sync_interval_seconds, :integer, default: -> { SyncStrategies::IncrementalSyncStrategy::UNSEEN_AFTER.to_i }
|
|
27
|
+
|
|
28
|
+
scope :readable, -> { where(status: READABLE_STATUSES) }
|
|
29
|
+
|
|
30
|
+
# override
|
|
31
|
+
def sync_manager
|
|
32
|
+
SyncManager.new(integration: self)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Only for rooms switch.cm can see (RoomCatalog): a hotel's other products, and rooms that need
|
|
36
|
+
# confirmation, keep BookMe+ counts since switch.cm never pushes them.
|
|
37
|
+
# override
|
|
38
|
+
def inventory_external_wins?(variant)
|
|
39
|
+
owns_inventory? && variant.accommodation? && !variant.need_confirmation?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def owns_inventory?
|
|
43
|
+
active? && external_wins?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# switch.cm pulls reservations, so a BookMe+ sale needs nothing from it at checkout, and the
|
|
47
|
+
# hotel's own cancellation rules decide whether a booking can be cancelled.
|
|
48
|
+
# override
|
|
49
|
+
def unstock_external_inventory!(_order, _line_items); end
|
|
50
|
+
|
|
51
|
+
# override
|
|
52
|
+
def restock_external_inventory!(_order, _line_items); end
|
|
53
|
+
|
|
54
|
+
# override
|
|
55
|
+
def sync_item_availability!(_options = {}); end
|
|
56
|
+
|
|
57
|
+
# override
|
|
58
|
+
def external_inventory_items(_order)
|
|
59
|
+
[]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# override
|
|
63
|
+
def external_inventory_item_status(_order, _options)
|
|
64
|
+
{}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A row saved without this class's callbacks (e.g. created as the base Integration) has no stamp;
|
|
68
|
+
# nil would hide every reservation from the feeds and from UnseenRoomNights, so it falls back to
|
|
69
|
+
# the row's creation time, which is what the stamp would have been.
|
|
70
|
+
def connected_at
|
|
71
|
+
stored_connected_at || created_at
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# While switch.cm owns the rooms, nights it hasn't pushed stay closed, so the admin page shows
|
|
75
|
+
# this until the first full push lands.
|
|
76
|
+
def push_progress
|
|
77
|
+
Inventory::PushedNights.new(self).progress
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Checks the stored value, not #connected_at: Rails sets created_at before create callbacks run,
|
|
83
|
+
# so the fallback would already answer and the stamp would never be written.
|
|
84
|
+
def set_connected_at
|
|
85
|
+
self.connected_at = Time.current.iso8601 if stored_connected_at.nil?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Taking over closes the rooms' unpushed nights; letting go (paused, disconnected, or another
|
|
89
|
+
# conflict strategy) reopens them, while pushed nights keep switch.cm's last numbers.
|
|
90
|
+
def sync_unpushed_nights
|
|
91
|
+
owned_before = status_before_last_save.to_s == 'active' && conflict_strategy_before_last_save.to_s == 'external_wins'
|
|
92
|
+
return if owned_before == owns_inventory?
|
|
93
|
+
|
|
94
|
+
Inventory::PushedNights.new(self).reconcile_unpushed!
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -16,6 +16,11 @@ module SpreeCmCommissioner
|
|
|
16
16
|
|
|
17
17
|
has_many :prices, class_name: 'Spree::Price', inverse_of: :inventory_item, dependent: :destroy
|
|
18
18
|
|
|
19
|
+
# The variant's own prices for this date. A rate plan stores its date price on the same
|
|
20
|
+
# inventory item, so `prices` can hold several rows per currency; reading or writing through `prices`
|
|
21
|
+
# would show or overwrite a rate plan's price as the variant's. Rows built here get `rate_plan_id: nil`.
|
|
22
|
+
has_many :variant_prices, -> { where(rate_plan_id: nil) }, class_name: 'Spree::Price', inverse_of: :inventory_item
|
|
23
|
+
|
|
19
24
|
# Scoped to `unavailable` (not `reserved_or_on_hold`) so operator-locked seats are exposed
|
|
20
25
|
# everywhere a sold seat is — including the storefront API, which is what lets locked seats render
|
|
21
26
|
# as unavailable in the app with no client change.
|
|
@@ -27,6 +32,9 @@ module SpreeCmCommissioner
|
|
|
27
32
|
|
|
28
33
|
has_many :lock_movements, class_name: 'SpreeCmCommissioner::InventoryLockMovement', inverse_of: :inventory_item
|
|
29
34
|
|
|
35
|
+
# Closings of single rates on this date; closing every rate is `closed`.
|
|
36
|
+
has_many :rate_restrictions, class_name: 'SpreeCmCommissioner::RateRestriction', inverse_of: :inventory_item, dependent: :delete_all
|
|
37
|
+
|
|
30
38
|
# Validation
|
|
31
39
|
validates :quantity_available, numericality: { greater_than_or_equal_to: 0 }
|
|
32
40
|
validates :quantity_on_hold, numericality: { greater_than_or_equal_to: 0 }
|
|
@@ -34,13 +42,25 @@ module SpreeCmCommissioner
|
|
|
34
42
|
validates :quantity_locked_on_hold, numericality: { greater_than_or_equal_to: 0 }
|
|
35
43
|
validates :max_capacity, numericality: { greater_than_or_equal_to: 0 } # Originally inventory of each variant.
|
|
36
44
|
validates :inventory_date, presence: true, if: :permanent_stock?
|
|
37
|
-
|
|
45
|
+
# Create only: most saves just touch stock quantities, and the key below can't change afterwards. Still needed
|
|
46
|
+
# for NULL inventory_date rows, which the unique index does not cover.
|
|
47
|
+
validates :variant_id, uniqueness: { scope: :inventory_date, message: -> (object, _data) { "The variant is taken on #{object.inventory_date}" } },
|
|
48
|
+
on: :create
|
|
49
|
+
validate :variant_and_inventory_date_unchanged, on: :update
|
|
38
50
|
|
|
39
51
|
# Scope
|
|
40
52
|
scope :active, -> { where(inventory_date: nil).or(where('inventory_date >= ?', Time.zone.today)) }
|
|
41
53
|
|
|
54
|
+
# Same rule as CachedInventoryItem#sellable?, which booking checks: open for sale and not sold out.
|
|
55
|
+
scope :bookable_on, -> (dates) { where(inventory_date: dates, closed: false).where('quantity_available > 0') }
|
|
56
|
+
|
|
42
57
|
before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
|
|
43
58
|
|
|
59
|
+
# A night whose inventory a partner owns (Integration#inventory_external_wins?) starts closed: its
|
|
60
|
+
# count is only our seed from the physical room count, which the partner may already have sold
|
|
61
|
+
# elsewhere. The partner's first availability push opens it.
|
|
62
|
+
before_create -> { self.closed = true }, if: -> { permanent_stock? && variant&.inventory_external_wins? }
|
|
63
|
+
|
|
44
64
|
after_commit :schedule_product_cache_invalidation
|
|
45
65
|
after_commit :publish_live_stock_status_to_firestore
|
|
46
66
|
|
|
@@ -95,8 +115,10 @@ module SpreeCmCommissioner
|
|
|
95
115
|
available_before.to_i + locked_before.to_i
|
|
96
116
|
end
|
|
97
117
|
|
|
118
|
+
# nil above MAX_DISPLAY_STOCK: "3 left" helps a guest decide, "20 left" when 300 are free misleads. Apps read
|
|
119
|
+
# nil as plenty, not sold out.
|
|
98
120
|
def public_quantity_available
|
|
99
|
-
|
|
121
|
+
quantity_available if quantity_available <= MAX_DISPLAY_STOCK
|
|
100
122
|
end
|
|
101
123
|
|
|
102
124
|
# How many seats an operator has taken off sale for this date.
|
|
@@ -108,7 +130,7 @@ module SpreeCmCommissioner
|
|
|
108
130
|
end
|
|
109
131
|
|
|
110
132
|
def price_in(currency)
|
|
111
|
-
|
|
133
|
+
variant_prices.detect { |price| price.currency == currency } || variant_prices.build(currency: currency)
|
|
112
134
|
end
|
|
113
135
|
|
|
114
136
|
# This method is only used when admin update stock
|
|
@@ -194,5 +216,12 @@ module SpreeCmCommissioner
|
|
|
194
216
|
expires_at = [inventory_date.end_of_day + 7.days, 1.day.from_now].max
|
|
195
217
|
(expires_at - Time.zone.now).to_i
|
|
196
218
|
end
|
|
219
|
+
|
|
220
|
+
private
|
|
221
|
+
|
|
222
|
+
def variant_and_inventory_date_unchanged
|
|
223
|
+
errors.add(:variant_id, :cannot_be_changed) if will_save_change_to_variant_id?
|
|
224
|
+
errors.add(:inventory_date, :cannot_be_changed) if will_save_change_to_inventory_date?
|
|
225
|
+
end
|
|
197
226
|
end
|
|
198
227
|
end
|
|
@@ -95,6 +95,7 @@ module SpreeCmCommissioner
|
|
|
95
95
|
base.include SpreeCmCommissioner::ProductDelegation
|
|
96
96
|
base.include SpreeCmCommissioner::LineItemOpenDatedTrippable
|
|
97
97
|
base.include SpreeCmCommissioner::LineItemBundleable
|
|
98
|
+
base.include SpreeCmCommissioner::LineItemRatePlannable
|
|
98
99
|
base.include SpreeCmCommissioner::KycBitwise
|
|
99
100
|
end
|
|
100
101
|
|
|
@@ -118,21 +119,21 @@ module SpreeCmCommissioner
|
|
|
118
119
|
self.product_type = variant.product_type
|
|
119
120
|
self.from_date = opts.delete(:from_date)&.to_datetime
|
|
120
121
|
self.to_date = opts.delete(:to_date)&.to_datetime
|
|
122
|
+
self.rate_plan_id = opts.delete(:rate_plan_id).presence || rate_plan_id
|
|
121
123
|
|
|
122
124
|
base_price = variant.price_in(currency)
|
|
123
125
|
|
|
124
126
|
self.price = if permanent_stock?
|
|
125
127
|
date_range.map do |date|
|
|
126
|
-
|
|
127
|
-
(selected_price.amount || 0) + variant.price_modifier_amount_in(currency, opts.merge(date: date))
|
|
128
|
+
(unit_price_amount(currency, base_price, date: date) || 0) + variant.price_modifier_amount_in(currency, opts.merge(date: date))
|
|
128
129
|
end.sum
|
|
129
130
|
else
|
|
130
|
-
(base_price
|
|
131
|
+
(unit_price_amount(currency, base_price) || 0) + variant.price_modifier_amount_in(currency, opts)
|
|
131
132
|
end
|
|
132
133
|
end
|
|
133
134
|
|
|
134
135
|
# override to calculate price per date for permanent stock
|
|
135
|
-
def update_price
|
|
136
|
+
def update_price
|
|
136
137
|
# Skip price updates for ticket transfer orders to preserve custom transfer pricing
|
|
137
138
|
return if order&.ticket_transfer?
|
|
138
139
|
# prepaid_top_up line items carry a one-time custom_price set at creation (see
|
|
@@ -144,11 +145,10 @@ module SpreeCmCommissioner
|
|
|
144
145
|
base_price = variant.price_in(order.currency)
|
|
145
146
|
self.price = if permanent_stock?
|
|
146
147
|
date_range.map do |date|
|
|
147
|
-
|
|
148
|
-
selected_price.amount || 0
|
|
148
|
+
unit_price_amount(order.currency, base_price, date: date) || 0
|
|
149
149
|
end.sum
|
|
150
150
|
else
|
|
151
|
-
|
|
151
|
+
unit_price_amount(order.currency, base_price) || 0
|
|
152
152
|
end
|
|
153
153
|
end
|
|
154
154
|
|
|
@@ -3,13 +3,29 @@ module SpreeCmCommissioner
|
|
|
3
3
|
module MaintenanceTasks
|
|
4
4
|
class CacheInvalidation < MaintenanceTask
|
|
5
5
|
def maintain
|
|
6
|
-
|
|
6
|
+
if ENV['CACHE_AUTO_INVALIDATION_DISABLED'] == 'yes'
|
|
7
|
+
log_invalidation('skipped: CACHE_AUTO_INVALIDATION_DISABLED')
|
|
8
|
+
return
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
result = SpreeCmCommissioner::ApiCaches::Invalidate.call(model: maintainable.class.name, id: maintainable.id)
|
|
7
12
|
|
|
8
|
-
if
|
|
9
|
-
|
|
13
|
+
if result.success?
|
|
14
|
+
log_invalidation('sent', patterns: result.value[:api_patterns])
|
|
15
|
+
else
|
|
16
|
+
log_invalidation("failed: #{result.error}")
|
|
10
17
|
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
11
21
|
|
|
12
|
-
|
|
22
|
+
# Each path costs money on CloudFront, so every outcome is logged and kept on the task for troubleshooting.
|
|
23
|
+
def log_invalidation(outcome, patterns: [])
|
|
24
|
+
log_run("Invalidation #{outcome} #{patterns.join(', ')}".strip)
|
|
25
|
+
CmAppLogger.log(
|
|
26
|
+
label: "#{self.class.name} #{outcome}",
|
|
27
|
+
data: { maintenance_task_id: id, maintainable_type: maintainable_type, maintainable_id: maintainable_id, patterns: patterns }
|
|
28
|
+
)
|
|
13
29
|
end
|
|
14
30
|
end
|
|
15
31
|
end
|
|
@@ -26,7 +26,7 @@ module SpreeCmCommissioner
|
|
|
26
26
|
|
|
27
27
|
# Denormalises maintainable's price range onto EventMetadata#min_price/max_price/currency
|
|
28
28
|
# so the event-details bar can show "from $X" without loading the ticket list. Triggered
|
|
29
|
-
# by PriceDecorator's after_commit hook via a pending task
|
|
29
|
+
# by PriceDecorator's after_commit hook via a pending task. Only saves when the range moved.
|
|
30
30
|
def refresh_price_range
|
|
31
31
|
currency = Spree::Store.default.default_currency
|
|
32
32
|
|
|
@@ -35,11 +35,12 @@ module SpreeCmCommissioner
|
|
|
35
35
|
.where(variant_id: priced_variant_ids, currency: currency)
|
|
36
36
|
.pick(Arel.sql('MIN(amount)'), Arel.sql('MAX(amount)'))
|
|
37
37
|
|
|
38
|
-
maintainable.
|
|
38
|
+
maintainable.assign_attributes(
|
|
39
39
|
min_price: min_amount&.to_s,
|
|
40
40
|
max_price: max_amount&.to_s,
|
|
41
41
|
currency: min_amount.present? ? currency : nil
|
|
42
42
|
)
|
|
43
|
+
maintainable.save! if maintainable.changed?
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def update_conversion_for(product_taxon)
|