wip: show pricing

This commit is contained in:
Björn Fromme
2026-03-16 11:59:10 +01:00
parent 6bd483d768
commit bd5c6359fe
19 changed files with 602 additions and 183 deletions
@@ -0,0 +1,290 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\BusProNet\Constants;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Service;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
use App\Form\Model\ParticipantDto;
/**
* Calculates pricing for booking components including rooms and services.
*
* This service provides comprehensive pricing calculations for the booking system,
* handling room pricing based on quantities and service pricing per participant.
* It returns structured pricing data for display in forms and summaries.
*/
class BookingPriceCalculatorService
{
/**
* Calculates comprehensive pricing breakdown for a booking.
*
* @param BookingDtoInterface $bookingDto The booking data to calculate pricing for
*
* @return array{rooms: array, services: array, grandTotal: float} Complete pricing breakdown
*/
public function getPricingBreakdown(BookingDtoInterface $bookingDto): array
{
$roomPricing = $this->calculateRoomPricing($bookingDto);
$servicePricing = $this->calculateServicePricing($bookingDto);
$grandTotal = $this->calculateGrandTotal($bookingDto);
return [
'rooms' => $roomPricing,
'services' => $servicePricing,
'grandTotal' => $grandTotal,
];
}
/**
* Calculates pricing for all selected rooms.
*
* @param BookingDtoInterface $bookingDto The booking data containing room selections
*
* @return array Array of room pricing data with labels, quantities, and totals
*/
public function calculateRoomPricing(BookingDtoInterface $bookingDto): array
{
$roomPricing = [];
if (false === $bookingDto instanceof BookingCreateDto) {
return $roomPricing;
}
$selectedRooms = $bookingDto->getSelectedRooms();
if (true === empty($selectedRooms)) {
return $roomPricing;
}
foreach ($selectedRooms as $roomSelection) {
$room = $this->getRoomById($bookingDto, $roomSelection->roomId);
if (null === $room || null === $room->price) {
continue;
}
$totalPrice = $roomSelection->quantity * $room->price;
$roomPricing[] = [
'roomId' => $room->id,
'label' => $room->label,
'quantity' => $roomSelection->quantity,
'unitPrice' => $room->price,
'totalPrice' => $totalPrice,
];
}
return $roomPricing;
}
/**
* Calculates pricing for all selected services across all participants, grouped by subtype.
*
* @param BookingDtoInterface $bookingDto The booking data containing participants and their service selections
*
* @return array Array of service groups with each group containing services of the same subtype
*/
public function calculateServicePricing(BookingDtoInterface $bookingDto): array
{
$participants = $bookingDto->getParticipants();
if (true === empty($participants)) {
return [];
}
// Aggregate service selections across all participants
$serviceAggregation = [];
foreach ($participants as $participant) {
$this->aggregateParticipantServices($participant, $serviceAggregation);
}
// Group services by subtype and convert to pricing format
return $this->groupServicesBySubtype($serviceAggregation);
}
/**
* Calculates the grand total for the entire booking.
*
* @param BookingDtoInterface $bookingDto The booking data to calculate total for
*
* @return float The grand total price
*/
public function calculateGrandTotal(BookingDtoInterface $bookingDto): float
{
$roomTotal = $this->calculateRoomTotal($bookingDto);
$serviceTotal = $this->calculateServiceTotal($bookingDto);
return $roomTotal + $serviceTotal;
}
/**
* Calculates total price for all rooms.
*/
public function calculateRoomTotal(BookingDtoInterface $bookingDto): float
{
$roomPricing = $this->calculateRoomPricing($bookingDto);
return array_sum(array_column($roomPricing, 'totalPrice'));
}
/**
* Calculates total price for all services.
*/
public function calculateServiceTotal(BookingDtoInterface $bookingDto): float
{
$servicePricing = $this->calculateServicePricing($bookingDto);
return array_sum(array_column($servicePricing, 'groupTotal'));
}
/**
* Formats a price value for display with proper German formatting.
*
* @param float $price The price to format
*
* @return string Formatted price string (e.g., "123,45")
*/
public function formatPrice(float $price): string
{
return number_format($price, 2, ',', '.');
}
/**
* Formats a price with Euro symbol for display.
*
* @param float $price The price to format
*
* @return string Formatted price string with Euro symbol (e.g., "€123,45")
*/
public function formatPriceWithSymbol(float $price): string
{
return '€'.$this->formatPrice($price);
}
/**
* Groups services by their subtypes for display.
*
* @param array $serviceAggregation Aggregated service data
*
* @return array Grouped services by subtype
*/
private function groupServicesBySubtype(array $serviceAggregation): array
{
$groupedServices = [];
foreach ($serviceAggregation as $serviceData) {
if ($serviceData['totalPrice'] <= 0) {
continue;
}
$subType = $serviceData['subType'] ?? 'other';
$groupName = $this->getGroupNameForSubtype($subType);
if (false === isset($groupedServices[$groupName])) {
$groupedServices[$groupName] = [
'groupName' => $groupName,
'services' => [],
'groupTotal' => 0.0,
];
}
$groupedServices[$groupName]['services'][] = $serviceData;
$groupedServices[$groupName]['groupTotal'] += $serviceData['totalPrice'];
}
return array_values($groupedServices);
}
/**
* Maps service subtypes to user-friendly group names.
*/
private function getGroupNameForSubtype(string $subType): string
{
$groupMapping = [
Constants::TOKEN_COURSES => 'Kurse',
Constants::TOKEN_SKI_PASS => 'Skipässe',
Constants::TOKEN_ADDITIONAL => 'Zusatzleistungen',
Constants::TOKEN_BOARD => 'Verpflegung',
];
// Handle rentals array
if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) {
return 'Leihmaterial';
}
return $groupMapping[$subType] ?? 'Sonstige Leistungen';
}
/**
* Aggregates service selections from a single participant into the service aggregation array.
*/
private function aggregateParticipantServices(ParticipantDto $participant, array &$serviceAggregation): void
{
// Handle single service selections (skiPass)
if (null !== $participant->skiPass && null !== $participant->skiPass->price) {
$this->addToServiceAggregation($serviceAggregation, $participant->skiPass, 1);
}
// Handle multiple service selections
$multipleServiceArrays = [
'courses' => $participant->courses,
'additionalServices' => $participant->additionalServices,
'board' => $participant->board,
'rentals' => $participant->rentals,
];
foreach ($multipleServiceArrays as $serviceArray) {
if (true === is_array($serviceArray)) {
foreach ($serviceArray as $service) {
if ($service instanceof Service && null !== $service->price) {
$this->addToServiceAggregation($serviceAggregation, $service, 1);
}
}
}
}
}
/**
* Adds a service to the aggregation array, incrementing count and updating total price.
*/
private function addToServiceAggregation(array &$serviceAggregation, Service $service, int $quantity): void
{
$serviceKey = $service->id.'_'.$service->label;
if (false === isset($serviceAggregation[$serviceKey])) {
$serviceAggregation[$serviceKey] = [
'serviceId' => $service->id,
'label' => $service->label,
'unitPrice' => $service->price,
'participantCount' => 0,
'totalPrice' => 0.0,
'subType' => $service->subType,
];
}
$serviceAggregation[$serviceKey]['participantCount'] += $quantity;
$serviceAggregation[$serviceKey]['totalPrice'] += $service->price * $quantity;
}
/**
* Retrieves a room by ID from the booking's travel data.
*/
private function getRoomById(BookingCreateDto $bookingDto, ?int $roomId): ?Room
{
if (null === $roomId) {
return null;
}
foreach ($bookingDto->travel->rooms as $room) {
if ($room->id === $roomId) {
return $room;
}
}
return null;
}
}
+5 -2
View File
@@ -16,6 +16,7 @@ class BookingService
public function __construct(
private readonly TravelDataService $travelDataService,
private readonly BookingPriceCalculatorService $priceCalculator,
) {
}
@@ -156,18 +157,20 @@ class BookingService
}
/**
* Returns a summary of selected rooms and the resulting participant count for a booking.
* Returns a summary of selected rooms, participant count, and pricing information for a booking.
*
* @return array{selectedRooms: array, participantCount: int}
* @return array{selectedRooms: array, participantCount: int, pricing: array}
*/
public function getRoomSummaryAndParticipantCount(BookingCreateDto $bookingCreateDto): array
{
$selectedRooms = $bookingCreateDto->getSelectedRooms();
$participantCount = $this->getParticipantsCount($selectedRooms, $bookingCreateDto->travel);
$pricing = $this->priceCalculator->getPricingBreakdown($bookingCreateDto);
return [
'selectedRooms' => $selectedRooms,
'participantCount' => $participantCount,
'pricing' => $pricing,
];
}