feat: promotional and purchase vouchers
This commit is contained in:
@@ -12,6 +12,8 @@ use App\BusProNet\Model\BookingUpdate;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\Model\PromoVoucher;
|
||||
use App\BusProNet\Model\PurchaseVoucher;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\Traits\ApiClientTrait;
|
||||
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||
@@ -39,6 +41,8 @@ class ApiClient
|
||||
public const TYPE_PRODUCTS = 'PRODUKTE';
|
||||
public const TYPE_PRODUCT_DATA = 'PRODUKTDATEN';
|
||||
public const TYPE_AGENCIES = 'AGENTUREN';
|
||||
public const TYPE_PURCHASE_VOUCHER = 'GUTSCHEINPRUEFUNGEINLOESUNG';
|
||||
public const TYPE_PROMO_VOUCHER = 'AKTIONSGUTSCHEIN';
|
||||
|
||||
private array $config;
|
||||
|
||||
@@ -409,6 +413,62 @@ class ApiClient
|
||||
return $this->sendRequest(static::TYPE_AGENCIES, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a purchase voucher by redemption code.
|
||||
*
|
||||
* Purchase vouchers (Gutscheine) have a remaining balance that reduces with each redemption.
|
||||
* They can be regular purchased vouchers or goodwill vouchers (Kulanz).
|
||||
* Returns the voucher with remaining balance, or a Notification if not found or invalid.
|
||||
*
|
||||
* @param string $redemptionCode The voucher redemption code (einloesecode)
|
||||
*
|
||||
* @return Notification|PurchaseVoucher Notification on error, PurchaseVoucher on success
|
||||
*
|
||||
* @throws ApiClientException If the API request fails
|
||||
*/
|
||||
public function validatePurchaseVoucher(string $redemptionCode): Notification|PurchaseVoucher
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_PURCHASE_VOUCHER),
|
||||
'satz' => ['@typ' => static::TYPE_PURCHASE_VOUCHER],
|
||||
'einloesecode' => $redemptionCode,
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_PURCHASE_VOUCHER, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a promo voucher for a specific travel and participant price.
|
||||
*
|
||||
* Promo vouchers (Aktionsgutscheine) provide absolute discounts per person or per booking.
|
||||
* Applicability is indicated by the pro_buchung_person field: "P" = per person, "B" = per booking.
|
||||
*
|
||||
* @param string $promoCode The promo voucher code
|
||||
* @param int $travelId The travel ID this voucher applies to
|
||||
* @param float $participantPrice Current participant price for validation
|
||||
*
|
||||
* @return Notification|PromoVoucher Notification on error, PromoVoucher on success
|
||||
*
|
||||
* @throws ApiClientException If the API request fails
|
||||
*/
|
||||
public function validatePromoVoucher(string $promoCode, int $travelId, float $participantPrice): Notification|PromoVoucher
|
||||
{
|
||||
// Format price to German format (comma decimal separator)
|
||||
$priceFormatted = number_format($participantPrice, 2, ',', '');
|
||||
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_PROMO_VOUCHER),
|
||||
'satz' => ['@typ' => static::TYPE_PROMO_VOUCHER],
|
||||
'aktionsgutschein' => $promoCode,
|
||||
'idreise' => $travelId,
|
||||
'preis' => $priceFormatted,
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_PROMO_VOUCHER, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
|
||||
@@ -274,6 +274,17 @@ class BookingDataProcessor
|
||||
$this->buildServicesPayload($payload, $bookingData);
|
||||
$this->buildPickupPayload($payload, $bookingData);
|
||||
|
||||
// Add purchase vouchers if any exist
|
||||
$purchaseVouchers = $this->collectPurchaseVouchers($formData);
|
||||
if (false === empty($purchaseVouchers)) {
|
||||
$payload['gutscheine']['gutschein'] = [];
|
||||
foreach ($purchaseVouchers as $code) {
|
||||
$payload['gutscheine']['gutschein'][] = [
|
||||
'@einloesecode' => $code,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
@@ -676,6 +687,11 @@ class BookingDataProcessor
|
||||
$participantPayload['wünsche'] = $wishes;
|
||||
}
|
||||
}
|
||||
|
||||
// Add promo voucher if present
|
||||
if (null !== $dto->promoVoucherCode && '' !== trim($dto->promoVoucherCode)) {
|
||||
$participantPayload['aktionscode'] = trim($dto->promoVoucherCode);
|
||||
}
|
||||
}
|
||||
|
||||
$payload['teilnehmerliste']['teilnehmer'][] = $participantPayload;
|
||||
@@ -867,6 +883,11 @@ class BookingDataProcessor
|
||||
}
|
||||
}
|
||||
|
||||
// Add promo voucher if present
|
||||
if (null !== $participant->promoVoucherCode && '' !== trim($participant->promoVoucherCode)) {
|
||||
$participantData['aktionscode'] = trim($participant->promoVoucherCode);
|
||||
}
|
||||
|
||||
$payload['teilnehmerliste']['teilnehmer'][] = $participantData;
|
||||
}
|
||||
|
||||
@@ -884,6 +905,17 @@ class BookingDataProcessor
|
||||
$this->addServicesFromMap($payload, 'zustiege', 'zustieg', '@idzustieg', $pickupMap);
|
||||
$this->addServicesFromMap($payload, 'versicherungen', 'versicherung', '@idversicherung', $insuranceMap);
|
||||
|
||||
// Add purchase vouchers if any exist
|
||||
$purchaseVouchers = $this->collectPurchaseVouchers($bookingDto);
|
||||
if (false === empty($purchaseVouchers)) {
|
||||
$payload['gutscheine']['gutschein'] = [];
|
||||
foreach ($purchaseVouchers as $code) {
|
||||
$payload['gutscheine']['gutschein'][] = [
|
||||
'@einloesecode' => $code,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Add payment information
|
||||
$payload['zahlung'] = [
|
||||
'@idzahlungsart' => Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod
|
||||
@@ -1131,6 +1163,28 @@ class BookingDataProcessor
|
||||
return $insuranceMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all purchase vouchers from participants.
|
||||
*
|
||||
* Removes duplicates and empty values, returning unique redemption codes
|
||||
* for aggregation into the <gutscheine> collection in the booking payload.
|
||||
*
|
||||
* @return string[] Array of unique redemption codes
|
||||
*/
|
||||
private function collectPurchaseVouchers(BookingDto $bookingDto): array
|
||||
{
|
||||
$vouchers = [];
|
||||
|
||||
foreach ($bookingDto->participants as $participant) {
|
||||
if (null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
|
||||
$vouchers[] = trim($participant->purchaseVoucherCode);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates (in case multiple participants enter same code)
|
||||
return array_unique($vouchers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies bulk insurance assignment if the applicant has enabled it.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* Promo voucher validation response from BPN API.
|
||||
*
|
||||
* Represents a validated promotional voucher (Aktionsgutschein) applied per participant.
|
||||
* These vouchers provide discounts either as fixed amounts or percentages.
|
||||
*/
|
||||
final readonly class PromoVoucher
|
||||
{
|
||||
public const APPLICABILITY_PER_PERSON = 'P';
|
||||
public const APPLICABILITY_PER_BOOKING = 'B';
|
||||
|
||||
public function __construct(
|
||||
public string $code,
|
||||
public string $description,
|
||||
public string $applicability,
|
||||
public float $discountAmount,
|
||||
public float $discountPercentage,
|
||||
public float $minimumPrice,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isPerPerson(): bool
|
||||
{
|
||||
return self::APPLICABILITY_PER_PERSON === $this->applicability;
|
||||
}
|
||||
|
||||
public function isPerBooking(): bool
|
||||
{
|
||||
return self::APPLICABILITY_PER_BOOKING === $this->applicability;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
final readonly class PromoVoucherValidationResult
|
||||
{
|
||||
public function __construct(
|
||||
public bool $isValid,
|
||||
public ?PromoVoucher $voucher = null,
|
||||
public ?string $errorMessage = null,
|
||||
public ?int $errorCode = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* Purchase voucher validation response from BPN API.
|
||||
*
|
||||
* Represents a validated purchase voucher (Gutschein) with redemption code.
|
||||
* These vouchers have a remaining balance that can be redeemed against bookings.
|
||||
* The voucher reduces the booking price by its balance until depleted.
|
||||
*
|
||||
* Purchase vouchers can be regular purchased vouchers or goodwill vouchers (Kulanz)
|
||||
* issued to customers for complaints or other reasons.
|
||||
*/
|
||||
final readonly class PurchaseVoucher
|
||||
{
|
||||
public function __construct(
|
||||
public string $voucherNumber,
|
||||
public string $redemptionCode,
|
||||
public int $voucherId,
|
||||
public float $remainingBalance,
|
||||
public string $type,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
final readonly class PurchaseVoucherValidationResult
|
||||
{
|
||||
public function __construct(
|
||||
public bool $isValid,
|
||||
public ?PurchaseVoucher $voucher = null,
|
||||
public ?string $errorMessage = null,
|
||||
public ?int $errorCode = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,10 @@ class ApiResponseParser extends AbstractParser
|
||||
return (new TravelParser())->parse($travelNode->first(), ...$additionalArgs);
|
||||
case ApiClient::TYPE_AGENCIES:
|
||||
return (new AgencyParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PURCHASE_VOUCHER:
|
||||
return (new PurchaseVoucherParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PROMO_VOUCHER:
|
||||
return (new PromoVoucherParser())->parse($resultNode);
|
||||
}
|
||||
|
||||
throw new ResponseParserException('Unable to parse XML response');
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\PromoVoucher;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
final class PromoVoucherParser extends AbstractParser
|
||||
{
|
||||
public function parse(Crawler $node): PromoVoucher
|
||||
{
|
||||
$voucherNode = $node->filterXPath('//aktionsgutschein');
|
||||
|
||||
$code = trim($voucherNode->attr('code') ?? '');
|
||||
$description = trim($voucherNode->attr('text') ?? '');
|
||||
$applicability = $voucherNode->attr('pro_buchung_person') ?? '';
|
||||
$discountAmount = $this->stringToFloat($voucherNode->attr('wert_betrag') ?? '0,00');
|
||||
$discountPercentage = $this->stringToFloat($voucherNode->attr('wert_prozent') ?? '0,00');
|
||||
$minimumPrice = $this->stringToFloat($voucherNode->attr('ab_preis') ?? '0,00');
|
||||
|
||||
return new PromoVoucher(
|
||||
code: $code,
|
||||
description: $description,
|
||||
applicability: $applicability,
|
||||
discountAmount: $discountAmount,
|
||||
discountPercentage: $discountPercentage,
|
||||
minimumPrice: $minimumPrice
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\PurchaseVoucher;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
final class PurchaseVoucherParser extends AbstractParser
|
||||
{
|
||||
public function parse(Crawler $node): PurchaseVoucher
|
||||
{
|
||||
$voucherNode = $node->filterXPath('//gutschein');
|
||||
|
||||
$voucherNumber = $this->getStringOrNullValue($voucherNode->filterXPath('//gutscheinnnr')) ?? '';
|
||||
$redemptionCode = $this->getStringOrNullValue($voucherNode->filterXPath('//einloesecode')) ?? '';
|
||||
$voucherId = $this->getIntOrNullValue($voucherNode->filterXPath('//idgutschein')) ?? 0;
|
||||
$remainingBalance = $this->getFloatOrNullValue($voucherNode->filterXPath('//betrag')) ?? 0.0;
|
||||
$type = $this->getStringOrNullValue($voucherNode->filterXPath('//art')) ?? '';
|
||||
|
||||
return new PurchaseVoucher(
|
||||
voucherNumber: $voucherNumber,
|
||||
redemptionCode: $redemptionCode,
|
||||
voucherId: $voucherId,
|
||||
remainingBalance: $remainingBalance,
|
||||
type: $type
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user