From 27832d6548449a3aff3fdaff0599672fc11519d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Wed, 30 Sep 2026 12:44:39 +0200 Subject: [PATCH] fix: stop truncating imported prices to whole euros --- .../FacebookCatalogCsvFormatter.php | 2 +- .../GoogleBusinessFeedCsvFormatter.php | 2 +- .../ep_products/Classes/Domain/Model/Date.php | 32 ++++++------ .../Classes/Domain/Serialization/Date.php | 2 +- .../Classes/Domain/Serialization/Room.php | 4 +- .../Classes/Service/DateImportService.php | 52 +++++++++++++------ .../typo3conf/ext/ep_products/ext_tables.sql | 10 ++-- .../Classes/ViewHelpers/PriceViewHelper.php | 45 ++++++++++++++++ .../js/controllers/pricetable_controller.js | 20 +++++-- .../Private/Partials/Hotel/Teaser.html | 2 +- .../Private/Partials/Pricetable.html | 4 +- .../Private/Partials/Product/Teaser.html | 2 +- .../Templates/AjaxTable/Datestable.html | 6 +-- .../Templates/AjaxTable/EventPricetable.html | 4 +- .../Templates/AjaxTable/Pricetable.html | 4 +- .../Private/Templates/Reseller/Copypaste.html | 4 +- 16 files changed, 135 insertions(+), 60 deletions(-) create mode 100644 public/typo3conf/ext/ep_theme/Classes/ViewHelpers/PriceViewHelper.php diff --git a/public/typo3conf/ext/ep_products/Classes/CsvFormatter/FacebookCatalogCsvFormatter.php b/public/typo3conf/ext/ep_products/Classes/CsvFormatter/FacebookCatalogCsvFormatter.php index d6924b9c..2b86448f 100644 --- a/public/typo3conf/ext/ep_products/Classes/CsvFormatter/FacebookCatalogCsvFormatter.php +++ b/public/typo3conf/ext/ep_products/Classes/CsvFormatter/FacebookCatalogCsvFormatter.php @@ -56,7 +56,7 @@ class FacebookCatalogCsvFormatter $product['images']['image'][0], $product['link'], $product['name'], - $minPrice . ' EUR', + sprintf('%.2f EUR', $minPrice), 'brand-epreisen' ]; } diff --git a/public/typo3conf/ext/ep_products/Classes/CsvFormatter/GoogleBusinessFeedCsvFormatter.php b/public/typo3conf/ext/ep_products/Classes/CsvFormatter/GoogleBusinessFeedCsvFormatter.php index e39eb57c..9a8e17ca 100644 --- a/public/typo3conf/ext/ep_products/Classes/CsvFormatter/GoogleBusinessFeedCsvFormatter.php +++ b/public/typo3conf/ext/ep_products/Classes/CsvFormatter/GoogleBusinessFeedCsvFormatter.php @@ -48,7 +48,7 @@ class GoogleBusinessFeedCsvFormatter $product['name'], $product['link'], $product['images']['image'][0], - $minPrice . '.00 EUR', + sprintf('%.2f EUR', $minPrice), ]; } } diff --git a/public/typo3conf/ext/ep_products/Classes/Domain/Model/Date.php b/public/typo3conf/ext/ep_products/Classes/Domain/Model/Date.php index 7df0541f..a6060a2d 100644 --- a/public/typo3conf/ext/ep_products/Classes/Domain/Model/Date.php +++ b/public/typo3conf/ext/ep_products/Classes/Domain/Model/Date.php @@ -120,9 +120,9 @@ class Date extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity protected $busAvailable = false; /** - * @var int + * @var float */ - protected $busPrice = 0; + protected $busPrice = 0.0; /** * @var string @@ -145,19 +145,19 @@ class Date extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity protected $pickups = ''; /** - * @var int + * @var float */ - protected $discount = 0; + protected $discount = 0.0; /** - * @var int + * @var float */ - protected $minPrice = 0; + protected $minPrice = 0.0; /** - * @var int + * @var float */ - protected $pseudoPrice = 0; + protected $pseudoPrice = 0.0; /** * @var int @@ -438,7 +438,7 @@ class Date extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity } /** - * @return int + * @return float */ public function getBusPrice() { @@ -446,7 +446,7 @@ class Date extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity } /** - * @param int $busPrice + * @param float $busPrice */ public function setBusPrice($busPrice) { @@ -533,21 +533,21 @@ class Date extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity } /** - * @return int + * @return float */ public function getDiscount() { return $this->discount; } /** - * @param int $discount + * @param float $discount */ public function setDiscount($discount) { $this->discount = $discount; } /** - * @return int + * @return float */ public function getMinPrice() { @@ -555,7 +555,7 @@ class Date extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity } /** - * @param int $minPrice + * @param float $minPrice */ public function setMinPrice($minPrice) { @@ -563,7 +563,7 @@ class Date extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity } /** - * @return int + * @return float */ public function getPseudoPrice() { @@ -571,7 +571,7 @@ class Date extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity } /** - * @param int $pseudoPrice + * @param float $pseudoPrice */ public function setPseudoPrice($pseudoPrice) { diff --git a/public/typo3conf/ext/ep_products/Classes/Domain/Serialization/Date.php b/public/typo3conf/ext/ep_products/Classes/Domain/Serialization/Date.php index d4f62f37..05270a2f 100644 --- a/public/typo3conf/ext/ep_products/Classes/Domain/Serialization/Date.php +++ b/public/typo3conf/ext/ep_products/Classes/Domain/Serialization/Date.php @@ -44,7 +44,7 @@ class Date $date['@id'] = $array['dateBusProId']; $date['from'] = $array['dateStart']; $date['to'] = $array['dateEnd']; - $date['minprice'] = $array['dateMinPrice']; + $date['minprice'] = (float) $array['dateMinPrice']; $date['nights'] = $array['dateNights']; $date['skipassincluded'] = $array['dateSkiPassIncluded']; $date['busincluded'] = $array['dateBusIncluded']; diff --git a/public/typo3conf/ext/ep_products/Classes/Domain/Serialization/Room.php b/public/typo3conf/ext/ep_products/Classes/Domain/Serialization/Room.php index f9a9c90e..e6d60251 100644 --- a/public/typo3conf/ext/ep_products/Classes/Domain/Serialization/Room.php +++ b/public/typo3conf/ext/ep_products/Classes/Domain/Serialization/Room.php @@ -40,8 +40,8 @@ class Room $room['@pax'] = $array['roomPax']; $room['@code'] = $array['roomCode']; $room['name'] = $array['roomName']; - $room['price'] = $array['roomPrice']; - $room['busprice'] = $array['busPrice']; + $room['price'] = (float) $array['roomPrice']; + $room['busprice'] = (float) $array['busPrice']; $room['nights'] = $array['roomNights']; $room['available'] = $array['roomAvailable']; diff --git a/public/typo3conf/ext/ep_products/Classes/Service/DateImportService.php b/public/typo3conf/ext/ep_products/Classes/Service/DateImportService.php index 12527da7..bd31806d 100644 --- a/public/typo3conf/ext/ep_products/Classes/Service/DateImportService.php +++ b/public/typo3conf/ext/ep_products/Classes/Service/DateImportService.php @@ -193,10 +193,17 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface return $dateCount; } + /** + * Recreated on every run, so the temp tables always follow the schema of the + * live tables (a column type change would otherwise survive in them) and a + * crashed import cannot leave rows behind for the next one. + */ public function createTempTables(): void { - $this->db->executeStatement('CREATE TABLE IF NOT EXISTS tx_epproducts_domain_model_date_temp LIKE tx_epproducts_domain_model_date'); - $this->db->executeStatement('CREATE TABLE IF NOT EXISTS tx_epproducts_domain_model_room_temp LIKE tx_epproducts_domain_model_room'); + $this->db->executeStatement('DROP TABLE IF EXISTS tx_epproducts_domain_model_date_temp'); + $this->db->executeStatement('DROP TABLE IF EXISTS tx_epproducts_domain_model_room_temp'); + $this->db->executeStatement('CREATE TABLE tx_epproducts_domain_model_date_temp LIKE tx_epproducts_domain_model_date'); + $this->db->executeStatement('CREATE TABLE tx_epproducts_domain_model_room_temp LIKE tx_epproducts_domain_model_room'); } public function clearTables(): void @@ -425,7 +432,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface if ($discount) { $servicesTransportationOptional[] = [ 'label' => 'Rabatt bei Eigenanreise', - 'price' => $discount, + 'price' => number_format($discount, 2, ',', '.'), ]; } @@ -549,15 +556,15 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface */ protected function parseRooms(array $row, $hotelUid, array &$date, array $services): void { - $minPriceOverall = 0; - $minPriceAvailable = 0; + $minPriceOverall = 0.0; + $minPriceAvailable = 0.0; $minPax = 0; $availableTotal = 0; $boardCode = ''; $count = 0; if ($row['pseudoPrice'] !== null) { - $date['pseudo_price'] = (int) $row['pseudoPrice']; + $date['pseudo_price'] = (float) $row['pseudoPrice']; } foreach ($row['rooms'] as $roomRow) { @@ -567,7 +574,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface $pax = (int) $roomRow['maxPax']; $available = (int) $roomRow['available']; - $price = (int) $roomRow['price']; + $price = (float) $roomRow['price']; // Skip rooms with zero price _and_ zero availability. if (!$available && !$price) { @@ -594,12 +601,12 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface $availableTotal += $available * $pax; // Determine lowest price of available rooms - if ($code !== static::CODE_BABYROOM && $available > 0 && ($minPriceAvailable === 0 || $price < $minPriceAvailable)) { + if ($code !== static::CODE_BABYROOM && $available > 0 && ($minPriceAvailable === 0.0 || $price < $minPriceAvailable)) { $minPriceAvailable = $price; } // Determine lowest overall price - if ($code !== static::CODE_BABYROOM && ($minPriceOverall === 0 || $price < $minPriceOverall)) { + if ($code !== static::CODE_BABYROOM && ($minPriceOverall === 0.0 || $price < $minPriceOverall)) { $minPriceOverall = $price; } @@ -649,19 +656,20 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface /** * @param array $services - * @return int + * @return float */ - protected function determineDiscount(array $services): int + protected function determineDiscount(array $services): float { foreach ($services as $key => $section) { foreach ($section as $service) { - if ($service['price'] < 0) { - return (int) $service['price']; + $price = $this->parseFormattedPrice($service['price']); + if ($price < 0) { + return $price; } } } - return 0; + return 0.0; } /** @@ -788,12 +796,24 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface } foreach ($services['BUS'] as $service) { + $price = $this->parseFormattedPrice($service['price']); $date['bus_available'] = 1; - $date['bus_price'] = (int) $service['price']; - $date['bus_included'] = (int) ((int) $service['price'] === 0); + $date['bus_price'] = $price; + $date['bus_included'] = (int) ($price === 0.0); } } + /** + * Reads back a price parseService() formatted for display ("1.234,50"). + * + * @param string $price + * @return float + */ + protected function parseFormattedPrice(string $price): float + { + return (float) str_replace(['.', ','], ['', '.'], $price); + } + protected function getHotelMappings(): void { $sql = ' diff --git a/public/typo3conf/ext/ep_products/ext_tables.sql b/public/typo3conf/ext/ep_products/ext_tables.sql index c521370a..675354db 100644 --- a/public/typo3conf/ext/ep_products/ext_tables.sql +++ b/public/typo3conf/ext/ep_products/ext_tables.sql @@ -117,15 +117,15 @@ CREATE TABLE tx_epproducts_domain_model_date bus_included tinyint(1) unsigned DEFAULT '0' NOT NULL, bus_available tinyint(1) unsigned DEFAULT '0' NOT NULL, skipass_included tinyint(1) unsigned DEFAULT '0' NOT NULL, - bus_price int(11) DEFAULT '0' NOT NULL, + bus_price decimal(10,2) DEFAULT '0.00' NOT NULL, services_included text NOT NULL, services_general text NOT NULL, courses_included text NOT NULL, pickups text NOT NULL, guide text NOT NULL, - discount int(11) DEFAULT '0' NOT NULL, - min_price int(11) DEFAULT '0' NOT NULL, - pseudo_price int(11) DEFAULT '0' NOT NULL, + discount decimal(10,2) DEFAULT '0.00' NOT NULL, + min_price decimal(10,2) DEFAULT '0.00' NOT NULL, + pseudo_price decimal(10,2) DEFAULT '0.00' NOT NULL, available int(11) DEFAULT '0' NOT NULL, min_pax int(11) DEFAULT '0' NOT NULL, product int(11) unsigned DEFAULT '0', @@ -456,7 +456,7 @@ CREATE TABLE tx_epproducts_domain_model_room name varchar(255) DEFAULT '' NOT NULL, code varchar(255) DEFAULT '' NOT NULL, type int(11) DEFAULT '0' NOT NULL, - price int(11) DEFAULT '0' NOT NULL, + price decimal(10,2) DEFAULT '0.00' NOT NULL, services text NOT NULL, nights int(11) DEFAULT '0' NOT NULL, pax int(11) DEFAULT '0' NOT NULL, diff --git a/public/typo3conf/ext/ep_theme/Classes/ViewHelpers/PriceViewHelper.php b/public/typo3conf/ext/ep_theme/Classes/ViewHelpers/PriceViewHelper.php new file mode 100644 index 00000000..10b98bc1 --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Classes/ViewHelpers/PriceViewHelper.php @@ -0,0 +1,45 @@ + ep:price()} € + */ +class PriceViewHelper extends AbstractViewHelper +{ + use CompileWithContentArgumentAndRenderStatic; + + public function initializeArguments() + { + parent::initializeArguments(); + + $this->registerArgument('value', 'mixed', 'Amount in euros'); + } + + /** + * @param array $arguments + * @param \Closure $renderChildrenClosure + * @param RenderingContextInterface $renderingContext + * @return string + */ + public static function renderStatic( + array $arguments, + \Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext + ) { + $amount = round((float) $renderChildrenClosure(), 2); + $decimals = floor($amount) === $amount ? 0 : 2; + + return number_format($amount, $decimals, ',', ''); + } +} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/pricetable_controller.js b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/pricetable_controller.js index 2840d710..a05952d7 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/pricetable_controller.js +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/pricetable_controller.js @@ -42,13 +42,13 @@ export default class extends Controller { modeValueChanged() { this.priceTargets.forEach(price => { - let room = parseInt(price.dataset.room) - let bus = parseInt(price.dataset.bus) - let discount = parseInt(price.dataset.discount) - price.innerText = `${this.calculatePrice(room, bus, discount)} €` + let room = parseFloat(price.dataset.room) + let bus = parseFloat(price.dataset.bus) + let discount = parseFloat(price.dataset.discount) + price.innerText = `${this.formatPrice(this.calculatePrice(room, bus, discount))} €` }) this.busIconTargets.forEach(icon => { - let busPrice = parseInt(icon.dataset.busPrice) + let busPrice = parseFloat(icon.dataset.busPrice) let busIncluded = !!icon.dataset.busIncluded icon.classList.toggle('hidden', this.isHideBusIcon(busPrice, busIncluded)) }) @@ -87,6 +87,16 @@ export default class extends Controller { return minPrice } + // Keep in sync with EP\EpTheme\ViewHelpers\PriceViewHelper: whole euros + // as before ("229"), German decimals where there are cents ("99,90"). + formatPrice(amount) { + let cents = Math.round(amount * 100) + if (cents % 100 === 0) { + return String(cents / 100) + } + return (cents / 100).toFixed(2).replace('.', ',') + } + isHideBusIcon(busPrice, busIncluded) { return ( busPrice && this.modeValue !== 'surcharge' || diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Hotel/Teaser.html b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Hotel/Teaser.html index 77cb703c..c40a3ed1 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Hotel/Teaser.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Hotel/Teaser.html @@ -39,7 +39,7 @@ {hotel.teaser -> f:format.crop(maxCharacters: settings.teaserTextMaxChars) -> f:format.html()} - {f:if(condition: '{hotel.available} > 0', then: 'ab {hotel.minPrice} €', else: 'aktuell nicht buchbar')} + ab {hotel.minPrice -> ep:price()} €aktuell nicht buchbar diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Pricetable.html b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Pricetable.html index 31e5d278..15b689a9 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Pricetable.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Pricetable.html @@ -53,10 +53,10 @@ - {row.minPrice} € + {row.minPrice -> ep:price()} € - {row.minPrice} € + {row.minPrice -> ep:price()} € diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/Teaser.html b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/Teaser.html index ebc557cf..e54e2dd8 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/Teaser.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/Teaser.html @@ -104,7 +104,7 @@
- ab {minPrice} € + ab {minPrice -> ep:price()} €
p. P.{f:if(condition: '{season} == "w" && {skiPassIncluded}', then: ' / inkl. Skipass')} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Datestable.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Datestable.html index 4151e988..747389a5 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Datestable.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Datestable.html @@ -129,9 +129,9 @@ - + - {row.pseudoPrice} € + {row.pseudoPrice -> ep:price()} € - {row.minPrice} + {row.minPrice -> ep:price()} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/EventPricetable.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/EventPricetable.html index 4ad8df8a..e4c112ce 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/EventPricetable.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/EventPricetable.html @@ -61,14 +61,14 @@ - {row.rooms.{type}.price} + {row.rooms.{type}.price -> ep:price()} {f:if(condition: '{row.rooms.{type}.undersubscription}', then: '*')} - {row.rooms.{type}.price} + {row.rooms.{type}.price -> ep:price()} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Pricetable.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Pricetable.html index d52d3e80..8061e64e 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Pricetable.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Pricetable.html @@ -266,7 +266,7 @@ data-room="{row.minPrice}" data-bus="{row.busPrice}" data-discount="{row.discount}"> - {row.minPrice} + {row.minPrice -> ep:price()} @@ -279,7 +279,7 @@ data-room="{row.minPrice}" data-bus="{row.busPrice}" data-discount="{row.discount}"> - {row.minPrice} € + {row.minPrice -> ep:price()} € diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Reseller/Copypaste.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Reseller/Copypaste.html index fba528ee..31eb0703 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Reseller/Copypaste.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Reseller/Copypaste.html @@ -127,7 +127,7 @@ Abpreis - {date.minprice} € + {date.minprice -> ep:price()} € Bus inkl. @@ -154,7 +154,7 @@ {room.name} {v:variable.get(name: 'room.@pax')} - {room.price} € + {room.price -> ep:price()} € {room.available}