feat: groups price calculator admin crud, booking/offer flow and api
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use App\Entity\Groups\AccommodationPrice;
|
||||
use App\Entity\Groups\AdditionalService;
|
||||
use App\Entity\Groups\BoardService;
|
||||
use App\Enum\Groups\AdditionalServiceType;
|
||||
use App\Enum\Groups\Season;
|
||||
use App\Repository\Groups\AccommodationRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:groups:import-legacy-data',
|
||||
description: 'Imports Accommodation/AccommodationPrice/BoardService/AdditionalService data from the legacy TYPO3 CSV exports (hotels.csv, groupspriceconfig.csv, groupspriceboard.csv, groupspriceoption.csv, seasons.csv)',
|
||||
)]
|
||||
class ImportGroupsLegacyDataCommand extends Command
|
||||
{
|
||||
private const int DEFAULT_MIN_NIGHTS = 1;
|
||||
private const Season DEFAULT_SEASON = Season::SECONDARY;
|
||||
|
||||
/** @var array<int, AdditionalServiceType> maps groupspriceoption "type" column to AdditionalServiceType */
|
||||
private const array OPTION_TYPE_MAP = [
|
||||
1 => AdditionalServiceType::Flat,
|
||||
2 => AdditionalServiceType::PerPerson,
|
||||
3 => AdditionalServiceType::PerNight,
|
||||
4 => AdditionalServiceType::PerPersonPerNight,
|
||||
];
|
||||
|
||||
/** @var array<string, Season> maps seasons.csv "token" column to Season, per translations/messages.de.yaml */
|
||||
private const array SEASON_TOKEN_MAP = [
|
||||
'HS' => Season::PEAK,
|
||||
'NS' => Season::SECONDARY,
|
||||
'VNS' => Season::ADV_SECONDARY,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly AccommodationRepository $accommodationRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('path', InputArgument::OPTIONAL, 'Directory containing hotels.csv, groupspriceconfig.csv, groupspriceboard.csv, groupspriceoption.csv, seasons.csv', 'temp')
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Parse and report without persisting anything')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$dryRun = (bool) $input->getOption('dry-run');
|
||||
$path = rtrim((string) $input->getArgument('path'), '/');
|
||||
|
||||
foreach (['hotels.csv', 'groupspriceconfig.csv', 'groupspriceboard.csv', 'groupspriceoption.csv', 'seasons.csv'] as $file) {
|
||||
if (!is_readable($path.'/'.$file)) {
|
||||
$io->error(sprintf('Cannot read %s/%s', $path, $file));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
$hotels = $this->readCsv($path.'/hotels.csv');
|
||||
$configRows = $this->readCsv($path.'/groupspriceconfig.csv');
|
||||
$boardRows = $this->readCsv($path.'/groupspriceboard.csv');
|
||||
$optionRows = $this->readCsv($path.'/groupspriceoption.csv');
|
||||
$seasonRows = $this->readCsv($path.'/seasons.csv', ',');
|
||||
|
||||
$currencyByHotelUid = $this->deriveCurrencies($configRows);
|
||||
$seasonRanges = $this->parseSeasonRanges($seasonRows);
|
||||
|
||||
$created = [];
|
||||
$updated = [];
|
||||
/** @var array<int, Accommodation> $accommodationByUid */
|
||||
$accommodationByUid = [];
|
||||
|
||||
foreach ($hotels as $row) {
|
||||
$uid = (int) $row['uid'];
|
||||
$code = trim($row['code']);
|
||||
$name = trim($row['name']);
|
||||
$currency = $currencyByHotelUid[$uid] ?? 'EUR';
|
||||
|
||||
$existing = $this->accommodationRepository->findOneByCmsCode($code)
|
||||
?? $this->accommodationRepository->findOneByCalendarCode($code);
|
||||
|
||||
if (null !== $existing) {
|
||||
$existing->setName($name);
|
||||
$existing->setCurrency($currency);
|
||||
|
||||
foreach ($existing->getAccommodationPrices()->toArray() as $price) {
|
||||
$existing->removeAccommodationPrice($price);
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->remove($price);
|
||||
}
|
||||
}
|
||||
foreach ($existing->getBoardServices()->toArray() as $board) {
|
||||
$existing->removeBoardService($board);
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->remove($board);
|
||||
}
|
||||
}
|
||||
foreach ($existing->getAdditionalServices()->toArray() as $service) {
|
||||
$existing->removeAdditionalService($service);
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->remove($service);
|
||||
}
|
||||
}
|
||||
|
||||
$accommodationByUid[$uid] = $existing;
|
||||
$updated[] = sprintf('%s (%s) — replacing existing prices/board/services', $name, $code);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingByCalendarCode = null;
|
||||
foreach ($accommodationByUid as $a) {
|
||||
// guard against double-import: a prior run may already have created this one
|
||||
if ($a->getCalendarCode() === $code) {
|
||||
$existingByCalendarCode = $a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $existingByCalendarCode) {
|
||||
$accommodationByUid[$uid] = $existingByCalendarCode;
|
||||
continue;
|
||||
}
|
||||
|
||||
$accommodation = new Accommodation();
|
||||
$accommodation->setCalendarCode($code);
|
||||
$accommodation->setName($name);
|
||||
$accommodation->setCurrency($currency);
|
||||
$accommodation->setMaxAdolescentAge(0);
|
||||
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->persist($accommodation);
|
||||
}
|
||||
|
||||
$accommodationByUid[$uid] = $accommodation;
|
||||
$created[] = sprintf('%s (%s, %s)', $name, $code, $currency);
|
||||
}
|
||||
|
||||
$knownUids = array_keys($accommodationByUid);
|
||||
|
||||
[$priceCount, $skippedPriceUids, $unmatchedSeasonCount] = $this->importPrices($configRows, $accommodationByUid, $knownUids, $seasonRanges, $dryRun);
|
||||
[$boardCount, $skippedBoardUids] = $this->importBoardServices($boardRows, $accommodationByUid, $knownUids, $dryRun);
|
||||
[$optionCount, $skippedOptionUids, $optionsNeedingDefaultRange] = $this->importAdditionalServices($optionRows, $accommodationByUid, $knownUids, $dryRun);
|
||||
|
||||
// BoardService/AdditionalService need a date range; derive it from each
|
||||
// accommodation's freshly-imported AccommodationPrice rows now that they exist.
|
||||
// AdditionalServices whose title carried its own year keep the scoped range set above.
|
||||
$this->applyDateRangeToServices($accommodationByUid, $optionsNeedingDefaultRange);
|
||||
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$io->section($dryRun ? 'Dry run summary' : 'Import summary');
|
||||
$io->writeln(sprintf('Accommodations created: %d', \count($created)));
|
||||
foreach ($created as $line) {
|
||||
$io->writeln(' + '.$line);
|
||||
}
|
||||
$io->writeln(sprintf('Accommodations updated: %d', \count($updated)));
|
||||
foreach ($updated as $line) {
|
||||
$io->writeln(' ~ '.$line);
|
||||
}
|
||||
$io->writeln(sprintf('AccommodationPrice rows imported: %d', $priceCount));
|
||||
if ($unmatchedSeasonCount > 0) {
|
||||
$io->writeln(sprintf(' of which %d fell outside all seasons.csv ranges and defaulted to %s', $unmatchedSeasonCount, self::DEFAULT_SEASON->value));
|
||||
}
|
||||
$io->writeln(sprintf('BoardService rows imported: %d', $boardCount));
|
||||
$io->writeln(sprintf('AdditionalService rows imported: %d', $optionCount));
|
||||
|
||||
$skippedUids = array_unique(array_merge($skippedPriceUids, $skippedBoardUids, $skippedOptionUids));
|
||||
sort($skippedUids);
|
||||
if ([] !== $skippedUids) {
|
||||
$io->warning(sprintf(
|
||||
'Skipped rows referencing hotel uid(s) not present in hotels.csv: %s',
|
||||
implode(', ', $skippedUids),
|
||||
));
|
||||
}
|
||||
|
||||
$io->note('Dropped legacy columns with no entity equivalent (verified all-zero or unused): bonus_card_included, price_additional_person_group_1/2/3[_chf], ignore_with_board.');
|
||||
|
||||
if ($dryRun) {
|
||||
$io->note('Dry run — nothing was persisted.');
|
||||
}
|
||||
|
||||
$io->success('Done.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, string>>
|
||||
*/
|
||||
private function readCsv(string $file, string $separator = ';'): array
|
||||
{
|
||||
$handle = fopen($file, 'r');
|
||||
if (false === $handle) {
|
||||
throw new \RuntimeException(sprintf('Unable to open %s', $file));
|
||||
}
|
||||
|
||||
$header = fgetcsv($handle, escape: '\\', separator: $separator, enclosure: '"');
|
||||
if (false === $header) {
|
||||
fclose($handle);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
while (false !== ($row = fgetcsv($handle, escape: '\\', separator: $separator, enclosure: '"'))) {
|
||||
$rows[] = array_combine($header, $row);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives EUR/CHF per hotel uid from groupspriceconfig.csv: if any row for that hotel
|
||||
* has a non-zero price_chf, the hotel is priced in CHF, otherwise EUR.
|
||||
*
|
||||
* @param list<array<string, string>> $configRows
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function deriveCurrencies(array $configRows): array
|
||||
{
|
||||
$currencies = [];
|
||||
foreach ($configRows as $row) {
|
||||
$uid = (int) $row['hotel'];
|
||||
if ($this->parseDecimal($row['price_chf']) > 0.0) {
|
||||
$currencies[$uid] = 'CHF';
|
||||
} elseif (!isset($currencies[$uid])) {
|
||||
$currencies[$uid] = 'EUR';
|
||||
}
|
||||
}
|
||||
|
||||
return $currencies;
|
||||
}
|
||||
|
||||
private function parseDecimal(string $value): float
|
||||
{
|
||||
$value = trim($value);
|
||||
if ('' === $value) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (float) str_replace(',', '.', $value);
|
||||
}
|
||||
|
||||
private function toCents(string $value): int
|
||||
{
|
||||
return (int) round($this->parseDecimal($value) * 100);
|
||||
}
|
||||
|
||||
private function priceColumn(string $currency): string
|
||||
{
|
||||
return 'CHF' === $currency ? 'price_chf' : 'price';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses seasons.csv (token,date_from,date_to; date_to exclusive, same convention as
|
||||
* groupspriceconfig.csv) into Season-tagged ranges, skipping unrecognized tokens.
|
||||
*
|
||||
* @param list<array<string, string>> $rows
|
||||
*
|
||||
* @return list<array{season: Season, dateFrom: \DateTimeImmutable, dateTo: \DateTimeImmutable}>
|
||||
*/
|
||||
private function parseSeasonRanges(array $rows): array
|
||||
{
|
||||
$ranges = [];
|
||||
foreach ($rows as $row) {
|
||||
$season = self::SEASON_TOKEN_MAP[trim($row['token'])] ?? null;
|
||||
if (null === $season) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges[] = [
|
||||
'season' => $season,
|
||||
'dateFrom' => new \DateTimeImmutable(trim($row['date_from'])),
|
||||
'dateTo' => new \DateTimeImmutable(trim($row['date_to'])),
|
||||
];
|
||||
}
|
||||
|
||||
return $ranges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the season with the largest night-count overlap with [$dateFrom, $dateToExclusive)
|
||||
* among $seasonRanges. Falls back to DEFAULT_SEASON (with matched=false) when the price
|
||||
* period doesn't overlap any season range at all (e.g. a gap in seasons.csv).
|
||||
*
|
||||
* @param list<array{season: Season, dateFrom: \DateTimeImmutable, dateTo: \DateTimeImmutable}> $seasonRanges
|
||||
*
|
||||
* @return array{0: Season, 1: bool}
|
||||
*/
|
||||
private function resolveSeason(\DateTimeImmutable $dateFrom, \DateTimeImmutable $dateToExclusive, array $seasonRanges): array
|
||||
{
|
||||
$bestSeason = null;
|
||||
$bestOverlapDays = 0;
|
||||
|
||||
foreach ($seasonRanges as $range) {
|
||||
$overlapStart = max($dateFrom, $range['dateFrom']);
|
||||
$overlapEnd = min($dateToExclusive, $range['dateTo']);
|
||||
|
||||
if ($overlapStart >= $overlapEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$overlapDays = $overlapStart->diff($overlapEnd)->days;
|
||||
if ($overlapDays > $bestOverlapDays) {
|
||||
$bestOverlapDays = $overlapDays;
|
||||
$bestSeason = $range['season'];
|
||||
}
|
||||
}
|
||||
|
||||
return [$bestSeason ?? self::DEFAULT_SEASON, null !== $bestSeason];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, string>> $rows
|
||||
* @param array<int, Accommodation> $accommodationByUid
|
||||
* @param list<int> $knownUids
|
||||
* @param list<array{season: Season, dateFrom: \DateTimeImmutable, dateTo: \DateTimeImmutable}> $seasonRanges
|
||||
*
|
||||
* @return array{0: int, 1: list<int>, 2: int}
|
||||
*/
|
||||
private function importPrices(array $rows, array $accommodationByUid, array $knownUids, array $seasonRanges, bool $dryRun): array
|
||||
{
|
||||
$count = 0;
|
||||
$skipped = [];
|
||||
$unmatchedSeasonCount = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$uid = (int) $row['hotel'];
|
||||
if (!\in_array($uid, $knownUids, true)) {
|
||||
$skipped[] = $uid;
|
||||
continue;
|
||||
}
|
||||
|
||||
$accommodation = $accommodationByUid[$uid];
|
||||
$column = $this->priceColumn($accommodation->getCurrency());
|
||||
$additionalColumn = 'CHF' === $accommodation->getCurrency()
|
||||
? 'price_additional_person_chf'
|
||||
: 'price_additional_person';
|
||||
|
||||
$rawDateFrom = (new \DateTimeImmutable('@'.$row['date_from']))->setTime(0, 0);
|
||||
// Source date_to is exclusive; AccommodationPrice::dateTo is the last included night.
|
||||
$rawDateToExclusive = (new \DateTimeImmutable('@'.$row['date_to']))->setTime(0, 0);
|
||||
|
||||
[$season, $matched] = $this->resolveSeason($rawDateFrom, $rawDateToExclusive, $seasonRanges);
|
||||
if (!$matched) {
|
||||
++$unmatchedSeasonCount;
|
||||
}
|
||||
|
||||
$price = new AccommodationPrice();
|
||||
$price->setDateFrom($rawDateFrom);
|
||||
$price->setDateTo($rawDateToExclusive->modify('-1 day'));
|
||||
$price->setSeason($season);
|
||||
$price->setIncludedPax((int) $row['persons_included']);
|
||||
$price->setPricePerNight($this->toCents($row[$column]));
|
||||
$price->setPriceAdditionalPerson($this->toCents($row[$additionalColumn]));
|
||||
$price->setMinNights(self::DEFAULT_MIN_NIGHTS);
|
||||
$price->setType(null);
|
||||
$price->setAcceptUndersubscription(false);
|
||||
$price->setAcceptShortTerm(false);
|
||||
|
||||
$accommodation->addAccommodationPrice($price);
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->persist($price);
|
||||
}
|
||||
++$count;
|
||||
}
|
||||
|
||||
return [$count, $skipped, $unmatchedSeasonCount];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, string>> $rows
|
||||
* @param array<int, Accommodation> $accommodationByUid
|
||||
* @param list<int> $knownUids
|
||||
*
|
||||
* @return array{0: int, 1: list<int>}
|
||||
*/
|
||||
private function importBoardServices(array $rows, array $accommodationByUid, array $knownUids, bool $dryRun): array
|
||||
{
|
||||
$count = 0;
|
||||
$skipped = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$uid = (int) $row['hotel'];
|
||||
if (!\in_array($uid, $knownUids, true)) {
|
||||
$skipped[] = $uid;
|
||||
continue;
|
||||
}
|
||||
|
||||
$accommodation = $accommodationByUid[$uid];
|
||||
$column = $this->priceColumn($accommodation->getCurrency());
|
||||
|
||||
$board = new BoardService();
|
||||
$board->setLabel(trim($row['title']));
|
||||
$board->setPrice($this->toCents($row[$column]));
|
||||
// dateFrom/dateTo filled in applyDateRangeToServices() once prices are known.
|
||||
$board->setDateFrom(new \DateTimeImmutable('today'));
|
||||
$board->setDateTo(new \DateTimeImmutable('today'));
|
||||
|
||||
$accommodation->addBoardService($board);
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->persist($board);
|
||||
}
|
||||
++$count;
|
||||
}
|
||||
|
||||
return [$count, $skipped];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, string>> $rows
|
||||
* @param array<int, Accommodation> $accommodationByUid
|
||||
* @param list<int> $knownUids
|
||||
*
|
||||
* @return array{0: int, 1: list<int>, 2: list<AdditionalService>}
|
||||
*/
|
||||
private function importAdditionalServices(array $rows, array $accommodationByUid, array $knownUids, bool $dryRun): array
|
||||
{
|
||||
$count = 0;
|
||||
$skipped = [];
|
||||
$needsDefaultRange = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$uid = (int) $row['hotel'];
|
||||
if (!\in_array($uid, $knownUids, true)) {
|
||||
$skipped[] = $uid;
|
||||
continue;
|
||||
}
|
||||
|
||||
$accommodation = $accommodationByUid[$uid];
|
||||
$column = $this->priceColumn($accommodation->getCurrency());
|
||||
$type = self::OPTION_TYPE_MAP[(int) $row['type']] ?? AdditionalServiceType::Flat;
|
||||
[$year, $label] = $this->stripYearPrefix(trim($row['title']));
|
||||
|
||||
$service = new AdditionalService();
|
||||
$service->setLabel($label);
|
||||
$service->setPrice($this->toCents($row[$column]));
|
||||
$service->setType($type);
|
||||
|
||||
if (null !== $year) {
|
||||
// Title carried its own year (e.g. "2026 Endreinigung ..."); scope validity to
|
||||
// that accommodation's own price rows within that year, not the calendar year.
|
||||
[$yearDateFrom, $yearDateTo] = $this->findYearPriceRange($accommodation, $year);
|
||||
$service->setDateFrom($yearDateFrom ?? new \DateTimeImmutable($year.'-01-01'));
|
||||
$service->setDateTo($yearDateTo ?? new \DateTimeImmutable($year.'-12-31'));
|
||||
} else {
|
||||
// dateFrom/dateTo filled in applyDateRangeToServices() once prices are known.
|
||||
$service->setDateFrom(new \DateTimeImmutable('today'));
|
||||
$service->setDateTo(new \DateTimeImmutable('today'));
|
||||
$needsDefaultRange[] = $service;
|
||||
}
|
||||
|
||||
$accommodation->addAdditionalService($service);
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->persist($service);
|
||||
}
|
||||
++$count;
|
||||
}
|
||||
|
||||
return [$count, $skipped, $needsDefaultRange];
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a leading "YYYY " prefix off a title, e.g. "2026 Endreinigung Küche" →
|
||||
* [2026, "Endreinigung Küche"]. Returns [null, $label unchanged] when no such prefix
|
||||
* is present.
|
||||
*
|
||||
* @return array{0: ?int, 1: string}
|
||||
*/
|
||||
private function stripYearPrefix(string $label): array
|
||||
{
|
||||
if (preg_match('/^(\d{4})\s+(.+)$/', $label, $matches)) {
|
||||
return [(int) $matches[1], $matches[2]];
|
||||
}
|
||||
|
||||
return [null, $label];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the earliest dateFrom / latest dateTo among an accommodation's already-imported
|
||||
* AccommodationPrice rows that fall within the given calendar year.
|
||||
*
|
||||
* @return array{0: ?\DateTimeImmutable, 1: ?\DateTimeImmutable}
|
||||
*/
|
||||
private function findYearPriceRange(Accommodation $accommodation, int $year): array
|
||||
{
|
||||
$dateFrom = null;
|
||||
$dateTo = null;
|
||||
|
||||
foreach ($accommodation->getAccommodationPrices() as $price) {
|
||||
if ((int) $price->getDateFrom()->format('Y') !== $year) {
|
||||
continue;
|
||||
}
|
||||
if (null === $dateFrom || $price->getDateFrom() < $dateFrom) {
|
||||
$dateFrom = $price->getDateFrom();
|
||||
}
|
||||
if (null === $dateTo || $price->getDateTo() > $dateTo) {
|
||||
$dateTo = $price->getDateTo();
|
||||
}
|
||||
}
|
||||
|
||||
return [$dateFrom, $dateTo];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Accommodation> $accommodationByUid
|
||||
* @param list<AdditionalService> $additionalServicesNeedingDefaultRange only these
|
||||
* (titles without a leading year) get the accommodation-
|
||||
* wide fallback range; year-prefixed ones already have
|
||||
* their scoped range set in importAdditionalServices().
|
||||
*/
|
||||
private function applyDateRangeToServices(array $accommodationByUid, array $additionalServicesNeedingDefaultRange): void
|
||||
{
|
||||
$needsDefaultRange = new \SplObjectStorage();
|
||||
foreach ($additionalServicesNeedingDefaultRange as $service) {
|
||||
$needsDefaultRange->attach($service);
|
||||
}
|
||||
|
||||
$seen = [];
|
||||
foreach ($accommodationByUid as $accommodation) {
|
||||
$id = spl_object_id($accommodation);
|
||||
if (isset($seen[$id])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$id] = true;
|
||||
|
||||
$prices = $accommodation->getAccommodationPrices();
|
||||
if ($prices->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dateFrom = null;
|
||||
$dateTo = null;
|
||||
foreach ($prices as $price) {
|
||||
if (null === $dateFrom || $price->getDateFrom() < $dateFrom) {
|
||||
$dateFrom = $price->getDateFrom();
|
||||
}
|
||||
if (null === $dateTo || $price->getDateTo() > $dateTo) {
|
||||
$dateTo = $price->getDateTo();
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $dateFrom || null === $dateTo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($accommodation->getBoardServices() as $board) {
|
||||
$board->setDateFrom($dateFrom);
|
||||
$board->setDateTo($dateTo);
|
||||
}
|
||||
foreach ($accommodation->getAdditionalServices() as $service) {
|
||||
if ($needsDefaultRange->contains($service)) {
|
||||
$service->setDateFrom($dateFrom);
|
||||
$service->setDateTo($dateTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user