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\Model\XmlExportInfo;
|
||||||
use App\BusProNet\XmlLoader\TravelLoader;
|
use App\BusProNet\XmlLoader\TravelLoader;
|
||||||
use App\Service\TravelDataService;
|
use App\Service\TravelDataService;
|
||||||
|
use App\Service\TravelSnapshotService;
|
||||||
use League\Flysystem\FilesystemException;
|
use League\Flysystem\FilesystemException;
|
||||||
use League\Flysystem\FilesystemOperator;
|
use League\Flysystem\FilesystemOperator;
|
||||||
use League\Flysystem\StorageAttributes;
|
use League\Flysystem\StorageAttributes;
|
||||||
@@ -41,6 +42,7 @@ class BpnXmlSyncCommand extends Command
|
|||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
private readonly TravelLoader $travelLoader,
|
private readonly TravelLoader $travelLoader,
|
||||||
private readonly TravelDataService $travelDataService,
|
private readonly TravelDataService $travelDataService,
|
||||||
|
private readonly TravelSnapshotService $snapshotService,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -124,6 +126,7 @@ class BpnXmlSyncCommand extends Command
|
|||||||
|
|
||||||
$xmlFileMap = null;
|
$xmlFileMap = null;
|
||||||
$snapshotResult = null;
|
$snapshotResult = null;
|
||||||
|
$orphanedDeleted = null;
|
||||||
try {
|
try {
|
||||||
$xmlFileMap = $this->travelLoader->generateFilesMap();
|
$xmlFileMap = $this->travelLoader->generateFilesMap();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -142,18 +145,24 @@ class BpnXmlSyncCommand extends Command
|
|||||||
);
|
);
|
||||||
|
|
||||||
$io->progressFinish();
|
$io->progressFinish();
|
||||||
|
|
||||||
|
$orphanedDeleted = $this->snapshotService->purgeOrphanedFutureSnapshots(array_keys($xmlFileMap));
|
||||||
}
|
}
|
||||||
|
|
||||||
$successMessage = sprintf('Sync complete: %d files updated, %d files deleted', $syncResult['updated'], $syncResult['deleted']);
|
$successMessage = sprintf('Sync complete: %d files updated, %d files deleted', $syncResult['updated'], $syncResult['deleted']);
|
||||||
if (null !== $snapshotResult) {
|
if (null !== $snapshotResult) {
|
||||||
$successMessage .= sprintf(', snapshots: %d processed, %d failed', $snapshotResult['processed'], $snapshotResult['failed']);
|
$successMessage .= sprintf(', snapshots: %d processed, %d failed', $snapshotResult['processed'], $snapshotResult['failed']);
|
||||||
}
|
}
|
||||||
|
if (null !== $orphanedDeleted) {
|
||||||
|
$successMessage .= sprintf(', orphaned deleted: %d', $orphanedDeleted);
|
||||||
|
}
|
||||||
$io->success($successMessage);
|
$io->success($successMessage);
|
||||||
$this->logger->info('XML sync completed', [
|
$this->logger->info('XML sync completed', [
|
||||||
'files_updated' => $syncResult['updated'],
|
'files_updated' => $syncResult['updated'],
|
||||||
'files_deleted' => $syncResult['deleted'],
|
'files_deleted' => $syncResult['deleted'],
|
||||||
'snapshots_processed' => $snapshotResult['processed'] ?? null,
|
'snapshots_processed' => $snapshotResult['processed'] ?? null,
|
||||||
'snapshots_failed' => $snapshotResult['failed'] ?? null,
|
'snapshots_failed' => $snapshotResult['failed'] ?? null,
|
||||||
|
'orphaned_deleted' => $orphanedDeleted,
|
||||||
'remote_timestamp' => $remoteInfo->lastTransfer->format('c'),
|
'remote_timestamp' => $remoteInfo->lastTransfer->format('c'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -126,4 +126,31 @@ class TravelSnapshotRepository extends ServiceEntityRepository
|
|||||||
->getQuery()
|
->getQuery()
|
||||||
->execute();
|
->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);
|
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.
|
* 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);
|
$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
|
public function testGenerateMappingBuildsCorrectStructure(): void
|
||||||
{
|
{
|
||||||
$hotel = new Hotel();
|
$hotel = new Hotel();
|
||||||
|
|||||||
Reference in New Issue
Block a user