spree_cm_commissioner 2.13.2 → 2.13.3.pre.pre2
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/.env.example +1 -0
- data/Gemfile.lock +1 -1
- data/app/controllers/concerns/spree_cm_commissioner/operator/event_authorization.rb +19 -0
- data/app/controllers/spree/admin/homepage_section_controller.rb +32 -8
- data/app/controllers/spree/admin/integrations_controller.rb +5 -7
- data/app/controllers/spree/admin/inventory_items_controller.rb +12 -0
- data/app/controllers/spree/admin/notification_users_controller.rb +39 -7
- data/app/controllers/spree/admin/service_fee_rules_controller.rb +83 -0
- data/app/controllers/spree/admin/stock_managements_controller.rb +5 -5
- data/app/controllers/spree/api/switch_cm/base_controller.rb +98 -0
- data/app/controllers/spree/api/switch_cm/ota/avail_notifs_controller.rb +17 -0
- data/app/controllers/spree/api/switch_cm/ota/base_controller.rb +27 -0
- data/app/controllers/spree/api/switch_cm/ota/product_notifs_controller.rb +44 -0
- data/app/controllers/spree/api/switch_cm/ota/pushes_controller.rb +46 -0
- data/app/controllers/spree/api/switch_cm/ota/rate_amount_notifs_controller.rb +17 -0
- data/app/controllers/spree/api/switch_cm/ota/res_modify_notifs_controller.rb +31 -0
- data/app/controllers/spree/api/switch_cm/ota/res_notifs_controller.rb +29 -0
- data/app/controllers/spree/api/switch_cm/ota/reservation_streams_controller.rb +83 -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 +56 -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 +32 -0
- data/app/controllers/spree/api/v2/operator/check_ins_controller.rb +7 -6
- data/app/controllers/spree/api/v2/operator/event_charts_controller.rb +8 -10
- data/app/controllers/spree/api/v2/operator/event_ticket_aggregators_controller.rb +14 -4
- data/app/controllers/spree/api/v2/operator/pie_chart_event_aggregators_controller.rb +15 -1
- data/app/controllers/spree/api/v2/platform/homepage_section_relatable_options_controller.rb +28 -0
- data/app/controllers/spree/api/v2/storefront/cross_app_handoff_tokens_controller.rb +41 -0
- data/app/controllers/spree/api/v2/storefront/homepage_sections_controller.rb +4 -2
- data/app/controllers/spree/api/v2/storefront/transit/item_availabilities_controller.rb +66 -0
- data/app/controllers/spree/api/v2/tenant/checkout_controller.rb +11 -0
- data/app/controllers/spree/api/v2/tenant/vendor_photos_controller.rb +25 -0
- data/app/controllers/spree_cm_commissioner/admin/orders_controller_decorator.rb +9 -1
- data/app/controllers/spree_cm_commissioner/api/v2/storefront/checkout_controller_decorator.rb +15 -0
- data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +6 -2
- data/app/helpers/spree/admin/customer_notifications_helper.rb +57 -0
- data/app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb +45 -23
- data/app/interactors/spree_cm_commissioner/customer_notification_sender.rb +18 -9
- data/app/interactors/spree_cm_commissioner/user_cross_app_handoff_authenticator.rb +17 -0
- data/app/jobs/spree_cm_commissioner/audit_event_job.rb +29 -0
- data/app/models/concerns/spree_cm_commissioner/audit_wrapper.rb +139 -0
- data/app/models/concerns/spree_cm_commissioner/homepage_section_bitwise.rb +37 -1
- data/app/models/concerns/spree_cm_commissioner/integrations/inventory_sync_cachable.rb +9 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_open_dated_trippable.rb +2 -2
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +10 -0
- data/app/models/concerns/spree_cm_commissioner/phone_number_sanitizer.rb +5 -1
- data/app/models/concerns/spree_cm_commissioner/user_notification.rb +3 -1
- data/app/models/concerns/spree_cm_commissioner/variant_metadata.rb +24 -0
- data/app/models/concerns/spree_cm_commissioner/vehicle_kind.rb +1 -1
- data/app/models/spree_cm_commissioner/adjustable/adjuster/service_fee.rb +31 -0
- data/app/models/spree_cm_commissioner/adjustment_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/audit_event.rb +11 -0
- data/app/models/spree_cm_commissioner/calculators/service_fee_flat_amount.rb +17 -0
- data/app/models/spree_cm_commissioner/calculators/service_fee_percent.rb +20 -0
- data/app/models/spree_cm_commissioner/currency_rate.rb +15 -0
- data/app/models/spree_cm_commissioner/customer_notification.rb +15 -1
- data/app/models/spree_cm_commissioner/homepage_section.rb +25 -0
- data/app/models/spree_cm_commissioner/integration.rb +46 -0
- data/app/models/spree_cm_commissioner/integrations/book_me_bus_v1.rb +64 -0
- data/app/models/spree_cm_commissioner/integrations/larryta.rb +81 -0
- data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +4 -10
- data/app/models/spree_cm_commissioner/integrations/switch_cm.rb +86 -0
- data/app/models/spree_cm_commissioner/integrations/vireak_buntham.rb +82 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +18 -0
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +2 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +13 -1
- data/app/models/spree_cm_commissioner/order_updater_decorator.rb +31 -0
- data/app/models/spree_cm_commissioner/place.rb +2 -0
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -1
- data/app/models/spree_cm_commissioner/route.rb +1 -0
- data/app/models/spree_cm_commissioner/seat_layout.rb +1 -0
- data/app/models/spree_cm_commissioner/service_fee_rule.rb +183 -0
- data/app/models/spree_cm_commissioner/stock/availability_checker.rb +1 -1
- data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +1 -1
- data/app/models/spree_cm_commissioner/stock_item_decorator.rb +4 -0
- data/app/models/spree_cm_commissioner/trip.rb +2 -1
- data/app/models/spree_cm_commissioner/trip_stop.rb +2 -0
- data/app/models/spree_cm_commissioner/user_decorator.rb +4 -0
- data/app/models/spree_cm_commissioner/variant_decorator.rb +5 -0
- data/app/models/spree_cm_commissioner/vehicle.rb +3 -0
- data/app/models/spree_cm_commissioner/vehicle_type.rb +1 -0
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +17 -0
- data/app/models/spree_cm_commissioner/vendor_place.rb +2 -0
- data/app/overrides/spree/admin/orders/index/line_items_row.html.erb.deface +14 -0
- data/app/overrides/spree/admin/orders/index/row_actions.html.erb.deface +17 -0
- data/app/overrides/spree/admin/shared/sub_menu/_configuration/menu_items.html.erb.deface +5 -0
- data/app/queries/spree_cm_commissioner/users_by_event_fetcher_query.rb +6 -7
- data/app/serializables/spree_cm_commissioner/item_availability_sync.rb +10 -0
- data/app/serializers/spree/api/v2/platform/service_fee_rule_serializer.rb +17 -0
- data/app/serializers/spree/v2/storefront/homepage_section_serializer.rb +1 -1
- data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +8 -2
- data/app/serializers/spree/v2/tenant/cart_serializer.rb +2 -1
- data/app/serializers/spree/v2/tenant/line_item_serializer.rb +7 -1
- data/app/serializers/spree_cm_commissioner/v2/operator/guest_serializer.rb +2 -1
- data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +2 -1
- data/app/serializers/spree_cm_commissioner/v2/storefront/cross_app_handoff_token_serializer.rb +9 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/item_availability_sync_serializer.rb +10 -0
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +1 -1
- data/app/services/spree_cm_commissioner/audit_logger.rb +59 -0
- data/app/services/spree_cm_commissioner/checkout/advance_to_payment.rb +27 -0
- data/app/services/spree_cm_commissioner/cross_app_handoff_tokens/decode.rb +42 -0
- data/app/services/spree_cm_commissioner/cross_app_handoff_tokens/encode.rb +47 -0
- data/app/services/spree_cm_commissioner/currency_converter/create.rb +27 -0
- data/app/services/spree_cm_commissioner/currency_rates/manage.rb +99 -0
- data/app/services/spree_cm_commissioner/draft_orders/create.rb +1 -0
- data/app/services/spree_cm_commissioner/google_maps_geocoder.rb +41 -0
- data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +1 -2
- data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +11 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/client.rb +230 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/connection.rb +147 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_item_status.rb +52 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_items.rb +91 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/unstock_inventory.rb +188 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_inventory.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_item_availability.rb +354 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_locations.rb +103 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_nationality_pricing.rb +93 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_product.rb +70 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_route_stops.rb +121 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_routes.rb +128 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_seat_layout.rb +412 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_service_calendars.rb +59 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +224 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trips.rb +769 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_variant.rb +69 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_vehicle_types.rb +92 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/time_parser.rb +19 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/base.rb +40 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/inventory_item_status.rb +148 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/location.rb +55 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/reservation_cart.rb +85 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat.rb +128 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb +386 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/stop.rb +84 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/trip.rb +237 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/wallet_payment.rb +48 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_manager.rb +22 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/full_sync_strategy.rb +116 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/incremental_sync_strategy.rb +63 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/external_client/client.rb +314 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/external_client/connection.rb +74 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/inventory/external_inventory_item_status.rb +53 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/inventory/external_inventory_items.rb +106 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/inventory/hold_seats.rb +143 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/inventory/release_seats.rb +90 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/inventory/unstock_inventory.rb +143 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_item_availability.rb +325 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_product.rb +72 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_routes.rb +142 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_seat_layout.rb +255 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_stock_item.rb +55 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_trip_stop.rb +171 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_trips.rb +397 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_variant.rb +63 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_vehicle_type.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/base.rb +39 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking.rb +113 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking_session.rb +43 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking_transaction.rb +236 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/direction.rb +71 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/passenger.rb +48 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/schedule.rb +150 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat.rb +31 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat_availability.rb +49 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat_layout.rb +211 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/sync_manager.rb +17 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/sync_strategies/full_sync_strategy.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/sync_strategies/incremental_sync_strategy.rb +58 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +14 -15
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +7 -4
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +6 -3
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +0 -3
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +1 -3
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +1 -3
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +1 -2
- data/app/services/spree_cm_commissioner/integrations/switch_cm/accept_push.rb +101 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/accommodation_lock.rb +41 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/acknowledge_reservations.rb +43 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/apply_ari.rb +119 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/ari_update.rb +24 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/availability_readback.rb +86 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/mark_seen.rb +53 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/avail_notif_parser.rb +60 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/base_parser.rb +60 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/push_response.rb +38 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/rate_amount_notif_parser.rb +46 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/reservation_presenter.rb +99 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/ota/response_builder.rb +52 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/pushed_nights.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/reservation.rb +118 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/reservation_feed.rb +143 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/room_catalog.rb +56 -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/unseen_room_nights.rb +39 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/validate_ari_updates.rb +136 -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_presenter.rb +74 -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/external_client/client.rb +170 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/external_client/connection.rb +59 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/external_inventory_item_status.rb +48 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/external_inventory_items.rb +92 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/unstock_inventory.rb +230 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_item_availability.rb +270 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_locations.rb +96 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_nationality_pricing.rb +76 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_product.rb +69 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_routes.rb +110 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_seat_layout.rb +207 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_stock_item.rb +43 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_trip_stop.rb +192 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_trips.rb +377 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_variant.rb +56 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_vehicle_type.rb +51 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/base.rb +32 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/booking.rb +48 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/booking_transaction.rb +46 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/cell.rb +53 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/confirm_booking_request.rb +43 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/location.rb +37 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/schedule.rb +138 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/seat.rb +48 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/seat_layout.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_manager.rb +17 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_strategies/full_sync_strategy.rb +87 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_strategies/incremental_sync_strategy.rb +53 -0
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_by_variant.rb +4 -0
- data/app/services/spree_cm_commissioner/inventory_items/generate_permanent_item.rb +48 -0
- data/app/services/spree_cm_commissioner/inventory_items/reset.rb +4 -0
- data/app/services/spree_cm_commissioner/order_holds/hold.rb +17 -0
- data/app/services/spree_cm_commissioner/order_holds/release.rb +17 -0
- data/app/services/spree_cm_commissioner/place_resolver.rb +95 -0
- data/app/services/spree_cm_commissioner/transit_order/create.rb +1 -0
- data/app/services/spree_cm_commissioner/user_authenticator.rb +20 -12
- data/app/views/spree/admin/customer_notifications/_form.html.erb +9 -9
- data/app/views/spree/admin/customer_notifications/index.html.erb +36 -19
- data/app/views/spree/admin/homepage_section/_form.html.erb +19 -2
- data/app/views/spree/admin/homepage_section/_tabs.html.erb +7 -7
- data/app/views/spree/admin/homepage_section/index.html.erb +73 -6
- data/app/views/spree/admin/integrations/_book_me_bus_v1_fields.html.erb +47 -0
- data/app/views/spree/admin/integrations/_form.html.erb +16 -0
- data/app/views/spree/admin/integrations/_larryta_fields.html.erb +58 -0
- data/app/views/spree/admin/integrations/_switch_cm_fields.html.erb +53 -0
- data/app/views/spree/admin/integrations/_vireak_buntham_fields.html.erb +75 -0
- data/app/views/spree/admin/integrations/index.html.erb +1 -1
- data/app/views/spree/admin/notifications/index.html.erb +2 -1
- data/app/views/spree/admin/orders/line_items.html.erb +53 -0
- data/app/views/spree/admin/service_fee_rules/_form.html.erb +119 -0
- data/app/views/spree/admin/service_fee_rules/edit.html.erb +11 -0
- data/app/views/spree/admin/service_fee_rules/index.html.erb +91 -0
- data/app/views/spree/admin/service_fee_rules/new.html.erb +11 -0
- data/app/views/spree/admin/shared/_send_all_form.html.erb +1 -0
- data/app/views/spree/admin/shared/_send_test_form.html.erb +3 -3
- data/app/views/spree/admin/stock_managements/index.html.erb +14 -0
- data/config/locales/en.yml +36 -0
- data/config/locales/km.yml +32 -0
- data/config/routes.rb +34 -0
- data/db/migrate/20260327143200_create_cm_audit_events.rb +21 -0
- data/db/migrate/20260410045815_create_cm_currency_rates.rb +23 -0
- data/db/migrate/20260411102139_add_metadata_to_cm_inventory_items.rb +6 -0
- data/db/migrate/20260914000000_add_mini_app_to_homepage_sections.rb +5 -0
- data/db/migrate/20260917085551_create_cm_service_fee_rules.rb +16 -0
- data/db/migrate/20260917085552_add_service_fee_total_to_spree_line_items_and_orders.rb +6 -0
- data/db/migrate/20260928000000_add_oauth_application_id_to_cm_integrations.rb +5 -0
- data/db/migrate/20260928000001_add_closed_to_cm_inventory_items.rb +5 -0
- data/lib/spree_cm_commissioner/cached_inventory_item.rb +7 -1
- data/lib/spree_cm_commissioner/engine.rb +3 -0
- data/lib/spree_cm_commissioner/test_helper/factories/homepage_section_factory.rb +7 -0
- data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +17 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +187 -4
data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/unstock_inventory.rb
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
class SpreeCmCommissioner::Integrations::BookMeBusV1::Inventory::UnstockInventory
|
|
2
|
+
prepend ::Spree::ServiceModule::Base
|
|
3
|
+
|
|
4
|
+
# No enclosing DB transaction here (deliberately - see sync_line_item!): BookMeBus has no
|
|
5
|
+
# reservation-cancellation API in production, so once create_reservation! succeeds there is a
|
|
6
|
+
# real, un-undoable external side effect. Wrapping the loop in one transaction used to let a
|
|
7
|
+
# later failure (a different line item, or payment confirmation) roll back the local mapping
|
|
8
|
+
# that recorded an *earlier* line item's already-created BookMeBus reservation - on retry,
|
|
9
|
+
# sync_line_item! would then create a second, duplicate reservation for the same seats.
|
|
10
|
+
def call(integration:, order:, line_items:)
|
|
11
|
+
line_items.each do |line_item|
|
|
12
|
+
raise SpreeCmCommissioner::Integrations::SyncError, 'Invalid guests' if line_item.guests.size != line_item.quantity
|
|
13
|
+
|
|
14
|
+
sync_line_item!(integration, order, line_item)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
success(order: order, line_items: line_items)
|
|
18
|
+
rescue SpreeCmCommissioner::Integrations::SyncError,
|
|
19
|
+
SpreeCmCommissioner::Integrations::ExternalClientError => e
|
|
20
|
+
failure(nil, e.message)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Sync a single line item with BookMeBus
|
|
24
|
+
def sync_line_item!(integration, order, line_item) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
25
|
+
existing_mapping = line_item.integration_mappings.find_by(integration_id: integration.id, status: :active)
|
|
26
|
+
|
|
27
|
+
if existing_mapping
|
|
28
|
+
return if existing_mapping.external_payload&.dig('payment_confirmed')
|
|
29
|
+
|
|
30
|
+
# A BookMeBus reservation already exists from a prior attempt (recorded durably, see
|
|
31
|
+
# above), but payment was never confirmed - e.g. confirm_wallet_payment! failed last time
|
|
32
|
+
# and this got retried. We can't safely retry create_reservation! (would duplicate the
|
|
33
|
+
# reservation) and can't cancel the existing one (no API for it), so this needs a human,
|
|
34
|
+
# not another automatic attempt.
|
|
35
|
+
raise SpreeCmCommissioner::Integrations::SyncError,
|
|
36
|
+
"BookMeBus reservation #{existing_mapping.external_id} already exists for line item " \
|
|
37
|
+
"##{line_item.id} but payment was never confirmed - needs manual reconciliation, not an automatic retry"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
internal_trip_id = line_item.trip_id
|
|
41
|
+
raise SpreeCmCommissioner::Integrations::SyncError, 'trip_id missing from line item metadata' if internal_trip_id.blank?
|
|
42
|
+
|
|
43
|
+
trip_mapping = SpreeCmCommissioner::IntegrationMapping.find_by(
|
|
44
|
+
integration_id: integration.id,
|
|
45
|
+
internal_type: 'SpreeCmCommissioner::Trip',
|
|
46
|
+
internal_id: internal_trip_id,
|
|
47
|
+
status: :active
|
|
48
|
+
)
|
|
49
|
+
raise SpreeCmCommissioner::Integrations::SyncError, "Integration mapping not found for trip ##{internal_trip_id}" if trip_mapping.nil?
|
|
50
|
+
|
|
51
|
+
external_trip_id = trip_mapping.external_id
|
|
52
|
+
raise SpreeCmCommissioner::Integrations::SyncError, 'External trip_id missing from mapping' if external_trip_id.blank?
|
|
53
|
+
|
|
54
|
+
boarding_mapping = find_trip_stop_mapping(integration, line_item.boarding_trip_stop_id)
|
|
55
|
+
drop_off_mapping = find_trip_stop_mapping(integration, line_item.drop_off_trip_stop_id)
|
|
56
|
+
|
|
57
|
+
from_stop_id = boarding_mapping&.external_payload&.dig('stop', 'id') || trip_mapping.external_payload&.dig('from_stop_id')
|
|
58
|
+
to_stop_id = drop_off_mapping&.external_payload&.dig('stop', 'id') || trip_mapping.external_payload&.dig('to_stop_id')
|
|
59
|
+
raise SpreeCmCommissioner::Integrations::SyncError, 'from_stop_id/to_stop_id missing for line item' if from_stop_id.blank? || to_stop_id.blank?
|
|
60
|
+
|
|
61
|
+
from_stop_time_id = boarding_mapping&.external_payload&.dig('id')
|
|
62
|
+
to_stop_time_id = drop_off_mapping&.external_payload&.dig('id')
|
|
63
|
+
|
|
64
|
+
address = order.bill_address || order.ship_address
|
|
65
|
+
|
|
66
|
+
reservation_cart = integration.client.create_reservation!(
|
|
67
|
+
{
|
|
68
|
+
trip_id: external_trip_id.to_i,
|
|
69
|
+
from_stop_id: from_stop_id.to_i,
|
|
70
|
+
to_stop_id: to_stop_id.to_i,
|
|
71
|
+
from_stop_time_id: from_stop_time_id,
|
|
72
|
+
to_stop_time_id: to_stop_time_id,
|
|
73
|
+
departure_date: line_item.from_date.to_date.strftime('%Y-%m-%d'),
|
|
74
|
+
reserved_seats: build_reserved_seats(line_item, from_stop_id, integration),
|
|
75
|
+
contact_name: address&.full_name.presence || order.email,
|
|
76
|
+
contact_email: order.email,
|
|
77
|
+
contact_phone: address&.phone.presence || order.user&.phone,
|
|
78
|
+
internal_order: order
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Persist immediately, before anything that can still fail (sync_guests!, confirm_payment!) -
|
|
83
|
+
# this reservation now exists on BookMeBus for real and must be recorded regardless of what
|
|
84
|
+
# happens next (see the no-transaction note on #call).
|
|
85
|
+
line_item_mapping = line_item.integration_mappings.find_or_initialize_by(
|
|
86
|
+
integration: integration,
|
|
87
|
+
external_id: reservation_cart.id
|
|
88
|
+
)
|
|
89
|
+
line_item_mapping.mark_as_active!(external_payload: reservation_cart.to_h)
|
|
90
|
+
|
|
91
|
+
sync_guests!(line_item.guests, reservation_cart, integration)
|
|
92
|
+
confirm_payment!(integration, reservation_cart, order)
|
|
93
|
+
|
|
94
|
+
line_item_mapping.mark_as_active!(external_payload: reservation_cart.to_h.merge('payment_confirmed' => true))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Creates guest mappings and stores QR codes for check-in
|
|
98
|
+
def sync_guests!(guests, reservation_cart, integration)
|
|
99
|
+
reservation = reservation_cart.first_reservation
|
|
100
|
+
return unless reservation
|
|
101
|
+
|
|
102
|
+
guests.each do |guest|
|
|
103
|
+
guest_mapping = guest.integration_mappings.find_or_initialize_by(
|
|
104
|
+
integration: integration,
|
|
105
|
+
external_id: reservation.id
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
guest_mapping.external_qr_data = reservation.reservation_qr_code_url
|
|
109
|
+
guest_mapping.mark_as_active!(external_payload: reservation.to_h)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Create and confirm wallet payment for the reservation
|
|
114
|
+
def confirm_payment!(integration, reservation_cart, order)
|
|
115
|
+
wallet_payment = integration.client.create_wallet_payment!(
|
|
116
|
+
cart_id: reservation_cart.id,
|
|
117
|
+
internal_order: order
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
confirmed_payment = integration.client.confirm_wallet_payment!(
|
|
121
|
+
payment_id: wallet_payment.id,
|
|
122
|
+
internal_order: order
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
raise SpreeCmCommissioner::Integrations::SyncError, 'Payment confirmation failed' unless confirmed_payment.paid?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Find active trip stop mapping for integration
|
|
129
|
+
def find_trip_stop_mapping(integration, trip_stop_id)
|
|
130
|
+
return nil if trip_stop_id.blank?
|
|
131
|
+
|
|
132
|
+
SpreeCmCommissioner::IntegrationMapping.find_by(
|
|
133
|
+
integration_id: integration.id,
|
|
134
|
+
internal_type: 'SpreeCmCommissioner::TripStop',
|
|
135
|
+
internal_id: trip_stop_id,
|
|
136
|
+
status: :active
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Build reserved seats hash for BookMeBus API
|
|
141
|
+
#
|
|
142
|
+
# Trips where BookMeBus auto-assigns the seat (Trip#allow_seat_selection false, synced from
|
|
143
|
+
# their auto_assigned_seat flag) never give guests a block - that's expected, not an error.
|
|
144
|
+
# Avocado's own reservation flow (Transit::PassengerSeatCreator/ThirdParty::AutoAssignSeat)
|
|
145
|
+
# expects a full passenger list with label/layer nil in that case, not an empty payload, so it
|
|
146
|
+
# can assign real seats server-side - dropping those guests entirely would undercount
|
|
147
|
+
# number_of_seats and reserve fewer seats than the order was actually paid for.
|
|
148
|
+
def build_reserved_seats(line_item, from_stop_id, integration) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
149
|
+
seats = line_item.guests.map do |guest|
|
|
150
|
+
seat_label = guest.block&.label
|
|
151
|
+
|
|
152
|
+
if seat_label.blank? && line_item.required_seat_selection?
|
|
153
|
+
raise SpreeCmCommissioner::Integrations::SyncError, "Guest ##{guest.id} on line item ##{line_item.id} has no assigned seat"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
name = [guest.first_name, guest.last_name].compact.join(' ')
|
|
157
|
+
name = "Guest #{guest.id}" if name.blank?
|
|
158
|
+
|
|
159
|
+
block_type = guest.block&.block_type&.to_s
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
name: name,
|
|
163
|
+
label: seat_label,
|
|
164
|
+
seat_type: block_type == 'seat' || block_type.blank? ? 'normal' : block_type,
|
|
165
|
+
layer: seat_label.present? ? (guest.block&.seat_section&.name || 'BUS') : nil,
|
|
166
|
+
nationality: guest_nationality_code(guest, integration)
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
seats.empty? ? {} : { from_stop_id.to_s => seats }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Mirrors PricingModels::GuestContext's own fallback order (nationality_id, then
|
|
174
|
+
# guest.nationality_group, then guest.saved_guest.nationality_group) so the reservation actually
|
|
175
|
+
# sent to BookMeBus agrees with whatever nationality pricing already used to price the guest.
|
|
176
|
+
# Without this, a guest identified only via nationality_group (e.g. staff selecting it directly
|
|
177
|
+
# in the sell-ticket dashboard, which never sets nationality_id at all) was always reported as
|
|
178
|
+
# 'other' here regardless of what was actually selected.
|
|
179
|
+
def guest_nationality_code(guest, integration)
|
|
180
|
+
code = guest.nationality&.subtitle
|
|
181
|
+
return code.downcase if code.present?
|
|
182
|
+
|
|
183
|
+
group = guest.nationality_group || guest.saved_guest&.nationality_group
|
|
184
|
+
return (integration.vendor.service_origin&.subtitle || 'other').downcase if group == 'local'
|
|
185
|
+
|
|
186
|
+
'other'
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
|
|
2
|
+
# SyncInventory handles inventory synchronization for BookMeBus trips
|
|
3
|
+
#
|
|
4
|
+
# This service is responsible for:
|
|
5
|
+
# - Creating or updating StockItem records
|
|
6
|
+
# - Adjusting inventory counts based on external seat availability
|
|
7
|
+
# - Managing stock locations for vendors
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# sync_service = SyncInventory.new(sync_result: sync_result, vendor: vendor)
|
|
11
|
+
# sync_service.call(variant: variant, external_trip: external_trip)
|
|
12
|
+
#
|
|
13
|
+
class SyncInventory
|
|
14
|
+
def initialize(sync_result:, vendor:)
|
|
15
|
+
@sync_result = sync_result
|
|
16
|
+
@vendor = vendor
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(variant:, external_trip:)
|
|
20
|
+
return unless variant&.persisted?
|
|
21
|
+
|
|
22
|
+
stock_location = find_or_create_stock_location
|
|
23
|
+
stock_item = find_or_create_stock_item(variant, stock_location)
|
|
24
|
+
|
|
25
|
+
adjust_inventory_if_needed(stock_item, external_trip)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def find_or_create_stock_location
|
|
31
|
+
@vendor.stock_locations.first || @vendor.send(:create_stock_location)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def find_or_create_stock_item(variant, stock_location)
|
|
35
|
+
variant.stock_items.find_or_create_by!(stock_location: stock_location) do |item|
|
|
36
|
+
item.count_on_hand = 0
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def adjust_inventory_if_needed(stock_item, external_trip)
|
|
41
|
+
available_seats = external_trip.available_seats
|
|
42
|
+
return if available_seats.nil? # unknown, not zero - don't zero out real inventory
|
|
43
|
+
|
|
44
|
+
current_stock = stock_item.count_on_hand.to_i
|
|
45
|
+
difference = available_seats.to_i - current_stock
|
|
46
|
+
return if difference.zero?
|
|
47
|
+
|
|
48
|
+
@sync_result.track(:stock_item, stock_item) do
|
|
49
|
+
stock_item.adjust_count_on_hand(difference)
|
|
50
|
+
@sync_result.increment_metric(:inventory, :adjusted, difference.abs)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
adjust_inventory_item_for_date(stock_item.variant, external_trip, difference)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# StockItem.count_on_hand isn't date-aware and isn't read for real availability checks
|
|
57
|
+
# (see docs/concepts/inventory-system.md) - apply the same delta to the specific date's
|
|
58
|
+
# InventoryItem so ongoing BookMeBus availability changes actually reach bookable inventory.
|
|
59
|
+
# If it doesn't exist yet, PermanentInventoryItemsGeneratorJob will seed it later from
|
|
60
|
+
# count_on_hand, so there's nothing to adjust yet.
|
|
61
|
+
def adjust_inventory_item_for_date(variant, external_trip, difference)
|
|
62
|
+
trip_date = parse_trip_date(external_trip.on_date)
|
|
63
|
+
return unless trip_date
|
|
64
|
+
|
|
65
|
+
inventory_item = variant.inventory_items.find_by(inventory_date: trip_date)
|
|
66
|
+
return unless inventory_item
|
|
67
|
+
|
|
68
|
+
inventory_item.adjust_quantity!(difference)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_trip_date(date_string)
|
|
72
|
+
return nil if date_string.blank?
|
|
73
|
+
|
|
74
|
+
Date.parse(date_string.to_s)
|
|
75
|
+
rescue ArgumentError, TypeError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
|
|
2
|
+
# SyncItemAvailability syncs pricing and reserved blocks from BookMeBus API
|
|
3
|
+
#
|
|
4
|
+
# BookMeBus trips have an `is_selectable` flag that determines sync behavior:
|
|
5
|
+
# - is_selectable == true: Sync reserved blocks (booked seats)
|
|
6
|
+
# - is_selectable == false: Only update seats_remaining count
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# SyncItemAvailability.new(integration: integration, sync_result: sync_result)
|
|
10
|
+
# .call(trip_id: 1, on_date: '2026-04-06')
|
|
11
|
+
class SyncItemAvailability # rubocop:disable Metrics/ClassLength
|
|
12
|
+
include SpreeCmCommissioner::Integrations::InventorySyncCachable
|
|
13
|
+
|
|
14
|
+
# Sentinel returned by fetch_external_trip to mean "we got a real, successful response from
|
|
15
|
+
# BookMeBus, and this trip is confirmed absent/unbookable for this date" - distinct from a
|
|
16
|
+
# plain nil, which means we couldn't determine that (missing local mapping, or the API call
|
|
17
|
+
# itself failed) and must leave existing inventory alone rather than guess.
|
|
18
|
+
UNAVAILABLE = :unavailable
|
|
19
|
+
private_constant :UNAVAILABLE
|
|
20
|
+
|
|
21
|
+
def initialize(integration:, sync_result: nil)
|
|
22
|
+
@integration = integration
|
|
23
|
+
@sync_result = sync_result || SpreeCmCommissioner::Integrations::Base::SyncResult.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(trip_id:, on_date:)
|
|
27
|
+
trip = find_trip(trip_id)
|
|
28
|
+
return @sync_result unless trip
|
|
29
|
+
|
|
30
|
+
inventory_items = find_inventory_items_for_trip(trip, on_date)
|
|
31
|
+
return @sync_result if inventory_items.empty?
|
|
32
|
+
|
|
33
|
+
inventory_items.each do |inventory_item|
|
|
34
|
+
sync_item(trip, on_date, inventory_item)
|
|
35
|
+
end
|
|
36
|
+
# return the most recent last_synced_at from the inventory item mappings for this trip and date
|
|
37
|
+
@sync_result.last_synced_at =
|
|
38
|
+
inventory_items.last
|
|
39
|
+
&.integration_mappings
|
|
40
|
+
&.maximum(:last_synced_at)
|
|
41
|
+
|
|
42
|
+
@sync_result
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
@sync_result.record_error(e)
|
|
45
|
+
CmAppLogger.error(label: '[BookMeBusV1::SyncItemAvailability] sync failed', data: { error: e.message })
|
|
46
|
+
@sync_result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def sync_item(trip, on_date, inventory_item)
|
|
50
|
+
inventory_item_mapping = find_or_initialize_item_mapping(inventory_item)
|
|
51
|
+
|
|
52
|
+
return unless should_sync_item?(inventory_item_mapping)
|
|
53
|
+
|
|
54
|
+
ActiveRecord::Base.transaction do
|
|
55
|
+
external_trip = fetch_external_trip(trip, on_date)
|
|
56
|
+
|
|
57
|
+
case external_trip
|
|
58
|
+
when UNAVAILABLE
|
|
59
|
+
# A confirmed response (not a failed/ambiguous lookup - see fetch_external_trip) shows
|
|
60
|
+
# this trip is absent or no longer bookable for this specific date, while an
|
|
61
|
+
# already-synced InventoryItem here may still show real seats from a previous sync.
|
|
62
|
+
# Zero it out so BookMe+ stops selling a date BookMeBus has pulled, rather than only
|
|
63
|
+
# discovering that at the very end of checkout, after payment has been taken.
|
|
64
|
+
zero_out_inventory!(inventory_item)
|
|
65
|
+
when nil
|
|
66
|
+
return # rubocop:disable Rails/TransactionExitStatement -- couldn't determine (missing mapping data) - leave existing inventory untouched rather than guess
|
|
67
|
+
else
|
|
68
|
+
sync_price(inventory_item, external_trip)
|
|
69
|
+
|
|
70
|
+
# Sync strategy depends on trip type and seat availability
|
|
71
|
+
if should_sync_reserved_blocks?(external_trip)
|
|
72
|
+
sync_reserved_blocks(external_trip, on_date, inventory_item)
|
|
73
|
+
else
|
|
74
|
+
sync_stock_item(inventory_item, external_trip)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
restore_inventory_if_previously_zeroed!(inventory_item, external_trip)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
update_last_sync_at(inventory_item_mapping)
|
|
81
|
+
end
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
@sync_result.record_error(e)
|
|
84
|
+
CmAppLogger.error(label: '[BookMeBusV1::SyncItemAvailability] error', data: { inventory_item_id: inventory_item.id, error: e.message })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def client
|
|
90
|
+
@client ||= @integration.client
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def find_trip(trip_id)
|
|
94
|
+
SpreeCmCommissioner::Trip.find_by(id: trip_id)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def should_sync_item?(mapping)
|
|
98
|
+
mapping.new_record? || mapping.last_synced_at.nil? || mapping.last_synced_at < sync_cache_ttl.ago
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def should_sync_reserved_blocks?(external_trip)
|
|
102
|
+
# is_selectable is the field this class's own doc comment describes, but it was never
|
|
103
|
+
# actually wired up as an accessor - this used to fall back to a route_type heuristic that
|
|
104
|
+
# doesn't capture the real thing we need to know (whether BookMeBus exposes per-seat status
|
|
105
|
+
# for this trip at all, e.g. false for auto-assigned-seat trips). Keep the private_taxi
|
|
106
|
+
# exclusion too, defensively, since seat layouts don't apply there either.
|
|
107
|
+
external_trip.is_selectable && external_trip.route_type != 'private_taxi'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def find_inventory_items_for_trip(trip, on_date)
|
|
111
|
+
SpreeCmCommissioner::InventoryItem
|
|
112
|
+
.joins(variant: :product)
|
|
113
|
+
.where(spree_products: { id: trip.product_id })
|
|
114
|
+
.where(cm_inventory_items: { inventory_date: on_date })
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def find_or_initialize_item_mapping(inventory_item)
|
|
118
|
+
SpreeCmCommissioner::IntegrationMapping.find_or_initialize_by(
|
|
119
|
+
integration_id: @integration.id,
|
|
120
|
+
internal_type: 'SpreeCmCommissioner::InventoryItem',
|
|
121
|
+
internal_id: inventory_item.id,
|
|
122
|
+
external_id: "#{inventory_item.id}-#{inventory_item.inventory_date}"
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def trip_mapping_data(trip)
|
|
127
|
+
# Retrieves BookMeBus API identifiers (trip_id) from the integration mapping
|
|
128
|
+
# These are required to query the BookMeBus API for seat availability
|
|
129
|
+
mapping = SpreeCmCommissioner::IntegrationMapping.find_by(
|
|
130
|
+
integration_id: @integration.id,
|
|
131
|
+
internal_type: 'SpreeCmCommissioner::Trip',
|
|
132
|
+
internal_id: trip.id,
|
|
133
|
+
status: :active
|
|
134
|
+
)
|
|
135
|
+
return unless mapping
|
|
136
|
+
|
|
137
|
+
trip_id = mapping.external_payload&.dig('trip_id')
|
|
138
|
+
return unless trip_id
|
|
139
|
+
|
|
140
|
+
{ trip_id: trip_id }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def fetch_external_trip(trip, on_date)
|
|
144
|
+
data = trip_mapping_data(trip)
|
|
145
|
+
return unless data # can't determine - no local trip mapping to look up
|
|
146
|
+
|
|
147
|
+
origin_id = find_external_location_id(trip.origin_place)
|
|
148
|
+
destination_id = find_external_location_id(trip.destination_place)
|
|
149
|
+
return unless origin_id && destination_id # can't determine - location mapping missing
|
|
150
|
+
|
|
151
|
+
# A real ExternalClientError now propagates out of fetch_external_trips (rather than being
|
|
152
|
+
# swallowed into an indistinguishable []) up to sync_item's own rescue, which correctly
|
|
153
|
+
# leaves inventory untouched - only a genuinely successful, possibly-empty response reaches
|
|
154
|
+
# the line below, so an empty result here is a confirmed "not available", not a guess.
|
|
155
|
+
external_trips = fetch_external_trips(origin_id, destination_id, on_date)
|
|
156
|
+
found = external_trips.find { |t| t.trip_id == data[:trip_id] }
|
|
157
|
+
|
|
158
|
+
return UNAVAILABLE if found.nil? || !found.bookable?
|
|
159
|
+
|
|
160
|
+
found
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def find_external_location_id(place)
|
|
164
|
+
return unless place
|
|
165
|
+
|
|
166
|
+
vendor_place = SpreeCmCommissioner::VendorPlace.location.find_by(place_id: place.id, vendor_id: @integration.vendor.id)
|
|
167
|
+
return unless vendor_place
|
|
168
|
+
|
|
169
|
+
vendor_place.integration_mappings.find_by(integration_id: @integration.id, status: :active)&.external_id
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def fetch_external_trips(origin_id, destination_id, on_date)
|
|
173
|
+
# No rescue here - a real ExternalClientError must propagate up to sync_item's own rescue
|
|
174
|
+
# (which records/logs it and leaves inventory untouched), not collapse into the same []
|
|
175
|
+
# a genuinely successful-but-empty response produces. Conflating the two would mean a
|
|
176
|
+
# transient API failure gets treated as BookMeBus confirming the trip is unavailable,
|
|
177
|
+
# incorrectly zeroing out real inventory.
|
|
178
|
+
@sync_result.track_api_call('search_trips') do
|
|
179
|
+
client.search_trips!(
|
|
180
|
+
origin_id: origin_id,
|
|
181
|
+
destination_id: destination_id,
|
|
182
|
+
on_date: on_date
|
|
183
|
+
) || []
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Correct a stale-positive InventoryItem for a date BookMeBus has confirmed is no longer
|
|
188
|
+
# available (see fetch_external_trip's UNAVAILABLE sentinel) - without this, a date that was
|
|
189
|
+
# available on a previous sync stays sellable on BookMe+ indefinitely even after BookMeBus
|
|
190
|
+
# pulls it, since nothing else ever corrects an already-synced InventoryItem back down.
|
|
191
|
+
def zero_out_inventory!(inventory_item)
|
|
192
|
+
inventory_item.with_lock do
|
|
193
|
+
next if inventory_item.quantity_available.to_i.zero?
|
|
194
|
+
|
|
195
|
+
set_quantity_available_with_redis_sync!(inventory_item, 0)
|
|
196
|
+
@sync_result.increment_metric(:inventory, :zeroed_unavailable_date)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# If a previous sync zeroed this date out (zero_out_inventory!, because the trip was confirmed
|
|
201
|
+
# unavailable then) and BookMeBus now confirms it's bookable again with real seats, restore
|
|
202
|
+
# it. sync_stock_item alone can't be relied on for this: its delta is computed against
|
|
203
|
+
# StockItem#count_on_hand (a date-independent aggregate zero_out_inventory! never touches -
|
|
204
|
+
# deliberately, since it's not specific to this one date), so once available_seats matches
|
|
205
|
+
# that unrelated baseline again the delta comes back 0 and this InventoryItem would otherwise
|
|
206
|
+
# stay stuck at the 0 we forced it to, even though the date is genuinely bookable again.
|
|
207
|
+
def restore_inventory_if_previously_zeroed!(inventory_item, external_trip)
|
|
208
|
+
available_seats = external_trip.available_seats
|
|
209
|
+
return if available_seats.to_i.zero?
|
|
210
|
+
|
|
211
|
+
inventory_item.with_lock do
|
|
212
|
+
next unless inventory_item.quantity_available.to_i.zero?
|
|
213
|
+
|
|
214
|
+
set_quantity_available_with_redis_sync!(inventory_item, available_seats.to_i)
|
|
215
|
+
@sync_result.increment_metric(:inventory, :restored_after_unavailable_date)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# The real checkout-time availability check (Stock::AvailabilityChecker ->
|
|
220
|
+
# RedisStock::VariantCachedInventoryItemsBuilder) reads Redis, not the DB row directly - a
|
|
221
|
+
# plain `inventory_item.update!(quantity_available: ...)` looked correct here but left Redis
|
|
222
|
+
# stale, so checkout kept rejecting real purchases even after the DB value was fixed
|
|
223
|
+
# (confirmed live). `adjust_quantity!` is the sanctioned path that keeps both in sync via
|
|
224
|
+
# adjust_quantity_in_redis - but it also shifts max_capacity by the same delta, which isn't
|
|
225
|
+
# wanted here (max_capacity is the seat ceiling for whenever the trip runs, not something
|
|
226
|
+
# that should shrink/grow because today's availability changed), so restore it right after.
|
|
227
|
+
def set_quantity_available_with_redis_sync!(inventory_item, target_quantity)
|
|
228
|
+
original_max_capacity = inventory_item.max_capacity
|
|
229
|
+
delta = target_quantity - inventory_item.quantity_available
|
|
230
|
+
return if delta.zero?
|
|
231
|
+
|
|
232
|
+
inventory_item.adjust_quantity!(delta)
|
|
233
|
+
inventory_item.update_column(:max_capacity, original_max_capacity) # rubocop:disable Rails/SkipsModelValidations
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def sync_price(inventory_item, external_trip)
|
|
237
|
+
price_amount = external_trip.sellable_price
|
|
238
|
+
return if price_amount.nil?
|
|
239
|
+
|
|
240
|
+
price = inventory_item.prices.find_or_initialize_by(currency: Spree::Store.default.default_currency)
|
|
241
|
+
|
|
242
|
+
@sync_result.track(:price, price) do |tracker|
|
|
243
|
+
price.assign_attributes(
|
|
244
|
+
amount: price_amount,
|
|
245
|
+
variant_id: inventory_item.variant_id,
|
|
246
|
+
inventory_item_id: inventory_item.id
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
tracker.save_if_changed!(price)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
sync_nationality_pricing(inventory_item, external_trip)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Keeps the vendor's local/non-local discount PricingModel current every time this trip's
|
|
256
|
+
# price is refreshed - additive to sellable_price above (which stays the base everyone pays
|
|
257
|
+
# by default), not a replacement for it. Never lets a pricing-model hiccup break the
|
|
258
|
+
# availability/price sync it's piggybacking on.
|
|
259
|
+
def sync_nationality_pricing(inventory_item, external_trip)
|
|
260
|
+
trip = inventory_item.variant&.trip
|
|
261
|
+
return unless trip&.route
|
|
262
|
+
|
|
263
|
+
SyncNationalityPricing.new(
|
|
264
|
+
route: trip.route,
|
|
265
|
+
vendor: @integration.vendor,
|
|
266
|
+
local_price: external_trip.local_price,
|
|
267
|
+
non_local_price: external_trip.non_local_price
|
|
268
|
+
).call
|
|
269
|
+
rescue StandardError => e
|
|
270
|
+
@sync_result.record_error(e)
|
|
271
|
+
CmAppLogger.error(label: '[BookMeBusV1::SyncItemAvailability] nationality pricing sync failed', data: { error: e.message })
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def sync_stock_item(inventory_item, external_trip)
|
|
275
|
+
sync_inventory = SpreeCmCommissioner::Integrations::BookMeBusV1::Polling::SyncInventory.new(
|
|
276
|
+
sync_result: @sync_result,
|
|
277
|
+
vendor: inventory_item.variant.vendor
|
|
278
|
+
)
|
|
279
|
+
sync_inventory.call(variant: inventory_item.variant, external_trip: external_trip)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def sync_reserved_blocks(external_trip, _on_date, inventory_item)
|
|
283
|
+
external_seat_layout = fetch_external_seat_layout(external_trip)
|
|
284
|
+
return unless external_seat_layout
|
|
285
|
+
|
|
286
|
+
trip_seat_layout = inventory_item.variant.trip&.seat_layout
|
|
287
|
+
return unless trip_seat_layout
|
|
288
|
+
|
|
289
|
+
sync_reserved_blocks_from_layout(trip_seat_layout, external_seat_layout, inventory_item)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def fetch_external_seat_layout(external_trip)
|
|
293
|
+
@sync_result.track_api_call('get_seat_layout') do
|
|
294
|
+
client.get_seat_layout!(
|
|
295
|
+
trip_id: external_trip.id,
|
|
296
|
+
from_stop_id: external_trip.from_stop_id,
|
|
297
|
+
to_stop_id: external_trip.to_stop_id,
|
|
298
|
+
on_date: external_trip.on_date || Time.zone.today.to_s
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
rescue SpreeCmCommissioner::Integrations::ExternalClientError => e
|
|
302
|
+
CmAppLogger.error(label: '[BookMeBusV1::SyncItemAvailability] get_seat_layout failed', data: { error: e.message })
|
|
303
|
+
@sync_result.record_error(e)
|
|
304
|
+
nil
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def sync_reserved_blocks_from_layout(seat_layout, external_seat_layout, inventory_item)
|
|
308
|
+
external_seats_by_label = external_seat_layout.seats.index_by(&:label)
|
|
309
|
+
|
|
310
|
+
seat_layout.blocks.each do |block|
|
|
311
|
+
next unless block.sellable?
|
|
312
|
+
|
|
313
|
+
external_seat = external_seats_by_label[block.label]
|
|
314
|
+
next unless external_seat
|
|
315
|
+
|
|
316
|
+
reserved_block = SpreeCmCommissioner::ReservedBlock.find_or_initialize_by(
|
|
317
|
+
inventory_item: inventory_item,
|
|
318
|
+
block: block
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if external_seat.booked?
|
|
322
|
+
next if reserved_block.reserved?
|
|
323
|
+
|
|
324
|
+
@sync_result.track(:reserved_block, reserved_block) do |tracker|
|
|
325
|
+
reserved_block.assign_attributes(
|
|
326
|
+
status: :reserved,
|
|
327
|
+
expired_at: nil,
|
|
328
|
+
inventory_date: inventory_item.inventory_date,
|
|
329
|
+
guest: nil,
|
|
330
|
+
updated_by: nil
|
|
331
|
+
)
|
|
332
|
+
tracker.save_if_changed!(reserved_block)
|
|
333
|
+
end
|
|
334
|
+
elsif reserved_block.persisted?
|
|
335
|
+
# Locked, and guest_id re-checked after acquiring the lock, so a real customer hold
|
|
336
|
+
# being created concurrently on this exact block can't be destroyed out from under them.
|
|
337
|
+
reserved_block.with_lock do
|
|
338
|
+
next unless reserved_block.guest_id.nil?
|
|
339
|
+
|
|
340
|
+
@sync_result.track(:reserved_block, reserved_block) do |_tracker|
|
|
341
|
+
reserved_block.destroy!
|
|
342
|
+
@sync_result.increment_metric(:reserved_block, :deleted)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def update_last_sync_at(mapping)
|
|
350
|
+
mapping.last_synced_at = Time.current
|
|
351
|
+
mapping.save! if mapping.changed?
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|