fix: stop truncating imported prices to whole euros

This commit is contained in:
fromme committed 2026-09-30 14:57:20 +02:00
1 parent b510299032
commit 27832d6548
16 files changed
+135 -60

No files matched your search

@@ -56,7 +56,7 @@ class FacebookCatalogCsvFormatter
$product['images']['image'][0],
$product['link'],
$product['name'],
$minPrice . ' EUR',
sprintf('%.2f EUR', $minPrice),
'brand-epreisen'
];
}
@@ -48,7 +48,7 @@ class GoogleBusinessFeedCsvFormatter
$product['name'],
$product['link'],
$product['images']['image'][0],
$minPrice . '.00 EUR',
sprintf('%.2f EUR', $minPrice),
];
}
}
@@ -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)
{
@@ -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'];
@@ -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'];
@@ -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 = '
@@ -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,
@@ -0,0 +1,45 @@
<?php
namespace EP\EpTheme\ViewHelpers;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic;
/**
* Formats a euro amount the way the website always has for whole euros ("229",
* "1234"), and with German decimals where there are cents ("99,90"). Prices are
* stored with cents, and the database hands them over as strings like "229.00".
*
* Keep in sync with formatPrice() in pricetable_controller.js.
*
* Usage: {row.minPrice -> 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, ',', '');
}
}
@@ -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' ||
@@ -39,7 +39,7 @@
{hotel.teaser -> f:format.crop(maxCharacters: settings.teaserTextMaxChars) -> f:format.html()}
<f:if condition="{hotel.available} != -1">
<span class="teaserbox__price">
<strong>{f:if(condition: '{hotel.available} > 0', then: 'ab {hotel.minPrice} €', else: 'aktuell nicht buchbar')}</strong>
<strong><f:if condition="{hotel.available} > 0"><f:then>ab {hotel.minPrice -> ep:price()} €</f:then><f:else>aktuell nicht buchbar</f:else></f:if></strong>
</span>
</f:if>
</div>
@@ -53,10 +53,10 @@
<td class="px-2 py-1 border border-zinc-200">
<f:if condition="{row.available}">
<f:then>
{row.minPrice} €
{row.minPrice -> ep:price()} €
</f:then>
<f:else>
<s>{row.minPrice} €</s>
<s>{row.minPrice -> ep:price()} €</s>
</f:else>
</f:if>
</td>
@@ -104,7 +104,7 @@
</ul>
<div class="bg-ep-primary-dark text-white font-bold px-4 py-1 peer-hover:bg-badge-gradient peer-hover:text-white transition-colors">
<div class="lg:inline-block">
ab <span class="product-teaser__price mx-1">{minPrice} €</span>
ab <span class="product-teaser__price mx-1">{minPrice -> ep:price()} €</span>
</div>
<div class="product-teaser__label-price">
p. P.{f:if(condition: '{season} == "w" && {skiPassIncluded}', then: ' / inkl. Skipass')}
@@ -129,9 +129,9 @@
</td>
</f:if>
<td class="border-r border-zinc-200 px-2 py-1 md:p-2 text-center">
<f:if condition="{row.pseudoPrice}">
<f:if condition="{row.pseudoPrice} > 0">
<span class="block text-sm line-through">
{row.pseudoPrice} €
{row.pseudoPrice -> ep:price()} €
</span>
</f:if>
<span class="font-bold normal-case text-sm md:text-base whitespace-nowrap"
@@ -139,7 +139,7 @@
data-room="{row.minPrice}"
data-bus="{row.busPrice}"
data-discount="{row.discount}">
{row.minPrice}
{row.minPrice -> ep:price()}
</span>
</td>
<td class="px-2 py-1 md:p-2 text-center">
@@ -61,14 +61,14 @@
<f:link.typolink
parameter="{row.hotelDetailPageUid}"
class="whitespace-nowrap {ep:themeClasses(classes: '{sbw: \'text-sbw-primary\', snz: \'text-snz-primary\', uch: \'text-uch-primary\'}', key: settings.themekey)}">
{row.rooms.{type}.price}
{row.rooms.{type}.price -> ep:price()}
{f:if(condition: '{row.rooms.{type}.undersubscription}', then: '*')}
</f:link.typolink>
</f:then>
<f:else>
<f:if condition="{row.rooms.{type}.available}">
<f:else>
<span class="line-through text-zinc-500">{row.rooms.{type}.price}</span>
<span class="line-through text-zinc-500">{row.rooms.{type}.price -> ep:price()}</span>
</f:else>
</f:if>
</f:else>
@@ -266,7 +266,7 @@
data-room="{row.minPrice}"
data-bus="{row.busPrice}"
data-discount="{row.discount}">
{row.minPrice}
{row.minPrice -> ep:price()}
</span>
</a>
</f:then>
@@ -279,7 +279,7 @@
data-room="{row.minPrice}"
data-bus="{row.busPrice}"
data-discount="{row.discount}">
{row.minPrice} €
{row.minPrice -> ep:price()} €
</span>
</f:else>
</f:if>
@@ -127,7 +127,7 @@
</tr>
<tr>
<th>Abpreis</th>
<td>{date.minprice} €</td>
<td>{date.minprice -> ep:price()} €</td>
</tr>
<tr>
<th>Bus inkl.</th>
@@ -154,7 +154,7 @@
<tr>
<td>{room.name}</td>
<td>{v:variable.get(name: 'room.@pax')}</td>
<td>{room.price} €</td>
<td>{room.price -> ep:price()} €</td>
<td>{room.available}</td>
</tr>
</f:for>