test_candidate_selection_service.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. from __future__ import annotations
  2. from datetime import date, timedelta
  3. from app.schemas.travel_request import (
  4. FlightPreferences,
  5. HotelPreferences,
  6. TravelRequest,
  7. )
  8. from app.services.candidate_selection_service import (
  9. CandidateSelectionService,
  10. )
  11. def build_request(
  12. *,
  13. priority: str = "balanced",
  14. max_stops: int | None = None,
  15. ) -> TravelRequest:
  16. departure_date = (
  17. date.today() + timedelta(days=10)
  18. )
  19. return TravelRequest(
  20. origin_city="上海",
  21. destination_city="成都",
  22. departure_date=departure_date,
  23. return_date=(
  24. departure_date
  25. + timedelta(days=3)
  26. ),
  27. adults=2,
  28. flight_preferences=(
  29. FlightPreferences(
  30. priority=priority,
  31. max_stops=max_stops,
  32. )
  33. ),
  34. hotel_preferences=(
  35. HotelPreferences(
  36. max_price_per_night=600,
  37. minimum_rating=4.5,
  38. )
  39. ),
  40. )
  41. def test_price_priority_prefers_lower_price() -> None:
  42. service = CandidateSelectionService()
  43. flights = [
  44. {
  45. "option_id": "cheap",
  46. "price": 700,
  47. "duration_minutes": 300,
  48. "stop_count": 1,
  49. "departure_airport": "PVG",
  50. "arrival_airport": "TFU",
  51. "departure_time": (
  52. "2030-01-01 08:00"
  53. ),
  54. "arrival_time": (
  55. "2030-01-01 13:00"
  56. ),
  57. },
  58. {
  59. "option_id": "fast",
  60. "price": 900,
  61. "duration_minutes": 120,
  62. "stop_count": 0,
  63. "departure_airport": "PVG",
  64. "arrival_airport": "TFU",
  65. "departure_time": (
  66. "2030-01-01 09:00"
  67. ),
  68. "arrival_time": (
  69. "2030-01-01 11:00"
  70. ),
  71. },
  72. ]
  73. ranked, warnings = service.rank_flights(
  74. flights,
  75. build_request(priority="price"),
  76. )
  77. assert ranked[0].option_id == "cheap"
  78. assert warnings == []
  79. def test_max_stops_filters_connections() -> None:
  80. service = CandidateSelectionService()
  81. flights = [
  82. {
  83. "option_id": "direct",
  84. "price": 900,
  85. "duration_minutes": 150,
  86. "stop_count": 0,
  87. },
  88. {
  89. "option_id": "transfer",
  90. "price": 700,
  91. "duration_minutes": 280,
  92. "stop_count": 1,
  93. },
  94. ]
  95. ranked, _ = service.rank_flights(
  96. flights,
  97. build_request(max_stops=0),
  98. )
  99. assert len(ranked) == 1
  100. assert ranked[0].option_id == "direct"
  101. def test_hotel_hard_filters() -> None:
  102. service = CandidateSelectionService()
  103. hotels = [
  104. {
  105. "hotel_id": "valid",
  106. "name": "酒店A",
  107. "price_per_night": 500,
  108. "overall_rating": 4.6,
  109. "location_rating": 4.5,
  110. },
  111. {
  112. "hotel_id": "too-expensive",
  113. "name": "酒店B",
  114. "price_per_night": 800,
  115. "overall_rating": 4.8,
  116. "location_rating": 4.7,
  117. },
  118. {
  119. "hotel_id": "low-rating",
  120. "name": "酒店C",
  121. "price_per_night": 400,
  122. "overall_rating": 4.0,
  123. "location_rating": 4.2,
  124. },
  125. ]
  126. ranked, _ = service.rank_hotels(
  127. hotels,
  128. build_request(),
  129. )
  130. assert len(ranked) == 1
  131. assert ranked[0].hotel_id == "valid"
  132. def test_round_trip_uses_return_quote() -> None:
  133. service = CandidateSelectionService()
  134. combinations = [
  135. {
  136. "outbound": {
  137. "option_id": "out-1",
  138. "price": 1000,
  139. "duration_minutes": 180,
  140. "stop_count": 0,
  141. "currency": "CNY",
  142. },
  143. "return_flight": {
  144. "option_id": "ret-1",
  145. "price": 1800,
  146. "duration_minutes": 190,
  147. "stop_count": 0,
  148. "currency": "CNY",
  149. },
  150. }
  151. ]
  152. ranked = (
  153. service.rank_round_trip_combinations(
  154. combinations,
  155. build_request(),
  156. )
  157. )
  158. assert len(ranked) == 1
  159. # 不是1000 + 1800。
  160. assert ranked[0].quoted_price == 1800