fix: list boarding surcharges per leg and count them once in the summary
This commit is contained in:
1 parent
57b86a8fbc
commit
d4296f30a5
2 files changed
+104
-75
No files matched your search
@@ -21,6 +21,12 @@ use App\Form\Model\ParticipantDto;
|
|||||||
*/
|
*/
|
||||||
class BookingPricingAssembler
|
class BookingPricingAssembler
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* BusPro's label for the per-leg boarding surcharge, suffixed " (Hinfahrt)" or " (Rückfahrt)".
|
||||||
|
* A booking response's `<zuschlag>` carries no type attribute, so the label is all there is.
|
||||||
|
*/
|
||||||
|
private const BOARDING_SURCHARGE_LABEL = 'Abfahrtsortzuschlag';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly RoomPricingCalculator $roomPricingCalculator,
|
private readonly RoomPricingCalculator $roomPricingCalculator,
|
||||||
private readonly ParticipantEligibilityChecker $participantEligibilityChecker,
|
private readonly ParticipantEligibilityChecker $participantEligibilityChecker,
|
||||||
@@ -108,6 +114,12 @@ class BookingPricingAssembler
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listed per leg among the transportation services, from the participants' current
|
||||||
|
// selection rather than the stored booking, so BusPro's copy would count it twice
|
||||||
|
if (str_starts_with($surcharge->label ?? '', self::BOARDING_SURCHARGE_LABEL)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$surchargePricing[] = [
|
$surchargePricing[] = [
|
||||||
'label' => $surcharge->label ?? 'unbekannt',
|
'label' => $surcharge->label ?? 'unbekannt',
|
||||||
'participantCount' => count($surcharge->mapping),
|
'participantCount' => count($surcharge->mapping),
|
||||||
@@ -122,8 +134,10 @@ class BookingPricingAssembler
|
|||||||
* Aggregates all transportation-related services and pricing into separate line items.
|
* Aggregates all transportation-related services and pricing into separate line items.
|
||||||
*
|
*
|
||||||
* Creates separate entries for:
|
* Creates separate entries for:
|
||||||
* - Beförderung: Sum of all positive pickup prices and base transportation costs
|
* - Beförderung: Sum of all positive base transportation costs
|
||||||
* - Beförderung - Rabatt: Sum of all negative transportation prices (discounts)
|
* - Beförderung - Rabatt: Sum of all negative base transportation prices (discounts)
|
||||||
|
* - Abfahrtsortzuschlag (Hinfahrt) / (Rückfahrt): Boarding surcharges per leg, labelled as
|
||||||
|
* BusPro bills them, so the breakdown reads like the invoice
|
||||||
* - Parkplatz: Sum of all parking service prices
|
* - Parkplatz: Sum of all parking service prices
|
||||||
*
|
*
|
||||||
* Only includes transportation costs from eligible participants.
|
* Only includes transportation costs from eligible participants.
|
||||||
@@ -134,36 +148,25 @@ class BookingPricingAssembler
|
|||||||
{
|
{
|
||||||
$participants = $bookingDto->getParticipants();
|
$participants = $bookingDto->getParticipants();
|
||||||
|
|
||||||
$transportationPositiveTotal = 0.0;
|
$totals = [
|
||||||
$transportationDiscountTotal = 0.0;
|
'transportation_positive' => 0.0,
|
||||||
$parkingTotal = 0.0;
|
'transportation_discount' => 0.0,
|
||||||
|
'transportation_pickup' => 0.0,
|
||||||
$transportationParticipants = 0;
|
'transportation_drop_off' => 0.0,
|
||||||
$discountParticipants = 0;
|
'transportation_parking' => 0.0,
|
||||||
$parkingParticipants = 0;
|
];
|
||||||
|
$participantCounts = array_fill_keys(array_keys($totals), 0);
|
||||||
|
|
||||||
foreach ($participants as $participantIndex => $participant) {
|
foreach ($participants as $participantIndex => $participant) {
|
||||||
if (false === $this->isParticipantBilled($bookingDto, $participantIndex, $participant)) {
|
if (false === $this->isParticipantBilled($bookingDto, $participantIndex, $participant)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$participantTransportationPositiveCost = 0.0;
|
$participantCosts = array_fill_keys(array_keys($totals), 0.0);
|
||||||
$participantTransportationDiscountCost = 0.0;
|
|
||||||
$participantParkingCost = 0.0;
|
|
||||||
|
|
||||||
if (null !== $participant->transportationOutbound && null !== $participant->transportationOutbound->price) {
|
foreach ([$participant->transportationOutbound, $participant->transportationInbound] as $transportation) {
|
||||||
if ($participant->transportationOutbound->price < 0) {
|
if (null !== $transportation && null !== $transportation->price) {
|
||||||
$participantTransportationDiscountCost += $participant->transportationOutbound->price;
|
$participantCosts[$transportation->price < 0 ? 'transportation_discount' : 'transportation_positive'] += $transportation->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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,71 +176,47 @@ class BookingPricingAssembler
|
|||||||
&& DirectionMapper::isBus($participant->transportationInbound->subType);
|
&& DirectionMapper::isBus($participant->transportationInbound->subType);
|
||||||
|
|
||||||
if ($hasOutboundBus && null !== $participant->pickup && null !== $participant->pickup->price) {
|
if ($hasOutboundBus && null !== $participant->pickup && null !== $participant->pickup->price) {
|
||||||
if ($participant->pickup->price < 0) {
|
$participantCosts['transportation_pickup'] += $participant->pickup->price;
|
||||||
$participantTransportationDiscountCost += $participant->pickup->price;
|
|
||||||
} else {
|
|
||||||
$participantTransportationPositiveCost += $participant->pickup->price;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hasInboundBus && null !== $participant->dropOff && null !== $participant->dropOff->price) {
|
if ($hasInboundBus && null !== $participant->dropOff && null !== $participant->dropOff->price) {
|
||||||
if ($participant->dropOff->price < 0) {
|
$participantCosts['transportation_drop_off'] += $participant->dropOff->price;
|
||||||
$participantTransportationDiscountCost += $participant->dropOff->price;
|
|
||||||
} else {
|
|
||||||
$participantTransportationPositiveCost += $participant->dropOff->price;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $participant->parkingService && null !== $participant->parkingService->price) {
|
if (null !== $participant->parkingService && null !== $participant->parkingService->price) {
|
||||||
$participantParkingCost += $participant->parkingService->price;
|
$participantCosts['transportation_parking'] += $participant->parkingService->price;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($participantTransportationPositiveCost > 0) {
|
foreach ($participantCosts as $key => $cost) {
|
||||||
$transportationPositiveTotal += $participantTransportationPositiveCost;
|
if (0.0 !== round($cost, 2)) {
|
||||||
++$transportationParticipants;
|
$totals[$key] += $cost;
|
||||||
}
|
++$participantCounts[$key];
|
||||||
if ($participantTransportationDiscountCost < 0) {
|
}
|
||||||
$transportationDiscountTotal += $participantTransportationDiscountCost;
|
|
||||||
++$discountParticipants;
|
|
||||||
}
|
|
||||||
if ($participantParkingCost > 0) {
|
|
||||||
$parkingTotal += $participantParkingCost;
|
|
||||||
++$parkingParticipants;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$definitions = [
|
||||||
|
'transportation_positive' => ['Beförderung', Constants::GROUP_TRANSPORTATION],
|
||||||
|
'transportation_discount' => ['Beförderung - Rabatt', Constants::GROUP_TRANSPORTATION.'_discount'],
|
||||||
|
'transportation_pickup' => [self::BOARDING_SURCHARGE_LABEL.' (Hinfahrt)', Constants::GROUP_TRANSPORTATION],
|
||||||
|
'transportation_drop_off' => [self::BOARDING_SURCHARGE_LABEL.' (Rückfahrt)', Constants::GROUP_TRANSPORTATION],
|
||||||
|
'transportation_parking' => [Constants::SERVICE_LABELS[Constants::TOKEN_PARKING], Constants::GROUP_TRANSPORTATION],
|
||||||
|
];
|
||||||
|
|
||||||
$transportationItems = [];
|
$transportationItems = [];
|
||||||
|
|
||||||
if ($transportationPositiveTotal > 0) {
|
foreach ($definitions as $key => [$label, $subType]) {
|
||||||
$transportationItems['transportation_positive'] = [
|
if (0 === $participantCounts[$key]) {
|
||||||
'serviceId' => 'transportation_positive',
|
continue;
|
||||||
'label' => 'Beförderung',
|
}
|
||||||
'unitPrice' => null,
|
|
||||||
'participantCount' => $transportationParticipants,
|
|
||||||
'totalPrice' => $transportationPositiveTotal,
|
|
||||||
'subType' => Constants::GROUP_TRANSPORTATION,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($transportationDiscountTotal < 0) {
|
$transportationItems[$key] = [
|
||||||
$transportationItems['transportation_discount'] = [
|
'serviceId' => $key,
|
||||||
'serviceId' => 'transportation_discount',
|
'label' => $label,
|
||||||
'label' => 'Beförderung - Rabatt',
|
|
||||||
'unitPrice' => null,
|
'unitPrice' => null,
|
||||||
'participantCount' => $discountParticipants,
|
'participantCount' => $participantCounts[$key],
|
||||||
'totalPrice' => $transportationDiscountTotal,
|
'totalPrice' => $totals[$key],
|
||||||
'subType' => Constants::GROUP_TRANSPORTATION.'_discount',
|
'subType' => $subType,
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -328,6 +328,25 @@ class BookingPricingAssemblerTest extends TestCase
|
|||||||
$this->assertEqualsWithDelta(71.6, $group['groupTotal'], 0.001, 'Both legs are charged at €17,90 per participant');
|
$this->assertEqualsWithDelta(71.6, $group['groupTotal'], 0.001, 'Both legs are charged at €17,90 per participant');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testBoardingSurchargeIsListedPerLegAsBusProBillsIt(): void
|
||||||
|
{
|
||||||
|
$bookingDto = $this->createBusBookingDto('BUS', 'BUS');
|
||||||
|
|
||||||
|
$group = $this->findServiceGroup($this->assembler->calculateServicePricing($bookingDto), 'Beförderung');
|
||||||
|
|
||||||
|
$this->assertSame([
|
||||||
|
['label' => 'Abfahrtsortzuschlag (Hinfahrt)', 'participantCount' => 2, 'totalPrice' => 35.8],
|
||||||
|
['label' => 'Abfahrtsortzuschlag (Rückfahrt)', 'participantCount' => 2, 'totalPrice' => 35.8],
|
||||||
|
], array_map(
|
||||||
|
static fn (array $line): array => [
|
||||||
|
'label' => $line['label'],
|
||||||
|
'participantCount' => $line['participantCount'],
|
||||||
|
'totalPrice' => round($line['totalPrice'], 2),
|
||||||
|
],
|
||||||
|
$group['services']
|
||||||
|
), 'A free bus leg adds no Beförderung line of its own');
|
||||||
|
}
|
||||||
|
|
||||||
public function testBoardingSurchargeSkipsTheReturnLegWhenItIsNotByBus(): void
|
public function testBoardingSurchargeSkipsTheReturnLegWhenItIsNotByBus(): void
|
||||||
{
|
{
|
||||||
$bookingDto = $this->createBusBookingDto('BUS', 'PKW');
|
$bookingDto = $this->createBusBookingDto('BUS', 'PKW');
|
||||||
@@ -409,6 +428,37 @@ class BookingPricingAssemblerTest extends TestCase
|
|||||||
], $surchargePricing);
|
], $surchargePricing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSurchargePricingOmitsBoardingSurchargesListedAmongTheServices(): void
|
||||||
|
{
|
||||||
|
// Booking 98398: BusPro reports the boarding surcharge as untyped <zuschlag> lines,
|
||||||
|
// which the transportation group already lists per leg.
|
||||||
|
$booking = new Booking();
|
||||||
|
$booking->surcharges = [
|
||||||
|
$this->createSurcharge('Abfahrtsortzuschlag (Hinfahrt)', 141.9, range(0, 10)),
|
||||||
|
$this->createSurcharge('Abfahrtsortzuschlag (Rückfahrt)', 141.9, range(0, 10)),
|
||||||
|
$this->createSurcharge('Stornokosten', 120.0, [3]),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertSame([
|
||||||
|
['label' => 'Stornokosten', 'participantCount' => 1, 'totalPrice' => 120.0],
|
||||||
|
], $this->assembler->calculateSurchargePricing($booking));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPricingBreakdownCountsTheBoardingSurchargeOnceInEditMode(): void
|
||||||
|
{
|
||||||
|
$bookingDto = $this->createBusBookingDto('BUS', 'BUS');
|
||||||
|
$bookingDto->booking = new Booking();
|
||||||
|
$bookingDto->booking->surcharges = [
|
||||||
|
$this->createSurcharge('Abfahrtsortzuschlag (Hinfahrt)', 35.8, [0, 1]),
|
||||||
|
$this->createSurcharge('Abfahrtsortzuschlag (Rückfahrt)', 35.8, [0, 1]),
|
||||||
|
];
|
||||||
|
|
||||||
|
$breakdown = $this->assembler->getPricingBreakdown($bookingDto);
|
||||||
|
|
||||||
|
$this->assertArrayNotHasKey('surcharges', $breakdown);
|
||||||
|
$this->assertEqualsWithDelta(71.6, $breakdown['grandTotal'], 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
public function testPricingBreakdownOmitsSurchargesWhenAllAreZero(): void
|
public function testPricingBreakdownOmitsSurchargesWhenAllAreZero(): void
|
||||||
{
|
{
|
||||||
$travel = new Travel();
|
$travel = new Travel();
|
||||||
|
|||||||
Reference in new issue
Block a user