# test_flight_thr_mhd_e2e.py import requests import pytest from conftest import ( BASE_URL, TEST_EMAIL, TEST_PHONE_NUMBER, TRAVELER_FIRST_NAME, TRAVELER_LAST_NAME, TRAVELER_GENDER, TRAVELER_NATIONAL_CODE, TRAVELER_DATE_OF_BIRTH, TRAVELER_PLACE_OF_BIRTH, ) # ===================================================================================== # IATA / Airport discovery tests # ===================================================================================== def test_iata_domestic_default_list(auth_header): """ Validate the default domestic IATA list. Scenario: • Perform GET /customer/iata with: - keyword = "" (no filter) - is_international = false • Assert: - Response is HTTP 200. - "data" is a non-empty list. - Both THR and MHD are present in the list (core domestic airports). """ res = requests.get( f"{BASE_URL}/customer/iata", params={ "keyword": "", "is_international": "false", "other_iata_code": "", }, headers=auth_header, timeout=10, ) assert res.status_code == 200, "Expected HTTP 200 for domestic IATA default list" data = res.json() assert "data" in data and isinstance(data["data"], list), "Response should contain a 'data' list" assert data["data"], "Domestic IATA list should not be empty" codes = {item["code"] for item in data["data"]} assert "THR" in codes, "THR should be present in domestic IATA list" assert "MHD" in codes, "MHD should be present in domestic IATA list" def test_iata_search_origin_tehran(auth_header): """ Validate origin search for Tehran. Scenario: • Perform GET /customer/iata with: - keyword = "tehran" - is_international = false • Assert: - Response is HTTP 200. - "data" is a non-empty list. - THR is returned as one of the matches. """ res = requests.get( f"{BASE_URL}/customer/iata", params={ "keyword": "tehran", "is_international": "false", "other_iata_code": "", }, headers=auth_header, timeout=10, ) assert res.status_code == 200, "Expected HTTP 200 for IATA search with keyword 'tehran'" data = res.json() assert "data" in data and isinstance(data["data"], list), "Response should contain a 'data' list" assert data["data"], "IATA search(tehran) should not be empty" codes = {item["code"] for item in data["data"]} assert "THR" in codes, "THR should be present in IATA search result for 'tehran'" def test_iata_search_destination_mashhad_excluding_origin(auth_header): """ Validate destination search for Mashhad while excluding origin. Scenario: • Perform GET /customer/iata with: - keyword = "mashhad" - is_international = false - other_iata_code = "THR" • Assert: - Response is HTTP 200. - "data" is a non-empty list. - MHD is present in results. - THR is explicitly NOT present (origin and destination must differ). """ res = requests.get( f"{BASE_URL}/customer/iata", params={ "keyword": "mashhad", "is_international": "false", "other_iata_code": "THR", }, headers=auth_header, timeout=10, ) assert res.status_code == 200, "Expected HTTP 200 for IATA search with keyword 'mashhad'" data = res.json() assert "data" in data and isinstance(data["data"], list), "Response should contain a 'data' list" assert data["data"], "IATA search(mashhad) should not be empty" codes = {item["code"] for item in data["data"]} assert "MHD" in codes, "MHD should be in Mashhad search result" assert "THR" not in codes, "THR must NOT appear when other_iata_code=THR (origin must be excluded)" # ===================================================================================== # General and refund rules # ===================================================================================== def test_general_and_refund_rules(auth_header): """ Validate general rules and refund rules configuration. Endpoints under test: • GET /customer/setting/get/general-rules • GET /customer/setting/get/refund-rules Minimal structure validation: • general-rules: - "data.general-rules" exists and is non-empty (typically an HTML string). • refund-rules: - "data.refund_rules.domestic.data" is a non-empty list of items with "description" and "penalty" fields. - "data.refund_rules.international.data" behaves similarly. """ # --- general-rules --- res_gen = requests.get( f"{BASE_URL}/customer/setting/get/general-rules", headers=auth_header, timeout=10, ) assert res_gen.status_code == 200, "Expected HTTP 200 for general-rules endpoint" data_gen = res_gen.json() assert "data" in data_gen, "general-rules response must contain 'data' field" assert data_gen["data"].get("general-rules"), "general-rules must be present and non-empty" # --- refund-rules --- res_ref = requests.get( f"{BASE_URL}/customer/setting/get/refund-rules", headers=auth_header, timeout=10, ) assert res_ref.status_code == 200, "Expected HTTP 200 for refund-rules endpoint" data_ref = res_ref.json() refund_rules = data_ref.get("data", {}).get("refund_rules") assert refund_rules is not None, "refund_rules should exist under data.refund_rules" for key in ("domestic", "international"): section = refund_rules.get(key) assert section is not None, f"refund_rules.{key} should exist" items = section.get("data") assert isinstance(items, list) and items, f"refund_rules.{key}.data should be a non-empty list" first = items[0] assert "description" in first, f"refund_rules.{key}.data[0] must contain 'description'" assert "penalty" in first, f"refund_rules.{key}.data[0] must contain 'penalty'" # ===================================================================================== # E2E: THR → MHD single adult flow (search → book → issue → refund) # ===================================================================================== @pytest.mark.e2e def test_thr_mhd_single_adult_book_issue_refund_e2e( auth_header, thr_to_mhd_search_params, api_wait, ): """ End-to-end happy path for a single adult THR → MHD domestic flight. High-level flow: 1) Search for THR → MHD flights for (today + 7 days). 2) Select the first itinerary (sorted by lowest-price). 3) Book the itinerary for a single Adult passenger. 4) Verify that the user's wallet credit is >= booking price. 5) Issue the ticket using wallet credit (pay_with_credit = true). 6) Retrieve customer tickets and locate the ticket associated with this booking. 7) Call refund-penalty and validate: - No penalty (penalty_percent == 0). - Full amount is refundable (amount_that_should_pay_back_to_passenger == booking_price). 8) Perform the actual refund. 9) Retrieve tickets again and assert the ticket's refund_status reflects the refund. """ # ------------------------------------------------------------------------- # 1) Search THR → MHD # ------------------------------------------------------------------------- print("\n[STEP 1] Searching for THR → MHD itineraries ...") search_payload = { "origin_destination_option_list": [ { "OriginIataCode": thr_to_mhd_search_params["origin_iata"], "DestinationIataCode": thr_to_mhd_search_params["destination_iata"], "FlightDate": thr_to_mhd_search_params["flight_date"], } ], "page": thr_to_mhd_search_params["page"], "page_size": thr_to_mhd_search_params["page_size"], "sort_option": thr_to_mhd_search_params["sort_option"], } res_search = requests.post( f"{BASE_URL}/customer/flight/search", json=search_payload, headers={**auth_header, "Content-Type": "application/json"}, timeout=15, ) assert res_search.status_code == 200, "Expected HTTP 200 for flight search" data_search = res_search.json() itinerary_list = data_search.get("itinerary_list") or [] assert itinerary_list, "No itineraries returned for THR→MHD search" itinerary = itinerary_list[0] flight_id = itinerary.get("flight_id") assert flight_id, "flight_id must be present for the selected itinerary" # Minimal segment validation – we assert the route is THR → MHD. segments = itinerary.get("flight_segment_list") or [] assert segments, "flight_segment_list should not be empty" first_segment = segments[0] assert first_segment.get("origin_iata_code") == "THR", "Segment origin_iata_code must be THR" assert first_segment.get("destination_iata_code") == "MHD", "Segment destination_iata_code must be MHD" # Ticket must not be free – ensure we are dealing with a priced product. price_info = itinerary.get("price_info") or {} total_fare = price_info.get("total_fare") assert isinstance(total_fare, (int, float)) and total_fare > 0, "total_fare must be a positive number" print( f"[STEP 1] Selected itinerary flight_id={flight_id} " f"with total_fare={total_fare} and {len(segments)} segment(s)." ) api_wait(0.5) # ------------------------------------------------------------------------- # 2) Book the selected itinerary for a single Adult passenger # ------------------------------------------------------------------------- print("[STEP 2] Booking selected itinerary for a single adult passenger ...") book_payload = { "country_code": "98", "email": TEST_EMAIL, "flight_id": flight_id, "phone_number": TEST_PHONE_NUMBER, "travelers": [ { "date_of_birth": TRAVELER_DATE_OF_BIRTH, "first_name": TRAVELER_FIRST_NAME, "gender": TRAVELER_GENDER, "last_name": TRAVELER_LAST_NAME, "national_code": TRAVELER_NATIONAL_CODE, "place_of_birth": TRAVELER_PLACE_OF_BIRTH, "type": "ADT", } ], } res_book = requests.post( f"{BASE_URL}/customer/flight/book", json=book_payload, headers={**auth_header, "Content-Type": "application/json"}, timeout=15, ) assert res_book.status_code == 200, "Expected HTTP 200 for flight book" data_book = res_book.json() booking_id = data_book.get("id") expires_at = data_book.get("expires_at") booking_price = data_book.get("price") assert isinstance(booking_id, int) and booking_id > 0, "Booking id must be a positive integer" assert isinstance(expires_at, int) and expires_at > 0, "Booking expiry (minutes) must be a positive integer" assert isinstance(booking_price, (int, float)) and booking_price > 0, "Booking price must be > 0" print( f"[STEP 2] Booking created successfully. " f"id={booking_id}, price={booking_price}, expires_in={expires_at} minutes." ) api_wait(1.0) # ------------------------------------------------------------------------- # 3) Validate wallet credit is sufficient for the booking price # ------------------------------------------------------------------------- print("[STEP 3] Verifying wallet credit is sufficient for booking price ...") res_credit = requests.get( f"{BASE_URL}/admin/user/credit", headers=auth_header, timeout=10, ) assert res_credit.status_code == 200, "Expected HTTP 200 for user credit endpoint" data_credit = res_credit.json() credit = data_credit.get("credit") assert isinstance(credit, (int, float)), "credit must be numeric" assert credit >= booking_price, "User credit is not sufficient for booking price" print(f"[STEP 3] Wallet credit={credit} is sufficient for booking_price={booking_price}.") api_wait(0.5) # ------------------------------------------------------------------------- # 4) Issue the ticket using wallet credit # ------------------------------------------------------------------------- print("[STEP 4] Issuing the ticket using wallet credit ...") issue_payload = { "pay_with_credit": True, "pay_with_flight_coin": False, "payment_gateway": "", "pay_for": "ticket", "platform": "mobile", } res_issue = requests.post( f"{BASE_URL}/customer/flight/book/{booking_id}/issue", json=issue_payload, headers={**auth_header, "Content-Type": "application/json"}, timeout=30, ) assert res_issue.status_code == 204, "Expected HTTP 204 (No Content) for ticket issue" print("[STEP 4] Ticket issue request completed with HTTP 204. Backend is finalizing issuance ...") api_wait(2.0) # ------------------------------------------------------------------------- # 5) Retrieve tickets and locate the one associated with this booking # ------------------------------------------------------------------------- print("[STEP 5] Fetching active tickets and locating the issued one ...") res_tickets_after_issue = requests.get( f"{BASE_URL}/customer/ticket", params={ "page": 1, "page_size": 20, "is_past": "false", "statuses": "confirmed", "sort": "issue_date", "sort_dir": "DESC", }, headers=auth_header, timeout=15, ) assert res_tickets_after_issue.status_code == 200, "Expected HTTP 200 for ticket list after issue" tickets_data = res_tickets_after_issue.json() ticket_records = tickets_data.get("data") or [] assert ticket_records, "Ticket list after issue should not be empty" # For simplicity, locate the first ticket whose total matches our booking price. ticket = None for t in ticket_records: if t.get("total") == booking_price: ticket = t break assert ticket is not None, "Issued ticket with matching price not found in ticket list" ticket_id = ticket["id"] print(f"[STEP 5] Located ticket id={ticket_id} with total={ticket.get('total')}.") travelers = ticket.get("travelers") or [] assert travelers, "Ticket must have at least one traveler" traveler = travelers[0] # Single adult in this scenario ticket_traveler_id = traveler["id"] segments = ticket.get("segments") or [] assert segments, "Ticket must have at least one segment" segment = segments[0] ticket_segment_id = segment["id"] print( f"[STEP 5] Using ticket_segment_id={ticket_segment_id}, " f"ticket_traveler_id={ticket_traveler_id} for refund operations." ) api_wait(1.5) # ------------------------------------------------------------------------- # 6) Call refund-penalty and validate zero-penalty scenario # ------------------------------------------------------------------------- print("[STEP 6] Calculating refund penalty and validating zero-penalty scenario ...") refund_penalty_payload = { "ticket_segment_id": ticket_segment_id, "ticket_traveler_ids": [ticket_traveler_id], } res_refund_penalty = requests.post( f"{BASE_URL}/customer/flight/refund-penalty", json=refund_penalty_payload, headers={**auth_header, "Content-Type": "application/json"}, timeout=15, ) assert res_refund_penalty.status_code == 200, "Expected HTTP 200 for refund-penalty endpoint" data_refund_penalty = res_refund_penalty.json() penalty_items = data_refund_penalty.get("data") or [] assert penalty_items, "refund-penalty data list must not be empty" pitem = penalty_items[0] payable = pitem.get("payable") amount_back = pitem.get("amount_that_should_pay_back_to_passenger") penalty_percent = pitem.get("penalty_percent") returned_traveler_id = pitem.get("ticket_traveler_id") assert isinstance(payable, (int, float)) and payable > 0, "payable must be > 0" assert isinstance(amount_back, (int, float)) and amount_back > 0, "amount_that_should_pay_back_to_passenger must be > 0" assert penalty_percent == 0, "This E2E scenario is expected to have zero penalty" assert amount_back == booking_price, "Full amount should be refunded to passenger" assert returned_traveler_id == ticket_traveler_id, "ticket_traveler_id must match the requested one" print( f"[STEP 6] Refund penalty validated. penalty_percent={penalty_percent}, " f"amount_back={amount_back}, payable={payable}." ) api_wait(0.5) # ------------------------------------------------------------------------- # 7) Perform the actual refund # ------------------------------------------------------------------------- print("[STEP 7] Performing actual refund ...") refund_payload = { "ticket_segment_id": ticket_segment_id, "ticket_traveler_ids": [ticket_traveler_id], } res_refund = requests.post( f"{BASE_URL}/customer/flight/refund", json=refund_payload, headers={**auth_header, "Content-Type": "application/json"}, timeout=30, ) assert res_refund.status_code == 200, "Expected HTTP 200 for refund endpoint" print("[STEP 7] Refund request accepted. Waiting for status to converge ...") api_wait(2.0) # ------------------------------------------------------------------------- # 8) Verify that the ticket is now marked as refunded/refunded_partially # ------------------------------------------------------------------------- print("[STEP 8] Fetching tickets after refund and validating refund_status ...") res_tickets_after_refund = requests.get( f"{BASE_URL}/customer/ticket", params={ "page": 1, "page_size": 20, "is_past": "true", "statuses": "confirmed,refunded_partially,refunded", "sort": "issue_date", "sort_dir": "DESC", }, headers=auth_header, timeout=15, ) assert res_tickets_after_refund.status_code == 200, "Expected HTTP 200 for ticket list after refund" tickets_after_ref_data = res_tickets_after_refund.json() tickets_after_ref = tickets_after_ref_data.get("data") or [] refunded_ticket = None for t in tickets_after_ref: if t.get("id") == ticket_id: refunded_ticket = t break assert refunded_ticket is not None, "Refunded ticket not found in ticket list after refund" refund_status = refunded_ticket.get("refund_status") assert refund_status in ("refunded", "refunded_partially"), ( f"Unexpected refund_status for ticket {ticket_id}: {refund_status}" ) print( f"[STEP 8] Refund flow completed successfully for ticket_id={ticket_id} " f"with refund_status={refund_status}.\n" )