travelLoader = $this->createMock(TravelLoader::class); $this->hotelLoader = $this->createMock(HotelLoader::class); $this->pickupLoader = $this->createMock(PickupLoader::class); $this->insuranceLoader = $this->createMock(InsuranceLoader::class); $this->apiClient = $this->createMock(ApiClient::class); $this->cache = $this->createMock(CacheInterface::class); $this->logger = $this->createMock(LoggerInterface::class); $this->travelSnapshotService = $this->createMock(TravelSnapshotService::class); $this->service = new TravelDataService( $this->travelLoader, $this->hotelLoader, $this->pickupLoader, $this->insuranceLoader, $this->apiClient, $this->cache, $this->logger, $this->travelSnapshotService, false, // preferRemote true, // enableFallback ); } public function testGetTravelDataFromXmlSuccess(): void { $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $this->pickupLoader ->expects($this->once()) ->method('patchPickupsDetails') ->with($travel); $this->hotelLoader ->expects($this->once()) ->method('patchHotelDetails') ->with($travel); $result = $this->service->getTravelDataFromXml($dateId, $hotelId); $this->assertSame($travel, $result); } public function testGetTravelDataFromXmlNotFound(): void { $dateId = 12345; $hotelId = 67890; $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willThrowException(new TravelNotFoundException($dateId)); $this->pickupLoader ->expects($this->never()) ->method('patchPickupsDetails'); $this->hotelLoader ->expects($this->never()) ->method('patchHotelDetails'); $this->expectException(TravelNotFoundException::class); $this->service->getTravelDataFromXml($dateId, $hotelId); } public function testGetTravelDataFromApiSuccess(): void { $dateId = 12345; $hotelId = 67890; $productId = 555; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelLoader ->expects($this->once()) ->method('mapDateIdToProductId') ->with($dateId) ->willReturn($productId); $this->apiClient ->expects($this->once()) ->method('getTravelData') ->with($productId, $hotelId) ->willReturn($travel); $result = $this->service->getTravelDataFromApi($dateId, $hotelId); $this->assertSame($travel, $result); } public function testGetTravelDataFromApiCannotMapDateId(): void { $dateId = 12345; $hotelId = 67890; $this->travelLoader ->expects($this->once()) ->method('mapDateIdToProductId') ->with($dateId) ->willReturn(null); $this->apiClient ->expects($this->never()) ->method('getTravelData'); $result = $this->service->getTravelDataFromApi($dateId, $hotelId); $this->assertNull($result); } public function testExistsLocallyTrueFromSnapshot(): void { $dateId = 12345; $hotelId = 67890; $this->travelSnapshotService ->expects($this->once()) ->method('exists') ->with($dateId, $hotelId) ->willReturn(true); $this->travelLoader ->expects($this->never()) ->method('generateFilesMap'); $result = $this->service->existsLocally($dateId, $hotelId); $this->assertTrue($result); } public function testExistsLocallyTrueFromXmlMap(): void { $dateId = 12345; $hotelId = 67890; $mapping = [ $dateId => [ 'id' => $dateId, 'hotels' => [ $hotelId => ['id' => $hotelId, 'name' => 'Test Hotel'], ], ], ]; $this->travelSnapshotService ->expects($this->once()) ->method('exists') ->with($dateId, $hotelId) ->willReturn(false); $this->travelLoader ->expects($this->once()) ->method('generateFilesMap') ->willReturn($mapping); $result = $this->service->existsLocally($dateId, $hotelId); $this->assertTrue($result); } public function testExistsLocallyFalseNoTravel(): void { $dateId = 12345; $hotelId = 67890; $mapping = []; $this->travelSnapshotService ->expects($this->once()) ->method('exists') ->with($dateId, $hotelId) ->willReturn(false); $this->travelLoader ->expects($this->once()) ->method('generateFilesMap') ->willReturn($mapping); $result = $this->service->existsLocally($dateId, $hotelId); $this->assertFalse($result); } public function testExistsLocallyFalseNoHotel(): void { $dateId = 12345; $hotelId = 67890; $mapping = [ $dateId => [ 'id' => $dateId, 'hotels' => [], ], ]; $this->travelSnapshotService ->expects($this->once()) ->method('exists') ->with($dateId, $hotelId) ->willReturn(false); $this->travelLoader ->expects($this->once()) ->method('generateFilesMap') ->willReturn($mapping); $result = $this->service->existsLocally($dateId, $hotelId); $this->assertFalse($result); } public function testGetAvailableSources(): void { $dateId = 12345; $hotelId = 67890; $productId = 555; $mapping = [ $dateId => [ 'id' => $dateId, 'hotels' => [ $hotelId => ['id' => $hotelId, 'name' => 'Test Hotel'], ], ], ]; $this->travelSnapshotService ->expects($this->once()) ->method('exists') ->with($dateId, $hotelId) ->willReturn(false); $this->travelLoader ->expects($this->once()) ->method('generateFilesMap') ->willReturn($mapping); $this->travelLoader ->expects($this->once()) ->method('mapDateIdToProductId') ->with($dateId) ->willReturn($productId); $result = $this->service->getAvailableSources($dateId, $hotelId); $this->assertEquals([ 'local' => true, 'remote' => true, ], $result); } public function testGetTravelDataFromLocalPrefersSnapshot(): void { $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willReturn($travel); $this->travelLoader ->expects($this->never()) ->method('loadById'); $result = $this->service->getTravelDataFromLocal($dateId, $hotelId); $this->assertSame($travel, $result); } public function testGetTravelDataFromLocalReplacesSnapshotInsurancesWithLoaderData(): void { $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; // Snapshot-provided insurances are intentionally replaced by InsuranceLoader data. $staleSnapshotInsurance = new Insurance(); $staleSnapshotInsurance->id = 'stale'; $travel->insurances = [$staleSnapshotInsurance]; $individual = new Insurance(); $individual->id = '10'; $individual->package = false; $package = new Insurance(); $package->id = '20'; $package->package = true; $package->containedInsuranceIds = ['10']; // InsuranceParser populates containedInsurances at parse time; loadAll() returns hydrated data. $package->containedInsurances = [$individual]; $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willReturn($travel); $this->insuranceLoader ->expects($this->once()) ->method('loadAll') ->willReturn([$individual, $package]); $result = $this->service->getTravelDataFromLocal($dateId, $hotelId); $this->assertSame($travel, $result); $this->assertSame([$individual, $package], array_values($travel->insurances)); $this->assertCount(1, $package->containedInsurances); $this->assertSame($individual, $package->containedInsurances[0]); } public function testGetTravelDataFromLocalFallsBackToXmlWhenSnapshotMissing(): void { $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willReturn(null); $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $this->pickupLoader ->expects($this->once()) ->method('patchPickupsDetails') ->with($travel); $this->hotelLoader ->expects($this->once()) ->method('patchHotelDetails') ->with($travel); $this->travelSnapshotService ->expects($this->once()) ->method('upsertFromTravel') ->with($travel); $result = $this->service->getTravelDataFromLocal($dateId, $hotelId); $this->assertSame($travel, $result); } public function testGetTravelDataFromLocalAttachesLoaderInsurancesFromXmlFallback(): void { $dateId = 12345; $hotelId = 67890; $individual = new Insurance(); $individual->id = '10'; $individual->package = false; $package = new Insurance(); $package->id = '20'; $package->package = true; $package->containedInsuranceIds = ['10']; // InsuranceParser populates containedInsurances at parse time; loadAll() returns hydrated data. $package->containedInsurances = [$individual]; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willReturn(null); $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $this->pickupLoader->expects($this->once())->method('patchPickupsDetails')->with($travel); $this->hotelLoader->expects($this->once())->method('patchHotelDetails')->with($travel); $this->insuranceLoader->expects($this->once())->method('loadAll')->willReturn([$individual, $package]); $this->travelSnapshotService->expects($this->once())->method('upsertFromTravel')->with($travel); $result = $this->service->getTravelDataFromLocal($dateId, $hotelId); $this->assertSame($travel, $result); $this->assertCount(1, $package->containedInsurances); $this->assertSame($individual, $package->containedInsurances[0]); } public function testGetTravelDataDelegatesToLoadUncached(): void { $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->cache ->expects($this->never()) ->method('get'); $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willReturn(null); $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $this->pickupLoader ->expects($this->once()) ->method('patchPickupsDetails') ->with($travel); $this->hotelLoader ->expects($this->once()) ->method('patchHotelDetails') ->with($travel); $result = $this->service->getTravelData($dateId, $hotelId, false); $this->assertSame($travel, $result); } public function testGetTravelDataFromLocalPropagatesTravelNotFoundFromXmlFallback(): void { $dateId = 12345; $hotelId = 67890; $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willReturn(null); $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willThrowException(new TravelNotFoundException($dateId)); $this->expectException(TravelNotFoundException::class); $this->service->getTravelDataFromLocal($dateId, $hotelId); } public function testSnapshotLoadFailureFallsBackToXml(): void { $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelSnapshotService ->expects($this->once()) ->method('loadTravel') ->with($dateId, $hotelId) ->willThrowException(new \RuntimeException('DB connection failed')); $this->logger ->expects($this->once()) ->method('warning') ->with('Snapshot lookup failed, falling back to XML', $this->arrayHasKey('dateId')); $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $result = $this->service->getTravelDataFromLocal($dateId, $hotelId); $this->assertSame($travel, $result); } public function testSnapshotPersistenceFailureDoesNotDiscardXmlTravel(): void { $dateId = 12345; $hotelId = 67890; $travel = new Travel(); $travel->id = $dateId; $travel->hotelId = $hotelId; $this->travelLoader ->expects($this->once()) ->method('loadById') ->with($dateId, $hotelId) ->willReturn($travel); $this->travelSnapshotService ->expects($this->once()) ->method('upsertFromTravel') ->willThrowException(new \RuntimeException('DB write failed')); $this->logger ->expects($this->once()) ->method('warning') ->with('Failed to persist travel snapshot after XML load', $this->arrayHasKey('dateId')); $result = $this->service->getTravelDataFromXml($dateId, $hotelId); $this->assertNotNull($result); $this->assertSame($travel, $result); } }