spree_cm_commissioner 2.13.7 → 2.14.1
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 +5 -3
- data/Gemfile.lock +1 -1
- data/app/assets/javascripts/spree_cm_commissioner/backend.js +1 -1
- data/app/assets/javascripts/spree_cm_commissioner/promotion_rule_pickers.js +12 -0
- 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/platform/homepage_section_relatable_options_controller.rb +1 -0
- 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/scoped_fee_rule.rb +1 -6
- 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 +23 -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/relatable_option_label.rb +20 -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 +16 -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/api/v2/platform/homepage_section_relatable_options_serializer.rb +2 -2
- 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/external_client/client.rb +11 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_items.rb +16 -3
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/unstock_inventory.rb +35 -5
- 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/book_me_bus_v1/polling/sync_locations.rb +39 -7
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_product.rb +1 -1
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_routes.rb +31 -13
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_seat_layout.rb +20 -2
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_service_calendars.rb +18 -10
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +7 -2
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_variant.rb +9 -2
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_vehicle_types.rb +12 -1
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/trip.rb +8 -1
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/trip_location.rb +32 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/full_sync_strategy.rb +2 -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 +94 -0
- data/app/services/spree_cm_commissioner/room_types/update_properties.rb +32 -0
- data/app/services/spree_cm_commissioner/room_types/update_room_variant.rb +22 -0
- data/app/services/spree_cm_commissioner/room_types.rb +49 -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/promotions/rules/_product.html.erb +25 -0
- data/app/views/spree/admin/promotions/rules/_user.html.erb +21 -0
- data/app/views/spree/admin/promotions/rules/_vendors.html.erb +14 -1
- 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 +246 -3
- data/config/locales/km.yml +246 -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 +100 -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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
# Stock, prices and sale status of a product's variants and their rate plans, per date.
|
|
4
|
+
# StockManagementsController keeps per-variant setup: stock locations, track inventory, purchases, resets.
|
|
5
|
+
class RatesAvailabilitiesController < Spree::Admin::BaseController
|
|
6
|
+
include RatesAvailabilityCells
|
|
7
|
+
|
|
8
|
+
MIN_DAYS_AHEAD = 14
|
|
9
|
+
MAX_BLOCK_DAYS = 62
|
|
10
|
+
VARIANTS_PER_PAGE = 25
|
|
11
|
+
BOOKINGS_PER_PAGE = 25
|
|
12
|
+
|
|
13
|
+
before_action :load_product
|
|
14
|
+
before_action :load_grid, only: %i[index dates]
|
|
15
|
+
|
|
16
|
+
helper SpreeCmCommissioner::Admin::RatesAvailabilitiesHelper
|
|
17
|
+
helper_method :filter_params
|
|
18
|
+
|
|
19
|
+
# GET /products/:product_id/rates_availabilities?from=¤cy=&option_value_ids[]=&archived=&page=
|
|
20
|
+
def index
|
|
21
|
+
@option_types = Spree::Variants::OptionTypesFinder.new(variant_ids: @product.variants.ids).execute.select(&:variant?)
|
|
22
|
+
@currencies = supported_currencies_for_all_stores.map(&:iso_code)
|
|
23
|
+
# A channel manager sets these variants' stock, prices and closings, so BulkUpdate skips them; the page says why.
|
|
24
|
+
@managing_integration = @product.vendor&.integration if @grid.variants.any?(&:inventory_external_wins?)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# GET /products/:product_id/rates_availabilities/dates?from=&to=&<index filters>
|
|
28
|
+
#
|
|
29
|
+
# The page adds this block next to the dates it already shows, so a range across months doesn't need a reload.
|
|
30
|
+
# It asks for whole months, so a month never spans two blocks.
|
|
31
|
+
def dates
|
|
32
|
+
render partial: 'block', locals: { grid: @grid }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# PATCH /products/:product_id/rates_availabilities/bulk_update, JSON: { bulk_action:, cells: [...], values: {...} }
|
|
36
|
+
#
|
|
37
|
+
# Answers with the counts, and the page then reloads its loaded dates.
|
|
38
|
+
def bulk_update
|
|
39
|
+
result = SpreeCmCommissioner::RatesAvailabilities::BulkUpdate.call(
|
|
40
|
+
product: @product,
|
|
41
|
+
store: current_store,
|
|
42
|
+
action: params[:bulk_action].to_s,
|
|
43
|
+
cells: selected_cells,
|
|
44
|
+
values: params.fetch(:values, {}).permit(:amount, :compare_at_amount, :quantity, :currency),
|
|
45
|
+
user: try_spree_current_user
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if result.success?
|
|
49
|
+
render json: result.value
|
|
50
|
+
else
|
|
51
|
+
render json: { error: result.error.to_s }, status: :unprocessable_entity
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# POST /products/:product_id/rates_availabilities/bookings, JSON: { cells: [...], filter:, q:, page: }
|
|
56
|
+
#
|
|
57
|
+
# The bookings modal for the selected cells. POST, as cells travel in the body.
|
|
58
|
+
def bookings
|
|
59
|
+
finder = SpreeCmCommissioner::RatesAvailabilities::FindBookings.new(
|
|
60
|
+
product: @product,
|
|
61
|
+
cells: selected_cells,
|
|
62
|
+
filter: params[:filter].to_s,
|
|
63
|
+
query: params[:q]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
line_items = finder.execute.page(params[:page]).per(BOOKINGS_PER_PAGE)
|
|
67
|
+
render partial: 'bookings', formats: [:html], locals: {
|
|
68
|
+
finder: finder,
|
|
69
|
+
line_items: line_items,
|
|
70
|
+
cells: selected_cells,
|
|
71
|
+
held_order_ids: finder.held_order_ids_among(line_items)
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def filter_params
|
|
78
|
+
params.permit(:currency, :archived, option_value_ids: []).to_h
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def load_product
|
|
82
|
+
@product = Spree::Product.find_by!(slug: params[:product_id])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def load_grid
|
|
86
|
+
@include_archived = params[:archived] == '1'
|
|
87
|
+
@variants = variants.page(params[:page]).per(VARIANTS_PER_PAGE)
|
|
88
|
+
@currency = (params[:currency].presence || current_currency).upcase
|
|
89
|
+
@from = parse_date(params[:from]) || Time.zone.today
|
|
90
|
+
|
|
91
|
+
dates = @product.permanent_stock? ? date_window.to_a : []
|
|
92
|
+
|
|
93
|
+
@grid = SpreeCmCommissioner::RatesAvailabilityGrid.new(
|
|
94
|
+
variants: @variants.to_a,
|
|
95
|
+
dates: dates,
|
|
96
|
+
currency: @currency,
|
|
97
|
+
include_archived: @include_archived
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def variants
|
|
102
|
+
rate_plans = @include_archived ? :rate_plans : :active_rate_plans
|
|
103
|
+
scope = @product.variants.exists? ? @product.variants : Spree::Variant.where(id: @product.master.id)
|
|
104
|
+
scope = scope.where(id: matching_variant_ids) if option_value_ids.present?
|
|
105
|
+
scope.includes(:stock_items, { rate_plans => { option_values: :option_type } }, { vendor: :integration }, option_values: :option_type)
|
|
106
|
+
.order(:position, :id)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def option_value_ids
|
|
110
|
+
@option_value_ids ||= Array(params[:option_value_ids]).compact_blank.map(&:to_i)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def matching_variant_ids
|
|
114
|
+
SpreeCmCommissioner::Variants::FindByOptionValueIds.new(product: @product, option_value_ids: option_value_ids).execute.ids
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The first window is `from`'s month, plus the next one when fewer than MIN_DAYS_AHEAD days are left, so a
|
|
118
|
+
# late `from` still shows weeks ahead. Whole months, so the blocks added next to it are whole months too.
|
|
119
|
+
# Kept to about a month: every day is a column on every row, and a dated product can have many variants.
|
|
120
|
+
# The page scrolls to `from`.
|
|
121
|
+
def date_window
|
|
122
|
+
to = parse_date(params[:to])
|
|
123
|
+
return @from..[to, @from + (MAX_BLOCK_DAYS - 1)].min if to
|
|
124
|
+
|
|
125
|
+
last = @from.end_of_month
|
|
126
|
+
last = @from.next_month.end_of_month if (last - @from).to_i + 1 < MIN_DAYS_AHEAD
|
|
127
|
+
@from.beginning_of_month..last
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def parse_date(value)
|
|
131
|
+
Date.parse(value.to_s) if value.present?
|
|
132
|
+
rescue Date::Error
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Same permission as the stock calendar this replaces.
|
|
137
|
+
def model_class
|
|
138
|
+
Spree::StockItem
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -5,7 +5,7 @@ module Spree
|
|
|
5
5
|
|
|
6
6
|
before_action :load_parent
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
helper SpreeCmCommissioner::Admin::InventoryItemsHelper
|
|
9
9
|
|
|
10
10
|
def load_parent
|
|
11
11
|
@product = Spree::Product.find_by(slug: params[:product_id])
|
|
@@ -40,58 +40,6 @@ module Spree
|
|
|
40
40
|
redirect_back fallback_location: admin_product_stock_managements_path(@product)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# GET /products/:slug/stock_managements/calendar?year=&selected_option_value_ids[]=
|
|
44
|
-
def calendar
|
|
45
|
-
@year = params[:year].present? ? params[:year].to_i : Time.zone.today.year
|
|
46
|
-
|
|
47
|
-
from_date = Date.new(@year, 1, 1).beginning_of_year
|
|
48
|
-
to_date = Date.new(@year, 1, 1).end_of_year
|
|
49
|
-
|
|
50
|
-
@variant_kind_option_types = Spree::Variants::OptionTypesFinder.new(variant_ids: @product.variants.ids)
|
|
51
|
-
.execute.select(&:variant?)
|
|
52
|
-
@selected_option_value_ids = Array(params[:selected_option_value_ids]).map(&:to_i)
|
|
53
|
-
|
|
54
|
-
inventory_items = @product.inventory_items.includes(:variant, :prices).where(
|
|
55
|
-
inventory_date: from_date..to_date
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
if @selected_option_value_ids.present?
|
|
59
|
-
variant_ids = SpreeCmCommissioner::Variants::FindByOptionValueIds.new(
|
|
60
|
-
product: @product, option_value_ids: @selected_option_value_ids
|
|
61
|
-
).execute.ids
|
|
62
|
-
inventory_items = inventory_items.where(variant_id: variant_ids)
|
|
63
|
-
@matched_variants_count = variant_ids.size
|
|
64
|
-
else
|
|
65
|
-
@matched_variants_count = @product.variants.count
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
@inventory_items = inventory_items.to_a
|
|
69
|
-
|
|
70
|
-
@cached_inventory_items = ::SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(@inventory_items)
|
|
71
|
-
.call
|
|
72
|
-
.index_by(&:inventory_item_id)
|
|
73
|
-
@events = SpreeCmCommissioner::CalendarEvent.from_inventory_items(@inventory_items)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def inventory_item_message(inventory_item, cached_inventory_item)
|
|
77
|
-
synced = inventory_item.quantity_available == cached_inventory_item.quantity_available
|
|
78
|
-
|
|
79
|
-
if synced
|
|
80
|
-
"Synced: Quantity available matches in both DB and Redis (#{cached_inventory_item.quantity_available})."
|
|
81
|
-
else
|
|
82
|
-
"Out of sync: Redis shows #{cached_inventory_item.quantity_available} available, which doesn't match the database."
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def inventory_item_hold_message(inventory_item, cached_inventory_item)
|
|
87
|
-
synced = inventory_item.quantity_on_hold == cached_inventory_item.quantity_on_hold
|
|
88
|
-
if synced
|
|
89
|
-
"Synced: Quantity on hold matches in both DB and Redis (#{cached_inventory_item.quantity_on_hold})."
|
|
90
|
-
else
|
|
91
|
-
"Out of sync: Redis shows #{cached_inventory_item.quantity_on_hold} on hold, which doesn't match the database."
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
43
|
private
|
|
96
44
|
|
|
97
45
|
def load_inventories
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module Spree::Api::SwitchCm
|
|
2
|
+
# The Booking.com-compatible API channel managers (switch.cm) call under /api/cm.
|
|
3
|
+
# It's built on ActionController::API rather than Spree's v2 base, whose rescue handlers
|
|
4
|
+
# answer in JSON:API: a Booking.com connector parses every answer, errors included, as its
|
|
5
|
+
# family's XML, which Xml::BaseController renders.
|
|
6
|
+
#
|
|
7
|
+
# One Doorkeeper app per channel manager, presented as a Bearer token from the token
|
|
8
|
+
# exchange or as HTTP Basic with the app's client id and secret. Booking.com accepted Basic
|
|
9
|
+
# until 31 Dec 2025, and an older connector may still send it.
|
|
10
|
+
class BaseController < ActionController::API
|
|
11
|
+
include ActionController::HttpAuthentication::Basic::ControllerMethods
|
|
12
|
+
|
|
13
|
+
# Bodies are parsed whole and pushes applied in the request, so both are capped; the cap on
|
|
14
|
+
# updates is AcceptPush's.
|
|
15
|
+
MAX_BODY_BYTES = 1.megabyte
|
|
16
|
+
|
|
17
|
+
before_action :authenticate_switch_cm!
|
|
18
|
+
before_action :reject_oversized_body!
|
|
19
|
+
|
|
20
|
+
rescue_from Nokogiri::XML::SyntaxError do
|
|
21
|
+
render_fault(:bad_request, 'Request body is not well-formed XML')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def authenticate_switch_cm!
|
|
27
|
+
@oauth_application = application_from_bearer || application_from_basic
|
|
28
|
+
return if @oauth_application.present? && switch_cm_integrations.exists?
|
|
29
|
+
|
|
30
|
+
render_fault(:unauthorized, 'Authorization Required')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reject_oversized_body!
|
|
34
|
+
return if request.raw_post.bytesize <= MAX_BODY_BYTES
|
|
35
|
+
|
|
36
|
+
render_fault(:payload_too_large, "Request body must not exceed #{MAX_BODY_BYTES / 1.megabyte} MB")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def application_from_bearer
|
|
40
|
+
doorkeeper_token.application if doorkeeper_token&.accessible?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def application_from_basic
|
|
44
|
+
authenticate_with_http_basic do |client_id, client_secret|
|
|
45
|
+
application = Spree::OauthApplication.find_by(uid: client_id)
|
|
46
|
+
application if application&.secret_matches?(client_secret)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def switch_cm_integrations
|
|
51
|
+
SpreeCmCommissioner::Integrations::SwitchCm.readable.where(oauth_application_id: @oauth_application.id)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# nil unless the requested hotel is one of this channel manager's connected hotels.
|
|
55
|
+
def current_integration
|
|
56
|
+
return @current_integration if defined?(@current_integration)
|
|
57
|
+
|
|
58
|
+
accommodation_id = accommodation_id_from_request
|
|
59
|
+
@current_integration = accommodation_id.present? ? switch_cm_integrations.find_by(vendor_id: accommodation_id) : nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def require_integration!
|
|
63
|
+
render_fault(:forbidden, 'Not authorized for this hotel') if current_integration.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Pushes need an active connection: a paused one is still being set up, and the hotel keeps
|
|
67
|
+
# selling on its own counts until it's activated.
|
|
68
|
+
def require_active_integration!
|
|
69
|
+
return render_fault(:forbidden, 'Not authorized for this hotel') if current_integration.nil?
|
|
70
|
+
|
|
71
|
+
render_fault(:forbidden, 'This hotel is not active on BookMe+ yet') unless current_integration.active?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Pushes may leave out the hotel id (B.XML never sends one); Booking.com derives it from the
|
|
75
|
+
# rooms. A room of another channel manager's hotel resolves to a hotel this app can't access.
|
|
76
|
+
def vendor_id_of_room(room_id)
|
|
77
|
+
Spree::Variant.where(id: room_id.to_i).pick(:vendor_id) if room_id.present?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Reservation pulls take a comma-separated list of hotel ids, or none for all of this
|
|
81
|
+
# channel manager's hotels. Unknown ids come back separately, for a warning each, as at
|
|
82
|
+
# Booking.com; a list with no known hotel at all is refused.
|
|
83
|
+
def requested_integrations(hotel_ids)
|
|
84
|
+
ids = hotel_ids.to_s.split(',').map(&:strip).compact_blank
|
|
85
|
+
scope = switch_cm_integrations.includes(:vendor).order(:vendor_id)
|
|
86
|
+
return [scope.to_a, []] if ids.empty?
|
|
87
|
+
|
|
88
|
+
integrations = scope.where(vendor_id: ids).to_a
|
|
89
|
+
[integrations, ids - integrations.map { |integration| integration.vendor_id.to_s }]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def accommodation_id_from_request
|
|
93
|
+
raise NotImplementedError, "#{self.class.name} must implement accommodation_id_from_request"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def render_fault(_status, _text, code: nil)
|
|
97
|
+
raise NotImplementedError, "#{self.class.name} must implement render_fault"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Rails parses neither XML nor text/xml bodies into params, so both families read the raw body.
|
|
101
|
+
def request_document
|
|
102
|
+
@request_document ||= Nokogiri::XML(request.raw_post) { |config| config.strict.nonet }.tap(&:remove_namespaces!)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ruid
|
|
106
|
+
request.request_id
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Spree::Api::SwitchCm
|
|
2
|
+
class TokensController < BaseController
|
|
3
|
+
# Booking.com's tokens also last an hour; connectors refresh on that schedule.
|
|
4
|
+
TOKEN_LIFETIME = 1.hour
|
|
5
|
+
|
|
6
|
+
skip_before_action :authenticate_switch_cm!
|
|
7
|
+
|
|
8
|
+
# POST /api/cm/token-based-authentication/exchange
|
|
9
|
+
# Client id and secret in (JSON or XML), { jwt, ruid } out, then `Authorization: Bearer` on every
|
|
10
|
+
# other call. The value is a Doorkeeper access token rather than a signed JWT, so a connector
|
|
11
|
+
# that only forwards it works but one that decodes it won't.
|
|
12
|
+
def create
|
|
13
|
+
client_id, client_secret = credentials
|
|
14
|
+
return render_fault(:bad_request, 'client_id and client_secret are required') if client_id.blank? || client_secret.blank?
|
|
15
|
+
|
|
16
|
+
@oauth_application = Spree::OauthApplication.find_by(uid: client_id)
|
|
17
|
+
unless @oauth_application&.secret_matches?(client_secret) && switch_cm_integrations.exists?
|
|
18
|
+
return render_fault(:unauthorized, 'Invalid client credentials')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
render_token(issue_token.plaintext_token)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def issue_token
|
|
27
|
+
Spree::OauthAccessToken.create_for(
|
|
28
|
+
application: @oauth_application,
|
|
29
|
+
resource_owner: nil,
|
|
30
|
+
scopes: '',
|
|
31
|
+
expires_in: TOKEN_LIFETIME.to_i,
|
|
32
|
+
use_refresh_token: false
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def credentials
|
|
37
|
+
return [params[:client_id], params[:client_secret]] unless xml_request?
|
|
38
|
+
|
|
39
|
+
%w[client_id client_secret].map { |name| request_document.at_xpath("/request/#{name}")&.text&.strip }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def xml_request?
|
|
43
|
+
request.content_mime_type&.symbol == :xml
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def render_token(token)
|
|
47
|
+
return render json: { jwt: token, ruid: ruid } unless xml_request?
|
|
48
|
+
|
|
49
|
+
document = SpreeCmCommissioner::Integrations::SwitchCm::Xml::ResponseBuilder.build('response', ruid: ruid) do |xml|
|
|
50
|
+
xml.jwt token
|
|
51
|
+
xml.ruid ruid
|
|
52
|
+
end
|
|
53
|
+
render xml: document
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# override
|
|
57
|
+
def render_fault(status, text, code: nil)
|
|
58
|
+
code ||= Rack::Utils.status_code(status)
|
|
59
|
+
|
|
60
|
+
if xml_request?
|
|
61
|
+
render xml: SpreeCmCommissioner::Integrations::SwitchCm::Xml::ResponseBuilder.fault('response', code: code, text: text, ruid: ruid),
|
|
62
|
+
status: status
|
|
63
|
+
else
|
|
64
|
+
render json: { errors: [{ code: code, message: text }], ruid: ruid }, status: status
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Spree::Api::SwitchCm
|
|
2
|
+
module Xml
|
|
3
|
+
class AvailabilitiesController < BaseController
|
|
4
|
+
before_action :require_active_integration!
|
|
5
|
+
|
|
6
|
+
# POST /api/cm/hotels/xml/availability
|
|
7
|
+
# Rooms to sell, prices and closed in one message, validated and applied in the request
|
|
8
|
+
# (Inventory::AcceptPush); the answer is the real outcome.
|
|
9
|
+
def create
|
|
10
|
+
result = SpreeCmCommissioner::Integrations::SwitchCm::Inventory::AcceptPush.call(
|
|
11
|
+
integration: current_integration,
|
|
12
|
+
message: 'availability',
|
|
13
|
+
updates: parser.updates,
|
|
14
|
+
request_id: ruid
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
push_response = SpreeCmCommissioner::Integrations::SwitchCm::Xml::PushResponse.new(result: result.value, ruid: ruid)
|
|
18
|
+
render xml: push_response.to_xml, status: push_response.http_status
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# override
|
|
24
|
+
def response_root
|
|
25
|
+
'availability'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parser
|
|
29
|
+
@parser ||= SpreeCmCommissioner::Integrations::SwitchCm::Xml::AvailabilityParser.new(request_document)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# B.XML pushes carry no hotel id; the rooms name it.
|
|
33
|
+
# override
|
|
34
|
+
def accommodation_id_from_request
|
|
35
|
+
vendor_id_of_room(parser.first_room_id)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Spree::Api::SwitchCm
|
|
2
|
+
module Xml
|
|
3
|
+
# Booking.com's B.XML family (/hotels/xml/...): a request that fails as a whole answers with
|
|
4
|
+
# a <fault code string/> under the call's root element. Requests carry their parameters
|
|
5
|
+
# in a <request> body.
|
|
6
|
+
class BaseController < Spree::Api::SwitchCm::BaseController
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def response_root
|
|
10
|
+
raise NotImplementedError, "#{self.class.name} must implement response_root"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# override
|
|
14
|
+
def render_fault(status, text, code: nil)
|
|
15
|
+
code ||= Rack::Utils.status_code(status)
|
|
16
|
+
render xml: SpreeCmCommissioner::Integrations::SwitchCm::Xml::ResponseBuilder.fault(response_root, code: code, text: text, ruid: ruid),
|
|
17
|
+
status: status
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render_document(&block)
|
|
21
|
+
render xml: SpreeCmCommissioner::Integrations::SwitchCm::Xml::ResponseBuilder.build(response_root, ruid: ruid, &block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# override
|
|
25
|
+
def accommodation_id_from_request
|
|
26
|
+
request_value('hotel_id')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A parameter from the <request> body, falling back to the query string.
|
|
30
|
+
def request_value(name)
|
|
31
|
+
return params[name] if request.raw_post.blank?
|
|
32
|
+
|
|
33
|
+
request_document.at_xpath("/request/#{name}")&.text&.strip.presence || params[name]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Spree::Api::SwitchCm
|
|
2
|
+
module Xml
|
|
3
|
+
class ReservationSummariesController < BaseController
|
|
4
|
+
before_action :require_integration!
|
|
5
|
+
|
|
6
|
+
# POST /api/cm/hotels/xml/reservationssummary with <request><hotel_id/></request>
|
|
7
|
+
# The hotel's confirmed stays checking out from tomorrow on, however long ago they were
|
|
8
|
+
# booked. The channel manager imports these while setting the hotel up, so they count against
|
|
9
|
+
# its availability before its first push. Read-only: nothing is marked seen.
|
|
10
|
+
def create
|
|
11
|
+
reservations = SpreeCmCommissioner::Integrations::SwitchCm::Reservations::Feed.new([current_integration]).summary
|
|
12
|
+
|
|
13
|
+
render_document do |xml|
|
|
14
|
+
reservations.each do |reservation|
|
|
15
|
+
SpreeCmCommissioner::Integrations::SwitchCm::Xml::ReservationSerializer.new(reservation, summary: true).build(xml)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# override
|
|
23
|
+
def response_root
|
|
24
|
+
'reservations'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Spree::Api::SwitchCm
|
|
2
|
+
module Xml
|
|
3
|
+
class ReservationsController < BaseController
|
|
4
|
+
# POST /api/cm/hotels/xml/reservations with <request><hotel_id/><last_change/><limit/><id/></request>
|
|
5
|
+
# New, modified and cancelled reservations in one stream. B.XML has no acknowledgement, so returning a
|
|
6
|
+
# reservation is what marks it seen; `last_change` re-reads recent ones regardless.
|
|
7
|
+
def create
|
|
8
|
+
integrations, = requested_integrations(request_value('hotel_id') || request_value('hotel_ids'))
|
|
9
|
+
return render_fault(:forbidden, 'Not authorized for these hotels') if integrations.empty?
|
|
10
|
+
|
|
11
|
+
reservations = SpreeCmCommissioner::Integrations::SwitchCm::Reservations::Feed.new(integrations).changed_reservations(
|
|
12
|
+
id: request_value('id'),
|
|
13
|
+
last_change: SpreeCmCommissioner::Integrations::SwitchCm::Reservations::Feed.parse_last_change(request_value('last_change')),
|
|
14
|
+
limit: request_value('limit')
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
document = SpreeCmCommissioner::Integrations::SwitchCm::Xml::ResponseBuilder.build(response_root, ruid: ruid) do |xml|
|
|
18
|
+
reservations.each { |reservation| SpreeCmCommissioner::Integrations::SwitchCm::Xml::ReservationSerializer.new(reservation).build(xml) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Marked only once the document is built. One that fails to mark is simply returned
|
|
22
|
+
# again next time, which is harmless: its status is the same.
|
|
23
|
+
failed = mark_seen(reservations)
|
|
24
|
+
log_mark_seen_failures(failed) if failed.any?
|
|
25
|
+
|
|
26
|
+
render xml: document
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# override
|
|
32
|
+
def response_root
|
|
33
|
+
'reservations'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def mark_seen(reservations)
|
|
37
|
+
reservations.each_with_object([]) do |reservation, failed|
|
|
38
|
+
SpreeCmCommissioner::Integrations::SwitchCm::Reservations::MarkSeen.call(
|
|
39
|
+
integration: reservation.integration, order: reservation.order, status: reservation.status,
|
|
40
|
+
modified_at: reservation.modified_at
|
|
41
|
+
)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
failed << {
|
|
44
|
+
order_id: reservation.id,
|
|
45
|
+
integration_id: reservation.integration.id,
|
|
46
|
+
error_class: e.class.name,
|
|
47
|
+
error_message: e.message
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def log_mark_seen_failures(failed)
|
|
53
|
+
CmAppLogger.error(label: "#{self.class.name}#mark_seen failed", data: { ruid: ruid, failed: failed })
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Spree::Api::SwitchCm
|
|
2
|
+
module Xml
|
|
3
|
+
class RoomRateAvailabilitiesController < BaseController
|
|
4
|
+
DAYS_RANGE = (1..31)
|
|
5
|
+
|
|
6
|
+
before_action :require_integration!
|
|
7
|
+
|
|
8
|
+
# POST /api/cm/hotels/xml/roomrateavailability with <request><hotel_id/><start_date/><number_of_days/><room_id/></request>
|
|
9
|
+
# What we hold per room and night, so the channel manager can check its pushes landed.
|
|
10
|
+
# number_of_days is 1-31, 31 by default, as at Booking.com.
|
|
11
|
+
def create
|
|
12
|
+
start_date = parse_date(request_value('start_date')) || Time.zone.today
|
|
13
|
+
days = (request_value('number_of_days').presence || DAYS_RANGE.max).to_i
|
|
14
|
+
return render_fault(:bad_request, "number_of_days must be between #{DAYS_RANGE.min} and #{DAYS_RANGE.max}") unless DAYS_RANGE.cover?(days)
|
|
15
|
+
|
|
16
|
+
rows = SpreeCmCommissioner::Integrations::SwitchCm::Inventory::AvailabilitySnapshot.new(
|
|
17
|
+
integration: current_integration,
|
|
18
|
+
dates: (start_date...(start_date + days)).to_a,
|
|
19
|
+
room_id: request_value('room_id')
|
|
20
|
+
).rows
|
|
21
|
+
|
|
22
|
+
render_document { |xml| rows.each { |row| build_row(xml, row) } }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# override
|
|
28
|
+
def response_root
|
|
29
|
+
'result'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_row(xml, row)
|
|
33
|
+
xml.roomrate(
|
|
34
|
+
{
|
|
35
|
+
date: row.date.iso8601,
|
|
36
|
+
room_id: row.room_id,
|
|
37
|
+
rate_id: row.rate_id,
|
|
38
|
+
rooms_to_sell: row.rooms_to_sell,
|
|
39
|
+
booked: row.booked,
|
|
40
|
+
cancelled: row.cancelled,
|
|
41
|
+
closed: row.closed ? 1 : 0,
|
|
42
|
+
price: row.price && format('%.2f', row.price),
|
|
43
|
+
currency: row.currency,
|
|
44
|
+
pushed_at: row.pushed_at&.utc&.iso8601
|
|
45
|
+
}.compact
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_date(value)
|
|
50
|
+
Date.iso8601(value) if value.present?
|
|
51
|
+
rescue Date::Error
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Spree::Api::SwitchCm
|
|
2
|
+
module Xml
|
|
3
|
+
class RoomRatesController < BaseController
|
|
4
|
+
before_action :require_integration!
|
|
5
|
+
|
|
6
|
+
# POST /api/cm/hotels/xml/roomrates with <request><hotel_id/></request>
|
|
7
|
+
# The hotel's rooms, each with its rates, which the channel manager maps to its own
|
|
8
|
+
# ("Refresh Room" in switch.cm). A derived rate plan is a child rate of the room's base rate.
|
|
9
|
+
def create
|
|
10
|
+
vendor = current_integration.vendor
|
|
11
|
+
rooms = SpreeCmCommissioner::Integrations::SwitchCm::RoomCatalog.new(vendor).rooms
|
|
12
|
+
|
|
13
|
+
render_document do |xml|
|
|
14
|
+
rooms.each do |room|
|
|
15
|
+
xml.room(id: room.id, hotel_id: vendor.id, hotel_name: vendor.name, room_name: room.name, max_children: room.max_children) do
|
|
16
|
+
xml.rates { room.rates.each { |rate| build_rate(xml, room, rate) } }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_rate(xml, room, rate)
|
|
25
|
+
attributes = { id: rate.id, rate_name: rate.name, max_persons: room.max_persons, is_child_rate: rate.child? ? 1 : 0, readonly: 0 }
|
|
26
|
+
return xml.rate(attributes) unless rate.child?
|
|
27
|
+
|
|
28
|
+
# Restrictions other than closing aren't stored, so the child rate follows only its parent's price and closings.
|
|
29
|
+
xml.rate(attributes) do
|
|
30
|
+
xml.rate_relation(
|
|
31
|
+
{
|
|
32
|
+
parent_rate_id: rate.parent_id, percentage: rate.percentage&.to_f, additional: rate.additional&.to_f,
|
|
33
|
+
follows_price: 1, follows_closed: 3, follows_restrictions: 0
|
|
34
|
+
}.compact
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# override
|
|
40
|
+
def response_root
|
|
41
|
+
'rooms'
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -7,6 +7,7 @@ module Spree
|
|
|
7
7
|
return Spree::Vendor if params['model_class'] == 'Spree::Vendor'
|
|
8
8
|
return Spree::Product if params['model_class'] == 'Spree::Product'
|
|
9
9
|
return Spree::Taxon if params['model_class'] == 'Spree::Taxon'
|
|
10
|
+
return Spree.user_class if params['model_class'] == 'Spree::User'
|
|
10
11
|
|
|
11
12
|
nil
|
|
12
13
|
end
|