Files
myep/src/Service/BookingPricingAssembler.php
T

442 lines
17 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service;
use App\BusProNet\Constants;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Insurance;
use App\BusProNet\Model\Service;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
/**
* Assembles display-ready pricing breakdowns for booking summaries and diagnostics.
*
* Transforms raw booking data into structured arrays of rooms, service groups,
* and surcharges ready for rendering in Twig templates or diagnostic payloads.
* For numeric price totals only, use BookingPriceCalculator.
*/
class BookingPricingAssembler
{
public function __construct(
private readonly RoomPricingCalculator $roomPricingCalculator,
private readonly ParticipantEligibilityChecker $participantEligibilityChecker,
private readonly InsuranceManager $insuranceService,
private readonly ParticipantPricingCalculator $participantPricingCalculator,
) {
}
/**
* Assembles a complete pricing breakdown for display or diagnostic use.
*
* @return array{rooms: array<int, array<string, mixed>>, services: array<int, array<string, mixed>>, grandTotal: float, surcharges?: array<int, array<string, mixed>>}
*/
public function getPricingBreakdown(
BookingDto $bookingDto,
string $roomPricingMode = RoomPricingCalculator::PRICING_MODE_ASSIGNMENT,
): array {
$roomPricing = $this->roomPricingCalculator->calculateRoomPricing($bookingDto, $roomPricingMode);
$servicePricing = $this->calculateServicePricing($bookingDto);
$grandTotal = array_sum(array_column($roomPricing, 'totalPrice'))
+ array_sum(array_column($servicePricing, 'groupTotal'));
$result = [
'rooms' => $roomPricing,
'services' => $servicePricing,
'grandTotal' => $grandTotal,
];
if (null !== $bookingDto->booking && [] !== $bookingDto->booking->surcharges) {
$surchargePricing = $this->calculateSurchargePricing($bookingDto->booking);
if ([] !== $surchargePricing) {
$result['surcharges'] = $surchargePricing;
$result['grandTotal'] += array_sum(array_column($surchargePricing, 'totalPrice'));
}
}
return $result;
}
/**
* Calculates pricing for all selected services across all participants, grouped by subtype.
*
* Only includes services from eligible participants (those with available skipasses for their age).
*
* @return array<int, array<string, mixed>>
*/
public function calculateServicePricing(BookingDto $bookingDto): array
{
$participants = $bookingDto->getParticipants();
if ([] === $participants) {
return [];
}
$serviceAggregation = [];
foreach ($participants as $participantIndex => $participant) {
if (false === $this->participantEligibilityChecker->isParticipantEligible($bookingDto, $participantIndex)) {
continue;
}
$this->aggregateParticipantServices($participant, $serviceAggregation, $bookingDto);
}
foreach ($this->aggregateTransportationServices($bookingDto) as $key => $transportationItem) {
$serviceAggregation[$key] = $transportationItem;
}
return $this->groupServicesBySubtype($serviceAggregation);
}
/**
* Calculates surcharge pricing grouped by label (edit mode only).
*
* @return array<int, array<string, mixed>>
*/
public function calculateSurchargePricing(Booking $bookingData): array
{
$surchargePricing = [];
foreach ($bookingData->surcharges as $surcharge) {
$surchargePricing[] = [
'label' => $surcharge->label ?? 'unbekannt',
'participantCount' => count($surcharge->mapping),
'totalPrice' => $surcharge->totalPrice ?? 0.0,
];
}
return $surchargePricing;
}
/**
* Aggregates all transportation-related services and pricing into separate line items.
*
* Creates separate entries for:
* - Beförderung: Sum of all positive pickup prices and base transportation costs
* - Beförderung - Rabatt: Sum of all negative transportation prices (discounts)
* - Parkplatz: Sum of all parking service prices
*
* Only includes transportation costs from eligible participants.
*
* @return array<string, array<string, mixed>>
*/
private function aggregateTransportationServices(BookingDto $bookingDto): array
{
$participants = $bookingDto->getParticipants();
$transportationPositiveTotal = 0.0;
$transportationDiscountTotal = 0.0;
$parkingTotal = 0.0;
$transportationParticipants = 0;
$discountParticipants = 0;
$parkingParticipants = 0;
foreach ($participants as $participantIndex => $participant) {
if (false === $this->participantEligibilityChecker->isParticipantEligible($bookingDto, $participantIndex)) {
continue;
}
$participantTransportationPositiveCost = 0.0;
$participantTransportationDiscountCost = 0.0;
$participantParkingCost = 0.0;
if (null !== $participant->transportationOutbound && null !== $participant->transportationOutbound->price) {
if ($participant->transportationOutbound->price < 0) {
$participantTransportationDiscountCost += $participant->transportationOutbound->price;
} else {
$participantTransportationPositiveCost += $participant->transportationOutbound->price;
}
}
if (null !== $participant->transportationInbound && null !== $participant->transportationInbound->price) {
if ($participant->transportationInbound->price < 0) {
$participantTransportationDiscountCost += $participant->transportationInbound->price;
} else {
$participantTransportationPositiveCost += $participant->transportationInbound->price;
}
}
$hasOutboundBus = null !== $participant->transportationOutbound
&& 'BUS' === $participant->transportationOutbound->subType;
if ($hasOutboundBus && null !== $participant->pickup && null !== $participant->pickup->price) {
if ($participant->pickup->price < 0) {
$participantTransportationDiscountCost += $participant->pickup->price;
} else {
$participantTransportationPositiveCost += $participant->pickup->price;
}
}
if (null !== $participant->dropOff && null !== $participant->dropOff->price) {
if ($participant->dropOff->price < 0) {
$participantTransportationDiscountCost += $participant->dropOff->price;
} else {
$participantTransportationPositiveCost += $participant->dropOff->price;
}
}
if (null !== $participant->parkingService && null !== $participant->parkingService->price) {
$participantParkingCost += $participant->parkingService->price;
}
if ($participantTransportationPositiveCost > 0) {
$transportationPositiveTotal += $participantTransportationPositiveCost;
++$transportationParticipants;
}
if ($participantTransportationDiscountCost < 0) {
$transportationDiscountTotal += $participantTransportationDiscountCost;
++$discountParticipants;
}
if ($participantParkingCost > 0) {
$parkingTotal += $participantParkingCost;
++$parkingParticipants;
}
}
$transportationItems = [];
if ($transportationPositiveTotal > 0) {
$transportationItems['transportation_positive'] = [
'serviceId' => 'transportation_positive',
'label' => 'Beförderung',
'unitPrice' => null,
'participantCount' => $transportationParticipants,
'totalPrice' => $transportationPositiveTotal,
'subType' => Constants::GROUP_TRANSPORTATION,
];
}
if ($transportationDiscountTotal < 0) {
$transportationItems['transportation_discount'] = [
'serviceId' => 'transportation_discount',
'label' => 'Beförderung - Rabatt',
'unitPrice' => null,
'participantCount' => $discountParticipants,
'totalPrice' => $transportationDiscountTotal,
'subType' => Constants::GROUP_TRANSPORTATION.'_discount',
];
}
if ($parkingTotal > 0) {
$transportationItems['transportation_parking'] = [
'serviceId' => 'transportation_parking',
'label' => Constants::SERVICE_LABELS[Constants::TOKEN_PARKING],
'unitPrice' => null,
'participantCount' => $parkingParticipants,
'totalPrice' => $parkingTotal,
'subType' => Constants::GROUP_TRANSPORTATION,
];
}
return $transportationItems;
}
/**
* Groups services by their subtypes for display, separating positive costs and discounts.
*
* @param array<string, array<string, mixed>> $serviceAggregation
*
* @return array<int, array<string, mixed>>
*/
private function groupServicesBySubtype(array $serviceAggregation): array
{
$groupedServices = [];
$servicesBySubtypeAndSign = [];
foreach ($serviceAggregation as $serviceData) {
$subType = $serviceData['subType'] ?? 'other';
if ('' === $subType) {
$subType = 'other';
}
if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) {
$subType = Constants::GROUP_RENTALS;
}
if (true === in_array($subType, Constants::TOKEN_INSURANCES, true)) {
$subType = Constants::GROUP_INSURANCE;
}
$isDiscount = $serviceData['totalPrice'] < 0;
$bucketKey = $subType.($isDiscount ? '_discount' : '_regular');
if (false === isset($servicesBySubtypeAndSign[$bucketKey])) {
$servicesBySubtypeAndSign[$bucketKey] = [
'subType' => $subType,
'isDiscount' => $isDiscount,
'services' => [],
'total' => 0.0,
'participantCount' => 0,
];
}
$servicesBySubtypeAndSign[$bucketKey]['services'][] = $serviceData;
$servicesBySubtypeAndSign[$bucketKey]['total'] += $serviceData['totalPrice'];
$servicesBySubtypeAndSign[$bucketKey]['participantCount'] += $serviceData['participantCount'];
}
foreach ($servicesBySubtypeAndSign as $bucketData) {
$baseGroupName = $this->getGroupNameForSubtype($bucketData['subType']);
$groupName = $bucketData['isDiscount'] ? $baseGroupName.' - Rabatt' : $baseGroupName;
$services = array_map(
fn (array $serviceData): array => $this->createServicePricingLineItem($serviceData),
$bucketData['services']
);
$groupedServices[] = [
'groupName' => $groupName,
'services' => $services,
'groupTotal' => $bucketData['total'],
];
}
return $groupedServices;
}
private function getGroupNameForSubtype(string $subType): string
{
if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) {
return Constants::SERVICE_LABELS[Constants::GROUP_RENTALS];
}
return Constants::SERVICE_LABELS[$subType] ?? 'Sonstige Leistungen';
}
/** @param array<string, array<string, mixed>> $serviceAggregation */
private function aggregateParticipantServices(
ParticipantDto $participant,
array &$serviceAggregation,
BookingDto $bookingDto,
): void {
if (null !== $participant->skiPass && null !== $participant->skiPass->price) {
$this->addToServiceAggregation($serviceAggregation, $participant->skiPass);
}
if (null !== $participant->veg && null !== $participant->veg->price) {
$this->addToServiceAggregation($serviceAggregation, $participant->veg);
}
if (null !== $participant->rentalInsurance && null !== $participant->rentalInsurance->price) {
$this->addToServiceAggregation($serviceAggregation, $participant->rentalInsurance);
}
$insuranceToAggregate = $this->resolveInsuranceForAggregation($participant, $bookingDto);
if (null !== $insuranceToAggregate && null !== $insuranceToAggregate->price && false === $insuranceToAggregate->isNoInsurance()) {
$this->addInsuranceToServiceAggregation($serviceAggregation, $insuranceToAggregate);
}
$multipleServiceArrays = [
'courses' => $participant->courses,
'additionalServices' => $participant->additionalServices,
'board' => $participant->board,
'rentals' => $participant->rentals,
];
foreach ($multipleServiceArrays as $serviceArray) {
foreach ($serviceArray as $service) {
if (null !== $service->price) {
$this->addToServiceAggregation($serviceAggregation, $service);
}
}
}
}
/** @param array<string, array<string, mixed>> $serviceAggregation */
private function addToServiceAggregation(array &$serviceAggregation, Service $service, int $quantity = 1): 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;
}
/** @param array<string, array<string, mixed>> $serviceAggregation */
private function addInsuranceToServiceAggregation(array &$serviceAggregation, Insurance $insurance, int $quantity = 1): void
{
$serviceKey = $insurance->id.'_'.$insurance->label;
if (false === isset($serviceAggregation[$serviceKey])) {
$serviceAggregation[$serviceKey] = [
'serviceId' => $insurance->id,
'label' => $insurance->label,
'unitPrice' => $insurance->price,
'participantCount' => 0,
'totalPrice' => 0.0,
'subType' => $insurance->getSubType(),
];
}
$serviceAggregation[$serviceKey]['participantCount'] += $quantity;
$serviceAggregation[$serviceKey]['totalPrice'] += $insurance->price * $quantity;
}
/**
* @param array<string, mixed> $serviceData
*
* @return array<string, mixed>
*/
private function createServicePricingLineItem(array $serviceData): array
{
return [
'serviceId' => $serviceData['serviceId'] ?? null,
'label' => (string) ($serviceData['label'] ?? ''),
'unitPrice' => array_key_exists('unitPrice', $serviceData) ? $serviceData['unitPrice'] : null,
'participantCount' => (int) ($serviceData['participantCount'] ?? 0),
'totalPrice' => (float) ($serviceData['totalPrice'] ?? 0.0),
'subType' => $serviceData['subType'] ?? null,
];
}
private function resolveInsuranceForAggregation(ParticipantDto $participant, BookingDto $bookingDto): ?Insurance
{
if (null !== $participant->insurance) {
return $participant->insurance;
}
$applicant = $bookingDto->getParticipant(0);
if (null === $applicant || false === $applicant->bulkInsuranceBooking || null === $applicant->insurance) {
return null;
}
if (0 === $participant->index) {
return $participant->insurance;
}
$selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel);
$sameTypeInsurances = $this->insuranceService->filterByType(
$selectableInsurances,
$applicant->insurance
);
$travelPrice = $this->participantPricingCalculator->calculateIndividualParticipantPriceExcludingInsurance(
$bookingDto,
$participant->index
);
$eligibleInsurances = $this->insuranceService->getEligibleInsurances(
$sameTypeInsurances,
$participant,
$bookingDto,
$travelPrice
);
return !empty($eligibleInsurances) ? array_values($eligibleInsurances)[0] : null;
}
}