feat: voucher purchase flow
This commit is contained in:
1 parent
a628e96d06
commit
f3dd122678
70 files changed
+3672
-122
No files matched your search
@@ -3,6 +3,7 @@
|
||||
namespace App\BusProNet;
|
||||
|
||||
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
||||
use App\BusProNet\DataProcessor\VoucherSalePayloadBuilder;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
||||
use App\BusProNet\Exception\ResponseParserException;
|
||||
@@ -22,9 +23,14 @@ use App\BusProNet\Model\PurchaseVoucher;
|
||||
use App\BusProNet\Model\RegistrationResponse;
|
||||
use App\BusProNet\Model\ServiceAvailabilityResponse;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\Model\VoucherPaymentMethod;
|
||||
use App\BusProNet\Model\VoucherPreview;
|
||||
use App\BusProNet\Model\VoucherSaleResponse;
|
||||
use App\BusProNet\Model\VoucherTemplates;
|
||||
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\RegistrationDto;
|
||||
use App\Form\Model\VoucherPurchaseDto;
|
||||
use App\Service\RequestIdGenerator;
|
||||
use League\Flysystem\FilesystemException;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
@@ -50,6 +56,11 @@ class ApiClient
|
||||
public const TYPE_AGENCIES = 'AGENTUREN';
|
||||
public const TYPE_PURCHASE_VOUCHER = 'GUTSCHEINPRUEFUNGEINLOESUNG';
|
||||
public const TYPE_PROMO_VOUCHER = 'AKTIONSGUTSCHEIN';
|
||||
// Selling Kaufgutscheine. Not to be confused with TYPE_PURCHASE_VOUCHER, which redeems one in a booking.
|
||||
public const TYPE_VOUCHER_TEMPLATES = 'GUTSCHEINVORLAGEN';
|
||||
public const TYPE_VOUCHER_PAYMENT_METHODS = 'GUTSCHEINZAHLUNGSARTEN';
|
||||
public const TYPE_VOUCHER_PREVIEW = 'GUTSCHEINVORSCHAU';
|
||||
public const TYPE_VOUCHER_SALE = 'GUTSCHEINBUCHUNG';
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
private array $config;
|
||||
@@ -64,6 +75,7 @@ class ApiClient
|
||||
private readonly FilesystemOperator $xmlDump,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly BookingDataProcessor $bookingDataProcessor,
|
||||
private readonly VoucherSalePayloadBuilder $voucherSalePayloadBuilder,
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly RequestIdGenerator $requestIdGenerator,
|
||||
array $options,
|
||||
@@ -532,6 +544,97 @@ class ApiClient
|
||||
return $this->sendRequest(static::TYPE_PROMO_VOUCHER, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the voucher designs on sale and the amount bounds.
|
||||
*
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getVoucherTemplates(): Notification|VoucherTemplates
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_VOUCHER_TEMPLATES),
|
||||
'satz' => ['@typ' => static::TYPE_VOUCHER_TEMPLATES],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_VOUCHER_TEMPLATES, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the payment methods offered for voucher purchases.
|
||||
*
|
||||
* @return list<VoucherPaymentMethod>|Notification
|
||||
*
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getVoucherPaymentMethods(): array|Notification
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_VOUCHER_PAYMENT_METHODS),
|
||||
'satz' => ['@typ' => static::TYPE_VOUCHER_PAYMENT_METHODS],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_VOUCHER_PAYMENT_METHODS, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the PDF preview of a voucher design with the entered data.
|
||||
*
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getVoucherPreview(VoucherPurchaseDto $voucherPurchaseDto): Notification|VoucherPreview
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_VOUCHER_PREVIEW),
|
||||
'satz' => ['@typ' => static::TYPE_VOUCHER_PREVIEW],
|
||||
...$this->voucherSalePayloadBuilder->buildPreviewPayload($voucherPurchaseDto),
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_VOUCHER_PREVIEW, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dry run of a voucher purchase: full validation, nothing is issued.
|
||||
*
|
||||
* Every HINWEIS is a rejection on this path, including 650 (missing address data), so the
|
||||
* result must not be judged by Notification::isError().
|
||||
*
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function createVoucherSaleInquiry(VoucherPurchaseDto $voucherPurchaseDto): Notification|VoucherSaleResponse
|
||||
{
|
||||
return $this->sendVoucherSale($voucherPurchaseDto, Constants::BOOKING_TYPE_INQUIRY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buys the voucher. BusPro mails it to the buyer once the payment has arrived.
|
||||
*
|
||||
* Every HINWEIS is a rejection on this path, including 650.
|
||||
*
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function createVoucherSale(VoucherPurchaseDto $voucherPurchaseDto): Notification|VoucherSaleResponse
|
||||
{
|
||||
return $this->sendVoucherSale($voucherPurchaseDto, Constants::BOOKING_TYPE_BOOKING);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
private function sendVoucherSale(VoucherPurchaseDto $voucherPurchaseDto, string $saleType): Notification|VoucherSaleResponse
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_VOUCHER_SALE),
|
||||
'satz' => ['@typ' => static::TYPE_VOUCHER_SALE],
|
||||
...$this->voucherSalePayloadBuilder->buildSalePayload($voucherPurchaseDto, $saleType),
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_VOUCHER_SALE, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends raw XML to the BPN API with key regeneration and automatic retry.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\DataProcessor;
|
||||
|
||||
use App\Form\Model\VoucherPurchaseDto;
|
||||
|
||||
/**
|
||||
* Builds the GUTSCHEINBUCHUNG and GUTSCHEINVORSCHAU payloads from the voucher purchase DTO.
|
||||
*
|
||||
* The buyer goes into <rechnungsempfänger> and nothing else: linking the purchase to a customer
|
||||
* record is BusPro's business. The <zahlung> attributes are echoed from GUTSCHEINZAHLUNGSARTEN.
|
||||
*/
|
||||
class VoucherSalePayloadBuilder
|
||||
{
|
||||
/**
|
||||
* @param string $saleType Constants::BOOKING_TYPE_INQUIRY for the dry run, Constants::BOOKING_TYPE_BOOKING for the purchase
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildSalePayload(VoucherPurchaseDto $dto, string $saleType): array
|
||||
{
|
||||
if (null === $dto->template || null === $dto->paymentMethod) {
|
||||
throw new \LogicException('A voucher sale needs a template and a payment method.');
|
||||
}
|
||||
|
||||
$communication = [
|
||||
'telefonprivat' => $dto->phone,
|
||||
'email' => $dto->email,
|
||||
];
|
||||
// Not in the vendor's GUTSCHEINBUCHUNG example; the element name is the one BUCHUNG uses.
|
||||
if (null !== $dto->mobile && '' !== trim($dto->mobile)) {
|
||||
$communication['telefonmobil'] = $dto->mobile;
|
||||
}
|
||||
|
||||
return [
|
||||
'buchungsart' => $saleType,
|
||||
'gutscheinart' => $dto->template->type,
|
||||
'idgutscheinvorlage' => $dto->template->id,
|
||||
'kaeufer' => $dto->from,
|
||||
'empfaenger' => $dto->to,
|
||||
'kaufpreis' => $dto->amount,
|
||||
'text' => '',
|
||||
'rechnungsempfänger' => [
|
||||
'name' => $dto->lastName,
|
||||
'vorname' => $dto->firstName,
|
||||
'anrede' => $dto->salutation,
|
||||
'titel' => $dto->title,
|
||||
'geschlecht' => '',
|
||||
'geburtsdatum' => '',
|
||||
'anschrift' => $dto->address->toPayload(),
|
||||
'kommunikation' => $communication,
|
||||
],
|
||||
'zahlung' => [
|
||||
'@idzahlungsart' => $dto->paymentMethod->id,
|
||||
'@bezeichnung' => $dto->paymentMethod->label,
|
||||
'@art' => $dto->paymentMethod->type,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildPreviewPayload(VoucherPurchaseDto $dto): array
|
||||
{
|
||||
if (null === $dto->template) {
|
||||
throw new \LogicException('A voucher preview needs a template.');
|
||||
}
|
||||
|
||||
return [
|
||||
'idgutscheinvorlage' => $dto->template->id,
|
||||
'kaeufer' => $dto->from,
|
||||
'empfaenger' => $dto->to,
|
||||
'kaufpreis' => $dto->amount,
|
||||
'text' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* A payment method for voucher purchases (GUTSCHEINZAHLUNGSARTEN).
|
||||
*
|
||||
* Echoed unchanged as the <zahlung> attributes of GUTSCHEINBUCHUNG.
|
||||
*/
|
||||
final readonly class VoucherPaymentMethod
|
||||
{
|
||||
public const string TYPE_TRANSFER = 'UEBERW';
|
||||
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $label,
|
||||
public string $type,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isTransfer(): bool
|
||||
{
|
||||
return self::TYPE_TRANSFER === $this->type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* The GUTSCHEINVORSCHAU response, decoded from its data URI to the raw PDF bytes.
|
||||
*/
|
||||
final readonly class VoucherPreview
|
||||
{
|
||||
public function __construct(
|
||||
public string $pdf,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* A GUTSCHEINBUCHUNG response that is not a HINWEIS.
|
||||
*
|
||||
* The dry run (buchungsart Anfrage) answers <buchung>möglich</buchung> and nothing else, so every
|
||||
* other field is only set by the actual purchase (buchungsart Buchung, <buchung>erfolgt</buchung>).
|
||||
*
|
||||
* The redemption code is deliberately not exposed to the customer before payment: BusPro mails the
|
||||
* voucher itself once the transfer has arrived.
|
||||
*/
|
||||
final readonly class VoucherSaleResponse
|
||||
{
|
||||
public const string SALE_POSSIBLE = 'möglich';
|
||||
public const string SALE_PROCESSED = 'erfolgt';
|
||||
|
||||
public function __construct(
|
||||
public string $status,
|
||||
public ?int $voucherId = null,
|
||||
public ?string $voucherNumber = null,
|
||||
public ?string $redemptionCode = null,
|
||||
public ?float $price = null,
|
||||
public ?string $invoiceNumber = null,
|
||||
public ?string $email = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isInquiryValid(): bool
|
||||
{
|
||||
return self::SALE_POSSIBLE === $this->status;
|
||||
}
|
||||
|
||||
public function isSaleSuccessful(): bool
|
||||
{
|
||||
return self::SALE_PROCESSED === $this->status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* A voucher design offered for online sale (GUTSCHEINVORLAGEN).
|
||||
*
|
||||
* The id doubles as the key for the local thumbnail, see VoucherThumbnailResolver.
|
||||
*/
|
||||
final readonly class VoucherTemplate
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $type,
|
||||
public string $label,
|
||||
public ?string $note = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* The GUTSCHEINVORLAGEN response: the designs on sale plus BusPro's amount bounds.
|
||||
*
|
||||
* The bounds (gutscheinminbetrag/gutscheinmaxbetrag) are not in the vendor documentation,
|
||||
* they were measured on the test system (10 and 5,000 €, both inclusive).
|
||||
*/
|
||||
final readonly class VoucherTemplates
|
||||
{
|
||||
/**
|
||||
* @param list<VoucherTemplate> $templates
|
||||
*/
|
||||
public function __construct(
|
||||
public int $minAmount,
|
||||
public int $maxAmount,
|
||||
public array $templates,
|
||||
) {
|
||||
}
|
||||
|
||||
public function find(int $id): ?VoucherTemplate
|
||||
{
|
||||
foreach ($this->templates as $template) {
|
||||
if ($id === $template->id) {
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,14 @@ class ApiResponseParser extends AbstractParser
|
||||
return (new PurchaseVoucherParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PROMO_VOUCHER:
|
||||
return (new PromoVoucherParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_VOUCHER_TEMPLATES:
|
||||
return (new VoucherTemplatesParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_VOUCHER_PAYMENT_METHODS:
|
||||
return (new VoucherPaymentMethodsParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_VOUCHER_PREVIEW:
|
||||
return (new VoucherPreviewParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_VOUCHER_SALE:
|
||||
return (new VoucherSaleResponseParser())->parse($resultNode);
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new ResponseParserException('Invalid XML response structure: '.$e->getMessage(), 0, $e);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\VoucherPaymentMethod;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
/**
|
||||
* Parses GUTSCHEINZAHLUNGSARTEN responses.
|
||||
*
|
||||
* Expected XML structure (test system, 2026-10-01):
|
||||
* <ergebnis>
|
||||
* <satz typ="GUTSCHEINZAHLUNGSARTEN"></satz>
|
||||
* <zahlungsarten>
|
||||
* <zahlungsart id="2" bezeichnung="Überweisung" art="UEBERW"></zahlungsart>
|
||||
* </zahlungsarten>
|
||||
* </ergebnis>
|
||||
*/
|
||||
final class VoucherPaymentMethodsParser extends AbstractParser
|
||||
{
|
||||
/**
|
||||
* @return list<VoucherPaymentMethod>
|
||||
*/
|
||||
public function parse(Crawler $node): array
|
||||
{
|
||||
$paymentMethods = [];
|
||||
$node->filterXPath('//zahlungsarten/zahlungsart')->each(function (Crawler $methodNode) use (&$paymentMethods): void {
|
||||
$paymentMethods[] = new VoucherPaymentMethod(
|
||||
id: (int) $this->getRequiredAttrValue($methodNode, 'id', 'voucher payment method'),
|
||||
label: $this->getRequiredAttrValue($methodNode, 'bezeichnung', 'voucher payment method'),
|
||||
type: $this->getRequiredAttrValue($methodNode, 'art', 'voucher payment method'),
|
||||
);
|
||||
});
|
||||
|
||||
return $paymentMethods;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\VoucherPreview;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
/**
|
||||
* Parses GUTSCHEINVORSCHAU responses.
|
||||
*
|
||||
* Expected XML structure (test system, 2026-10-01):
|
||||
* <ergebnis>
|
||||
* <satz typ="GUTSCHEINVORSCHAU"/>
|
||||
* <gutscheinpdf>data:application/pdf;base64,JVBERi0xLjQ…</gutscheinpdf>
|
||||
* </ergebnis>
|
||||
*/
|
||||
final class VoucherPreviewParser extends AbstractParser
|
||||
{
|
||||
private const string DATA_URI_PREFIX = 'data:application/pdf;base64,';
|
||||
|
||||
public function parse(Crawler $node): VoucherPreview
|
||||
{
|
||||
$dataUri = trim($this->getStringOrNullValue($node->filterXPath('//gutscheinpdf')) ?? '');
|
||||
if (!str_starts_with($dataUri, self::DATA_URI_PREFIX)) {
|
||||
throw new \InvalidArgumentException('Missing or malformed PDF data URI in voucher preview response.');
|
||||
}
|
||||
|
||||
$pdf = base64_decode(substr($dataUri, \strlen(self::DATA_URI_PREFIX)), true);
|
||||
if (false === $pdf || '' === $pdf) {
|
||||
throw new \InvalidArgumentException('Undecodable PDF in voucher preview response.');
|
||||
}
|
||||
|
||||
return new VoucherPreview($pdf);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\VoucherSaleResponse;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
/**
|
||||
* Parses GUTSCHEINBUCHUNG responses that are not a HINWEIS.
|
||||
*
|
||||
* Dry run (buchungsart Anfrage, measured on the test system 2026-10-01):
|
||||
* <ergebnis><satz typ="GUTSCHEINBUCHUNG"></satz><buchung>möglich</buchung></ergebnis>
|
||||
*
|
||||
* Purchase (buchungsart Buchung, as documented by the vendor):
|
||||
* <ergebnis>
|
||||
* <satz typ="GUTSCHEINBUCHUNG"/>
|
||||
* <buchung>erfolgt</buchung>
|
||||
* <idgutschein/><gutscheinnummer/><einloesecode/><preis/><rechnungsnummer/><email/>
|
||||
* </ergebnis>
|
||||
*/
|
||||
final class VoucherSaleResponseParser extends AbstractParser
|
||||
{
|
||||
public function parse(Crawler $node): VoucherSaleResponse
|
||||
{
|
||||
$status = trim($this->getStringOrNullValue($node->filterXPath('//buchung')) ?? '');
|
||||
if ('' === $status) {
|
||||
throw new \InvalidArgumentException('Missing required status in voucher sale response.');
|
||||
}
|
||||
|
||||
return new VoucherSaleResponse(
|
||||
status: $status,
|
||||
voucherId: $this->getIntOrNullValue($node->filterXPath('//idgutschein')),
|
||||
voucherNumber: $this->trimmedOrNull($node->filterXPath('//gutscheinnummer')),
|
||||
redemptionCode: $this->trimmedOrNull($node->filterXPath('//einloesecode')),
|
||||
price: $this->getFloatOrNullValue($node->filterXPath('//preis')),
|
||||
invoiceNumber: $this->trimmedOrNull($node->filterXPath('//rechnungsnummer')),
|
||||
email: $this->trimmedOrNull($node->filterXPath('//email')),
|
||||
);
|
||||
}
|
||||
|
||||
private function trimmedOrNull(Crawler $node): ?string
|
||||
{
|
||||
$value = trim($this->getStringOrNullValue($node) ?? '');
|
||||
|
||||
return '' !== $value ? $value : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\VoucherTemplate;
|
||||
use App\BusProNet\Model\VoucherTemplates;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
/**
|
||||
* Parses GUTSCHEINVORLAGEN responses.
|
||||
*
|
||||
* Expected XML structure (test system, 2026-10-01):
|
||||
* <ergebnis>
|
||||
* <satz typ="GUTSCHEINVORLAGEN"></satz>
|
||||
* <gutscheinminbetrag>10</gutscheinminbetrag>
|
||||
* <gutscheinmaxbetrag>5000</gutscheinmaxbetrag>
|
||||
* <gutscheine>
|
||||
* <gutschein idgutscheinvorlage="3">
|
||||
* <gutscheinart>K</gutscheinart>
|
||||
* <bezeichnung>Allgemein</bezeichnung>
|
||||
* <hinweis></hinweis>
|
||||
* </gutschein>
|
||||
* </gutscheine>
|
||||
* </ergebnis>
|
||||
*/
|
||||
final class VoucherTemplatesParser extends AbstractParser
|
||||
{
|
||||
public function parse(Crawler $node): VoucherTemplates
|
||||
{
|
||||
$minAmount = $this->getStringOrNullValue($node->filterXPath('//gutscheinminbetrag'));
|
||||
$maxAmount = $this->getStringOrNullValue($node->filterXPath('//gutscheinmaxbetrag'));
|
||||
if (null === $minAmount || '' === trim($minAmount) || null === $maxAmount || '' === trim($maxAmount)) {
|
||||
throw new \InvalidArgumentException('Missing voucher amount bounds in voucher templates response.');
|
||||
}
|
||||
|
||||
$templates = [];
|
||||
$node->filterXPath('//gutscheine/gutschein')->each(function (Crawler $templateNode) use (&$templates): void {
|
||||
$note = $this->getStringOrNullValue($templateNode->filterXPath('//hinweis'));
|
||||
|
||||
$templates[] = new VoucherTemplate(
|
||||
id: (int) $this->getRequiredAttrValue($templateNode, 'idgutscheinvorlage', 'voucher template'),
|
||||
type: trim($this->getStringOrNullValue($templateNode->filterXPath('//gutscheinart')) ?? ''),
|
||||
label: trim($this->getStringOrNullValue($templateNode->filterXPath('//bezeichnung')) ?? ''),
|
||||
note: null !== $note && '' !== trim($note) ? trim($note) : null,
|
||||
);
|
||||
});
|
||||
|
||||
return new VoucherTemplates(
|
||||
// whole euros: the flow only offers whole-euro amounts, so the bounds are kept as such
|
||||
minAmount: (int) ceil($this->stringToFloat($minAmount)),
|
||||
maxAmount: (int) floor($this->stringToFloat($maxAmount)),
|
||||
templates: $templates,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user