feat: extended price mismatch diagnostics in logs

This commit is contained in:
Björn Fromme
2026-03-18 15:24:52 +01:00
parent 02ff28acdd
commit ca7d0b6965
3 changed files with 244 additions and 0 deletions
@@ -14,6 +14,7 @@ use App\Form\BookingCreateStep3Type;
use App\Form\Model\BookingDto;
use App\Htmx\HxTrait;
use App\Service\BookingPriceCalculatorService;
use App\Service\BookingPriceMismatchDiagnosticsService;
use App\Service\BookingService;
use App\Service\BookingSummaryDataService;
use Psr\Log\LoggerInterface;
@@ -36,6 +37,7 @@ class Step3Controller extends AbstractController
private readonly BookingService $bookingService,
private readonly BookingSummaryDataService $summaryDataService,
private readonly BookingPriceCalculatorService $priceCalculator,
private readonly BookingPriceMismatchDiagnosticsService $priceMismatchDiagnostics,
private readonly ApiClient $apiClient,
private readonly LoggerInterface $logger,
) {
@@ -126,6 +128,9 @@ class Step3Controller extends AbstractController
$expectedTotal = round($calculatedSubtotal - $promoGoodwillDiscount - $apiAppliedDiscount, 2);
if ((int) round($apiTotal * 100) !== (int) round($expectedTotal * 100)) {
$diagnostics = $this->priceMismatchDiagnostics->buildDiagnostics($bookingCreateDto, $inquiryResponse);
$deltaBreakdown = $diagnostics['deltaBreakdown'] ?? [];
return $this->handleApiError(
'Price mismatch detected - payload incomplete',
[
@@ -135,6 +140,11 @@ class Step3Controller extends AbstractController
'apiAppliedDiscount' => $apiAppliedDiscount,
'expectedTotal' => $expectedTotal,
'difference' => abs($apiTotal - $expectedTotal),
'deltaTotal' => $deltaBreakdown['deltaTotal'] ?? null,
'deltaRoom' => $deltaBreakdown['deltaRoom'] ?? null,
'deltaService' => $deltaBreakdown['deltaService'] ?? null,
'deltaInsurance' => $deltaBreakdown['deltaInsurance'] ?? null,
'diagnostics' => $diagnostics,
],
'Preisabweichung festgestellt. Bitte wende dich an den Kundenservice.',
$bookingCreateDto,
@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\BusProNet\Constants;
use App\BusProNet\Model\BookingResponse;
use App\BusProNet\Model\PriceItem;
use App\Form\Model\BookingDto;
/**
* Builds diagnostic payloads for booking price mismatches.
*/
class BookingPriceMismatchDiagnosticsService
{
public function __construct(
private readonly BookingPriceCalculatorService $priceCalculator,
) {
}
/**
* @return array<string, mixed>
*/
public function buildDiagnostics(BookingDto $bookingCreateDto, BookingResponse $response): array
{
$pricingBreakdown = $this->priceCalculator->getPricingBreakdown($bookingCreateDto);
$roomLines = $pricingBreakdown['rooms'] ?? [];
$serviceGroups = $pricingBreakdown['services'] ?? [];
$roomTotal = round(array_sum(array_column($roomLines, 'totalPrice')), 2);
$serviceTotal = round(array_sum(array_column($serviceGroups, 'groupTotal')), 2);
$insuranceTotal = round($this->extractInsuranceTotal($serviceGroups), 2);
$selectedRooms = [];
foreach ($bookingCreateDto->getSelectedRooms() as $roomSelection) {
$selectedRooms[] = [
'roomId' => $roomSelection->id,
'quantity' => $roomSelection->quantity,
'unitPrice' => $roomSelection->price,
'capacity' => $roomSelection->capacity,
];
}
$participantInsurance = [];
foreach ($bookingCreateDto->getParticipants() as $index => $participant) {
$participantInsurance[] = [
'participantIndex' => $index,
'bulkInsuranceBooking' => $participant->bulkInsuranceBooking,
'insuranceId' => $participant->insurance?->id,
'insurancePrice' => $participant->insurance?->price,
'assignedRoomId' => $participant->assignedRoomId,
];
}
$apiPriceItems = array_map(static function (PriceItem $item): array {
return [
'position' => $item->position,
'type' => $item->type,
'subType' => $item->subType,
'id' => $item->id,
'quantity' => $item->quantity,
'totalPrice' => round($item->totalPrice, 2),
];
}, $response->priceItems);
$apiRoomTotal = round($this->sumApiRoomTotal($response->priceItems), 2);
$apiInsuranceTotal = round($this->sumApiInsuranceTotal($response->priceItems), 2);
$apiTotal = round($response->totalPrice ?? 0.0, 2);
$apiServiceTotal = round($apiTotal - $apiRoomTotal, 2);
$deltaTotal = round(($pricingBreakdown['grandTotal'] ?? 0.0) - $apiTotal, 2);
$deltaRoom = round($roomTotal - $apiRoomTotal, 2);
$deltaService = round($serviceTotal - $apiServiceTotal, 2);
$deltaInsurance = round($insuranceTotal - $apiInsuranceTotal, 2);
return [
'localBreakdown' => [
'grandTotal' => round($pricingBreakdown['grandTotal'] ?? 0.0, 2),
'roomTotal' => $roomTotal,
'serviceTotal' => $serviceTotal,
'insuranceTotal' => $insuranceTotal,
],
'apiBreakdown' => [
'total' => $apiTotal,
'roomTotal' => $apiRoomTotal,
'serviceTotal' => $apiServiceTotal,
'insuranceTotal' => $apiInsuranceTotal,
],
'deltaBreakdown' => [
'deltaTotal' => $deltaTotal,
'deltaRoom' => $deltaRoom,
'deltaService' => $deltaService,
'deltaInsurance' => $deltaInsurance,
],
'roomLines' => $roomLines,
'serviceGroups' => $serviceGroups,
'selectedRooms' => $selectedRooms,
'roomAssignmentCounts' => $bookingCreateDto->getRoomAssignmentCounts(),
'participantInsurance' => $participantInsurance,
'apiPriceItems' => $apiPriceItems,
];
}
/**
* @param array<int, array<string, mixed>> $serviceGroups
*/
private function extractInsuranceTotal(array $serviceGroups): float
{
$total = 0.0;
foreach ($serviceGroups as $group) {
foreach (($group['services'] ?? []) as $service) {
$subType = $service['subType'] ?? null;
if (null === $subType || false === in_array($subType, Constants::TOKEN_INSURANCES, true)) {
continue;
}
$total += (float) ($service['totalPrice'] ?? 0.0);
}
}
return $total;
}
/**
* @param array<int, PriceItem> $priceItems
*/
private function sumApiRoomTotal(array $priceItems): float
{
$total = 0.0;
foreach ($priceItems as $item) {
if ('UNT' === $item->type) {
$total += $item->totalPrice;
}
}
return $total;
}
/**
* @param array<int, PriceItem> $priceItems
*/
private function sumApiInsuranceTotal(array $priceItems): float
{
$total = 0.0;
foreach ($priceItems as $item) {
if (null !== $item->subType && true === in_array($item->subType, Constants::TOKEN_INSURANCES, true)) {
$total += $item->totalPrice;
}
}
return $total;
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\Model\BookingResponse;
use App\BusProNet\Model\Insurance;
use App\BusProNet\Model\PriceItem;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Model\RoomSelectionDto;
use App\Service\BookingPriceCalculatorService;
use App\Service\BookingPriceMismatchDiagnosticsService;
use PHPUnit\Framework\TestCase;
class BookingPriceMismatchDiagnosticsServiceTest extends TestCase
{
public function testBuildDiagnosticsProvidesDeltaBreakdown(): void
{
$priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
$priceCalculator->method('getPricingBreakdown')->willReturn([
'rooms' => [
['roomId' => 74, 'totalPrice' => 444.0],
],
'services' => [
[
'groupName' => 'Reiseversicherungen',
'groupTotal' => 69.5,
'services' => [
['subType' => 'RRV', 'totalPrice' => 115.0],
],
],
],
'grandTotal' => 513.5,
]);
$service = new BookingPriceMismatchDiagnosticsService($priceCalculator);
$bookingDto = new BookingDto(new Travel(), 197136);
$roomSelection = new RoomSelectionDto();
$roomSelection->id = 74;
$roomSelection->quantity = 1;
$roomSelection->price = 444.0;
$roomSelection->capacity = 1;
$bookingDto->roomSelections = [$roomSelection];
$participant = new ParticipantDto();
$participant->index = 0;
$participant->assignedRoomId = 74;
$participant->bulkInsuranceBooking = false;
$insurance = new Insurance();
$insurance->id = '178926';
$insurance->price = 115.0;
$participant->insurance = $insurance;
$bookingDto->participants = [$participant];
$response = new BookingResponse(
status: 'möglich',
priceItems: [
new PriceItem(1, 'UNT', 'ZIM', 'Room', null, null, 1, '1', 329.0, 329.0, null),
new PriceItem(2, 'SON', 'SPA', 'Skipass', null, null, 1, '1', 49.9, 49.9, '186099'),
new PriceItem(3, 'SON', 'SON', 'Tax', null, null, 1, '1', 9.6, 9.6, '186098'),
new PriceItem(4, 'SON', 'SON', 'Card', null, null, 1, '1', 5.0, 5.0, '186097'),
new PriceItem(5, 'BEF', 'BUS', 'Bus', null, null, 1, '1', 5.0, 5.0, '186092'),
],
totalPrice: 398.5,
);
$diagnostics = $service->buildDiagnostics($bookingDto, $response);
$this->assertSame(115.0, $diagnostics['deltaBreakdown']['deltaTotal']);
$this->assertSame(115.0, $diagnostics['deltaBreakdown']['deltaRoom']);
$this->assertSame(0.0, $diagnostics['deltaBreakdown']['deltaService']);
$this->assertSame(115.0, $diagnostics['deltaBreakdown']['deltaInsurance']);
}
}