feat: delete orphaned future snapshots after xml sync
This commit is contained in:
@@ -7,6 +7,7 @@ namespace App\Command;
|
||||
use App\BusProNet\Model\XmlExportInfo;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use App\Service\TravelDataService;
|
||||
use App\Service\TravelSnapshotService;
|
||||
use League\Flysystem\FilesystemException;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use League\Flysystem\StorageAttributes;
|
||||
@@ -41,6 +42,7 @@ class BpnXmlSyncCommand extends Command
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly TravelLoader $travelLoader,
|
||||
private readonly TravelDataService $travelDataService,
|
||||
private readonly TravelSnapshotService $snapshotService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -124,6 +126,7 @@ class BpnXmlSyncCommand extends Command
|
||||
|
||||
$xmlFileMap = null;
|
||||
$snapshotResult = null;
|
||||
$orphanedDeleted = null;
|
||||
try {
|
||||
$xmlFileMap = $this->travelLoader->generateFilesMap();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -142,18 +145,24 @@ class BpnXmlSyncCommand extends Command
|
||||
);
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
$orphanedDeleted = $this->snapshotService->purgeOrphanedFutureSnapshots(array_keys($xmlFileMap));
|
||||
}
|
||||
|
||||
$successMessage = sprintf('Sync complete: %d files updated, %d files deleted', $syncResult['updated'], $syncResult['deleted']);
|
||||
if (null !== $snapshotResult) {
|
||||
$successMessage .= sprintf(', snapshots: %d processed, %d failed', $snapshotResult['processed'], $snapshotResult['failed']);
|
||||
}
|
||||
if (null !== $orphanedDeleted) {
|
||||
$successMessage .= sprintf(', orphaned deleted: %d', $orphanedDeleted);
|
||||
}
|
||||
$io->success($successMessage);
|
||||
$this->logger->info('XML sync completed', [
|
||||
'files_updated' => $syncResult['updated'],
|
||||
'files_deleted' => $syncResult['deleted'],
|
||||
'snapshots_processed' => $snapshotResult['processed'] ?? null,
|
||||
'snapshots_failed' => $snapshotResult['failed'] ?? null,
|
||||
'orphaned_deleted' => $orphanedDeleted,
|
||||
'remote_timestamp' => $remoteInfo->lastTransfer->format('c'),
|
||||
]);
|
||||
|
||||
|
||||
@@ -126,4 +126,31 @@ class TravelSnapshotRepository extends ServiceEntityRepository
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes future snapshots whose dateId is no longer present in the XML export.
|
||||
*
|
||||
* Only snapshots with dateFrom > today are removed; in-progress and past travels
|
||||
* are intentionally left intact so the extended-availability fallback can still serve them.
|
||||
*
|
||||
* @param array<int> $activeXmlDateIds dateIds currently present in the XML file map
|
||||
*/
|
||||
public function deleteOrphanedFutureSnapshots(array $activeXmlDateIds, \DateTimeImmutable $today): int
|
||||
{
|
||||
// An empty list means the XML export is unavailable or empty — refuse to purge
|
||||
// anything rather than deleting all future snapshots due to NOT IN ([]) = 1=1.
|
||||
if ([] === $activeXmlDateIds) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) $this->createQueryBuilder('s')
|
||||
->delete()
|
||||
->where('s.dateFrom IS NOT NULL')
|
||||
->andWhere('s.dateFrom > :today')
|
||||
->andWhere('s.dateId NOT IN (:activeXmlDateIds)')
|
||||
->setParameter('today', $today)
|
||||
->setParameter('activeXmlDateIds', $activeXmlDateIds)
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +272,19 @@ class TravelSnapshotService
|
||||
return $this->snapshotRepository->deleteExpiredSnapshots($beforeDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges future snapshots whose dateId is no longer present in the XML export.
|
||||
*
|
||||
* @param array<int> $activeXmlDateIds dateIds currently present in the XML file map
|
||||
*/
|
||||
public function purgeOrphanedFutureSnapshots(array $activeXmlDateIds): int
|
||||
{
|
||||
return $this->snapshotRepository->deleteOrphanedFutureSnapshots(
|
||||
$activeXmlDateIds,
|
||||
new \DateTimeImmutable('today'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves snapshot by exact date/hotel or by date fallback.
|
||||
*/
|
||||
|
||||
@@ -450,6 +450,41 @@ class TravelSnapshotServiceTest extends TestCase
|
||||
$this->assertSame(['processed' => 0, 'updated' => 0, 'failed' => 0], $result);
|
||||
}
|
||||
|
||||
public function testPurgeOrphanedFutureSnapshotsReturnsZeroForEmptyList(): void
|
||||
{
|
||||
$this->snapshotRepository
|
||||
->expects($this->once())
|
||||
->method('deleteOrphanedFutureSnapshots')
|
||||
->with([], $this->isInstanceOf(\DateTimeImmutable::class))
|
||||
->willReturn(0);
|
||||
|
||||
$result = $this->service->purgeOrphanedFutureSnapshots([]);
|
||||
|
||||
$this->assertSame(0, $result);
|
||||
}
|
||||
|
||||
public function testPurgeOrphanedFutureSnapshotsDelegatesWithCorrectArguments(): void
|
||||
{
|
||||
$activeIds = [100, 200, 300];
|
||||
|
||||
$this->snapshotRepository
|
||||
->expects($this->once())
|
||||
->method('deleteOrphanedFutureSnapshots')
|
||||
->with(
|
||||
$activeIds,
|
||||
$this->callback(function (\DateTimeImmutable $date) {
|
||||
$expected = new \DateTimeImmutable('today');
|
||||
|
||||
return abs($date->getTimestamp() - $expected->getTimestamp()) < 5;
|
||||
})
|
||||
)
|
||||
->willReturn(3);
|
||||
|
||||
$result = $this->service->purgeOrphanedFutureSnapshots($activeIds);
|
||||
|
||||
$this->assertSame(3, $result);
|
||||
}
|
||||
|
||||
public function testGenerateMappingBuildsCorrectStructure(): void
|
||||
{
|
||||
$hotel = new Hotel();
|
||||
|
||||
Reference in New Issue
Block a user