fix: account for discounts applied by bpn api in validation request
This commit is contained in:
@@ -102,4 +102,45 @@ class BookingResponse
|
|||||||
2
|
2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets API-applied discounts that cannot be predicted by the local price calculator.
|
||||||
|
*
|
||||||
|
* The BusProNet API may apply automatic discounts based on business rules that
|
||||||
|
* are not known to the local application, such as:
|
||||||
|
* - ERM/GRU: Gruppenrabatt (group discount for large bookings)
|
||||||
|
* - Other ERM subtypes: Various automatic discounts
|
||||||
|
*
|
||||||
|
* These discounts appear as negative price items and must be accounted for when
|
||||||
|
* comparing the API total price against the locally calculated price.
|
||||||
|
*
|
||||||
|
* Note: Voucher discounts (AKTION, KULANZGUTSCHEIN) are handled separately via
|
||||||
|
* getVoucherDiscountFromPrices() as they are user-initiated, not automatic.
|
||||||
|
*
|
||||||
|
* @return array<PriceItem> Price items representing API-applied automatic discounts
|
||||||
|
*/
|
||||||
|
public function getApiAppliedDiscounts(): array
|
||||||
|
{
|
||||||
|
return array_filter(
|
||||||
|
$this->priceItems,
|
||||||
|
fn (PriceItem $item) => $item->totalPrice < 0
|
||||||
|
&& false === \in_array($item->type, ['AKTION', 'KULANZGUTSCHEIN'], true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets total discount from API-applied automatic discounts.
|
||||||
|
*
|
||||||
|
* Returns the absolute sum of negative price items that represent automatic
|
||||||
|
* discounts applied by the API (excluding voucher discounts which are handled separately).
|
||||||
|
*/
|
||||||
|
public function getApiAppliedDiscountTotal(): float
|
||||||
|
{
|
||||||
|
$discount = 0.0;
|
||||||
|
foreach ($this->getApiAppliedDiscounts() as $item) {
|
||||||
|
$discount += abs($item->totalPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($discount, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,12 +115,15 @@ class Step3Controller extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate price match (rounded to cent precision to avoid floating-point errors)
|
// Validate price match (rounded to cent precision to avoid floating-point errors)
|
||||||
// API gesamtpreis includes promotional/goodwill voucher discounts (negative price items),
|
// API gesamtpreis includes:
|
||||||
// but NOT purchase vouchers (those reduce restzahlung, not gesamtpreis)
|
// - Promotional/goodwill voucher discounts (negative price items with art=AKTION/KULANZGUTSCHEIN)
|
||||||
|
// - API-applied automatic discounts (e.g., Gruppenrabatt with art=ERM)
|
||||||
|
// - NOT purchase vouchers (those reduce restzahlung, not gesamtpreis)
|
||||||
$apiTotal = round($inquiryResponse->totalPrice ?? 0.0, 2);
|
$apiTotal = round($inquiryResponse->totalPrice ?? 0.0, 2);
|
||||||
$calculatedSubtotal = round($this->priceCalculator->calculateGrandTotal($bookingCreateDto), 2);
|
$calculatedSubtotal = round($this->priceCalculator->calculateGrandTotal($bookingCreateDto), 2);
|
||||||
$promoGoodwillDiscount = round($inquiryResponse->getVoucherDiscountFromPrices(), 2);
|
$promoGoodwillDiscount = round($inquiryResponse->getVoucherDiscountFromPrices(), 2);
|
||||||
$expectedTotal = round($calculatedSubtotal - $promoGoodwillDiscount, 2);
|
$apiAppliedDiscount = round($inquiryResponse->getApiAppliedDiscountTotal(), 2);
|
||||||
|
$expectedTotal = round($calculatedSubtotal - $promoGoodwillDiscount - $apiAppliedDiscount, 2);
|
||||||
|
|
||||||
if ((int) round($apiTotal * 100) !== (int) round($expectedTotal * 100)) {
|
if ((int) round($apiTotal * 100) !== (int) round($expectedTotal * 100)) {
|
||||||
return $this->handleApiError(
|
return $this->handleApiError(
|
||||||
@@ -129,6 +132,7 @@ class Step3Controller extends AbstractController
|
|||||||
'apiTotal' => $apiTotal,
|
'apiTotal' => $apiTotal,
|
||||||
'calculatedSubtotal' => $calculatedSubtotal,
|
'calculatedSubtotal' => $calculatedSubtotal,
|
||||||
'promoGoodwillDiscount' => $promoGoodwillDiscount,
|
'promoGoodwillDiscount' => $promoGoodwillDiscount,
|
||||||
|
'apiAppliedDiscount' => $apiAppliedDiscount,
|
||||||
'expectedTotal' => $expectedTotal,
|
'expectedTotal' => $expectedTotal,
|
||||||
'difference' => abs($apiTotal - $expectedTotal),
|
'difference' => abs($apiTotal - $expectedTotal),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -209,4 +209,107 @@ class BookingResponseParserTest extends TestCase
|
|||||||
$this->assertEquals(125.0, $response->paymentTerms->getPurchaseVoucherDiscount()); // 50 + 75
|
$this->assertEquals(125.0, $response->paymentTerms->getPurchaseVoucherDiscount()); // 50 + 75
|
||||||
$this->assertEquals(125.0, $response->getTotalVoucherDiscount());
|
$this->assertEquals(125.0, $response->getTotalVoucherDiscount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testParseBookingResponseWithGruppenrabatt(): void
|
||||||
|
{
|
||||||
|
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ergebnis>
|
||||||
|
<satz typ="BUCHUNG" />
|
||||||
|
<buchung>möglich</buchung>
|
||||||
|
<vorgang>321530</vorgang>
|
||||||
|
<preise>
|
||||||
|
<preis position="1" art="UNT" unterart="ZIM" bezeichnung="Doppelzimmer Dusche/WC" terminvon="27.03.2026" terminbis="29.03.2026" anzahl="6" zuordnung="1,2,3,4,5,6,7,8,9,10,11,12" preis="229,00" gesamtpreis="2748,00" />
|
||||||
|
<preis position="2" art="UNT" unterart="ZIM" bezeichnung="3er Zimmer Dusche/WC" terminvon="27.03.2026" terminbis="29.03.2026" anzahl="2" zuordnung="13,14,15,16,17,18" preis="219,00" gesamtpreis="1314,00" />
|
||||||
|
<preis position="3" art="ERM" unterart="GRU" bezeichnung="Gruppenrabatt" anzahl="1" preis="-90,00" gesamtpreis="-90,00" />
|
||||||
|
</preise>
|
||||||
|
<gesamtpreis>3972,00</gesamtpreis>
|
||||||
|
<zahlungsbedingungen>
|
||||||
|
<restzahlung betrag="3972,00" termin="02.12.2029"></restzahlung>
|
||||||
|
</zahlungsbedingungen>
|
||||||
|
</ergebnis>';
|
||||||
|
|
||||||
|
$crawler = new Crawler($xmlContent);
|
||||||
|
$response = $this->parser->parse($crawler->filter('ergebnis'));
|
||||||
|
|
||||||
|
$this->assertTrue($response->isInquiryValid());
|
||||||
|
$this->assertEquals(3972.0, $response->totalPrice);
|
||||||
|
|
||||||
|
// Verify Gruppenrabatt is NOT counted as voucher discount
|
||||||
|
$this->assertCount(0, $response->getVoucherDiscountsFromPrices());
|
||||||
|
$this->assertEquals(0.0, $response->getVoucherDiscountFromPrices());
|
||||||
|
|
||||||
|
// Verify Gruppenrabatt IS counted as API-applied discount
|
||||||
|
$apiAppliedDiscounts = $response->getApiAppliedDiscounts();
|
||||||
|
$this->assertCount(1, $apiAppliedDiscounts);
|
||||||
|
|
||||||
|
$gruppenrabatt = array_values($apiAppliedDiscounts)[0];
|
||||||
|
$this->assertEquals('ERM', $gruppenrabatt->type);
|
||||||
|
$this->assertEquals('GRU', $gruppenrabatt->subType);
|
||||||
|
$this->assertEquals('Gruppenrabatt', $gruppenrabatt->label);
|
||||||
|
$this->assertEquals(-90.0, $gruppenrabatt->totalPrice);
|
||||||
|
|
||||||
|
$this->assertEquals(90.0, $response->getApiAppliedDiscountTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseBookingResponseWithGruppenrabattAndVouchers(): void
|
||||||
|
{
|
||||||
|
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ergebnis>
|
||||||
|
<satz typ="BUCHUNG" />
|
||||||
|
<buchung>möglich</buchung>
|
||||||
|
<vorgang>321530</vorgang>
|
||||||
|
<preise>
|
||||||
|
<preis position="1" art="UNT" unterart="ZIM" bezeichnung="Doppelzimmer" preis="500,00" gesamtpreis="500,00" />
|
||||||
|
<preis position="2" art="AKTION" unterart="" bezeichnung="PROMO20" anzahl="1" zuordnung="1" preis="-20,00" gesamtpreis="-20,00" />
|
||||||
|
<preis position="3" art="ERM" unterart="GRU" bezeichnung="Gruppenrabatt" anzahl="1" preis="-50,00" gesamtpreis="-50,00" />
|
||||||
|
</preise>
|
||||||
|
<gesamtpreis>430,00</gesamtpreis>
|
||||||
|
<zahlungsbedingungen>
|
||||||
|
<restzahlung betrag="430,00" termin="02.12.2029"></restzahlung>
|
||||||
|
</zahlungsbedingungen>
|
||||||
|
</ergebnis>';
|
||||||
|
|
||||||
|
$crawler = new Crawler($xmlContent);
|
||||||
|
$response = $this->parser->parse($crawler->filter('ergebnis'));
|
||||||
|
|
||||||
|
// Voucher discount should only include AKTION
|
||||||
|
$this->assertEquals(20.0, $response->getVoucherDiscountFromPrices());
|
||||||
|
|
||||||
|
// API-applied discount should only include ERM (Gruppenrabatt)
|
||||||
|
$this->assertEquals(50.0, $response->getApiAppliedDiscountTotal());
|
||||||
|
|
||||||
|
// Verify they are separate and don't overlap
|
||||||
|
$voucherDiscounts = $response->getVoucherDiscountsFromPrices();
|
||||||
|
$apiDiscounts = $response->getApiAppliedDiscounts();
|
||||||
|
|
||||||
|
$this->assertCount(1, $voucherDiscounts);
|
||||||
|
$this->assertCount(1, $apiDiscounts);
|
||||||
|
|
||||||
|
$this->assertEquals('AKTION', array_values($voucherDiscounts)[0]->type);
|
||||||
|
$this->assertEquals('ERM', array_values($apiDiscounts)[0]->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseBookingResponseWithNoApiAppliedDiscounts(): void
|
||||||
|
{
|
||||||
|
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ergebnis>
|
||||||
|
<satz typ="BUCHUNG" />
|
||||||
|
<buchung>möglich</buchung>
|
||||||
|
<vorgang>321530</vorgang>
|
||||||
|
<preise>
|
||||||
|
<preis position="1" art="UNT" unterart="ZIM" bezeichnung="Bett im Mehrbettzimmer" preis="679,00" gesamtpreis="679,00" />
|
||||||
|
</preise>
|
||||||
|
<gesamtpreis>679,00</gesamtpreis>
|
||||||
|
<zahlungsbedingungen>
|
||||||
|
<restzahlung betrag="679,00" termin="02.12.2029"></restzahlung>
|
||||||
|
</zahlungsbedingungen>
|
||||||
|
</ergebnis>';
|
||||||
|
|
||||||
|
$crawler = new Crawler($xmlContent);
|
||||||
|
$response = $this->parser->parse($crawler->filter('ergebnis'));
|
||||||
|
|
||||||
|
// Verify no API-applied discounts
|
||||||
|
$this->assertCount(0, $response->getApiAppliedDiscounts());
|
||||||
|
$this->assertEquals(0.0, $response->getApiAppliedDiscountTotal());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user