Skip to content
5 changes: 5 additions & 0 deletions config/checkout.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@
'active' => (bool) env('CHECKOUT_COMPUTOP_TOKEN_ACTIVE', false),
'name' => env('CHECKOUT_COMPUTOP_TOKEN_NAME', 'Computop - Token'),
],
'passolution' => [
'active' => (bool) env('CHECKOUT_PASSOLUTION_ACTIVE', false),
'base_url' => env('CHECKOUT_PASSOLUTION_BASE_URL', 'https://api.passolution.eu/api/v2'),
'token' => env('CHECKOUT_PASSOLUTION_TOKEN', ''),
],
],

'insurance' => [
Expand Down
9 changes: 9 additions & 0 deletions lang/de/page.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@
'insurance_document_product_description' => 'Produktinformation / Produktbeschreibung',
'prrl_compliance_error_title' => 'Richtlinien der Pauschalreise nicht erfüllt',
'prrl_compliance_error_message' => 'Wir sind ein Reiseveranstalter für Pauschalreisen, daher haben Sie bitte Verständnis, dass wir keine einzelnen Reisebausteine anbieten. Ihre ausgewählten Reiseleistungen entsprechen leider nicht der Richtlinien einer Pauschalreise. Bitte buchen Sie mindestens zwei Reiseleistungen (z.B. Hotel und Mietwagen, Hotel und Flug oder Mietwagen und Flug)',
'general_entry_requirements' => 'Allgemeine Einreisebestimmungen',
'travel_information_intro' => 'Bitte prüfen Sie unten die Einreise- und Gesundheitsbestimmungen, die für die gewählten Reiseziele gelten.',
'travel_information_health' => 'Gesundheit',
'travel_information_entry_requirements' => 'Einreisebestimmungen',
'travel_information_visa_requirements' => 'Visabestimmungen',
'travel_information_transit_visa_requirements' => 'Transitvisabestimmungen',
'travel_information_confirmation' => 'Ich bestätige, dass ich die oben genannten Einreise- und Gesundheitsbestimmungen gelesen habe',
'travel_information_data_not_found' => 'Daten nicht gefunden',
'travel_information_combination_title' => 'Reiseziel :destination / Nationalität :nationality',
],
'dates' => [
'select' => 'Auswählen',
Expand Down
9 changes: 9 additions & 0 deletions lang/en/page.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@
'insurance_document_product_description' => 'Product Information / Product Description',
'prrl_compliance_error_title' => 'Package tour guidelines not fulfilled',
'prrl_compliance_error_message' => 'Please be aware that we do not offer individual travel components on this deal. Your selected travel services cannot be purchased individually in this package. Please re-search to book the full package, or alternatively use the contact us feature to get in touch with our excellent team.',
'general_entry_requirements' => 'General entry requirements',
'travel_information_intro' => 'Please check below Entry and Health Regulations that apply for the chosen destinations.',
'travel_information_health' => 'Health',
'travel_information_entry_requirements' => 'Entry Requirements',
'travel_information_visa_requirements' => 'Visa Requirements',
'travel_information_transit_visa_requirements' => 'Transit Visa Requirements',
'travel_information_confirmation' => 'I confirm that I read Entry and Health Regulations above',
'travel_information_data_not_found' => 'Data not found',
'travel_information_combination_title' => 'Destination :destination / Nationality :nationality',
],

'dates' => [
Expand Down
9 changes: 9 additions & 0 deletions lang/es/page.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@
'insurance_documents' => 'Documentos del seguro',
'insurance_document_ipid' => 'Documento de información del producto de seguro',
'insurance_document_product_description' => 'Información del producto / Descripción del producto',
'general_entry_requirements' => 'Requisitos generales de entrada',
'travel_information_intro' => 'Revise a continuación las normas de entrada y salud aplicables a los destinos elegidos.',
'travel_information_health' => 'Salud',
'travel_information_entry_requirements' => 'Requisitos de entrada',
'travel_information_visa_requirements' => 'Requisitos de visado',
'travel_information_transit_visa_requirements' => 'Requisitos de visado de tránsito',
'travel_information_confirmation' => 'Confirmo que he leído las normas de entrada y salud anteriores',
'travel_information_data_not_found' => 'Datos no encontrados',
'travel_information_combination_title' => 'Destino :destination / Nacionalidad :nationality',
],
'dates' => [
'select' => 'Seleccionar',
Expand Down
9 changes: 9 additions & 0 deletions lang/fr/page.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@
'insurance_document_product_description' => 'Information produit / Description du produit',
'prrl_compliance_error_title' => 'Les règles de voyages à forfait ne sont pas remplies',
'prrl_compliance_error_message' => 'Veuillez noter que nous ne proposons pas d’éléments de voyage individuels dans le cadre de cette offre. Les services de voyage que vous avez sélectionnés ne peuvent pas être achetés individuellement dans le cadre de ce forfait. Veuillez effectuer une nouvelle recherche pour réserver le forfait complet, ou utilisez la fonction « contactez-nous » pour prendre contact avec notre excellente équipe.',
'general_entry_requirements' => 'Conditions générales d’entrée',
'travel_information_intro' => 'Veuillez vérifier ci-dessous les réglementations d’entrée et de santé applicables aux destinations choisies.',
'travel_information_health' => 'Santé',
'travel_information_entry_requirements' => 'Conditions d’entrée',
'travel_information_visa_requirements' => 'Conditions de visa',
'travel_information_transit_visa_requirements' => 'Conditions de visa de transit',
'travel_information_confirmation' => 'Je confirme avoir lu les réglementations d’entrée et de santé ci-dessus',
'travel_information_data_not_found' => 'Données introuvables',
'travel_information_combination_title' => 'Destination :destination / Nationalité :nationality',
],
'dates' => [
'select' => 'Sélectionner',
Expand Down
103 changes: 103 additions & 0 deletions src/Actions/TravelInformation/LoadTravelInformationAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace Nezasa\Checkout\Actions\TravelInformation;

use Illuminate\Support\Collection;
use Nezasa\Checkout\Dtos\TravelInformation\TravelInformationCombination;
use Nezasa\Checkout\Integrations\Passolution\Connectors\PassolutionConnector;
use Nezasa\Checkout\Integrations\Passolution\Dtos\Responses\PassolutionContentResponse;
use Nezasa\Checkout\Integrations\Passolution\Requests\GetContentRequest;
use Nezasa\Checkout\Models\Checkout;
use Throwable;

class LoadTravelInformationAction
{
/**
* @param Collection<int, string>|array<int, string> $destinationCountries
* @return Collection<int, TravelInformationCombination>
*/
public function run(Checkout $checkout, Collection|array $destinationCountries, string $language): Collection
{
$destinations = $this->destinationCodes($destinationCountries);
$nationalities = $this->nationalityCodes($checkout);

if ($destinations->isEmpty() || $nationalities->isEmpty()) {
return new Collection;
}

$content = $this->loadContent($destinations, $nationalities, $language);

return $destinations
->crossJoin($nationalities)
->map(fn (array $pair): TravelInformationCombination => new TravelInformationCombination(
destinationCountryCode: $pair[0],
nationalityCountryCode: $pair[1],
record: $content->recordForCombination($pair[0], $pair[1]),
));
}

/**
* @param Collection<int, string>|array<int, string> $destinationCountries
*/
public function confirmationHash(Checkout $checkout, Collection|array $destinationCountries): string
{
return md5(json_encode([
'destinations' => $this->destinationCodes($destinationCountries)->sort()->values()->all(),
'nationalities' => $this->nationalityCodes($checkout)->sort()->values()->all(),
], JSON_THROW_ON_ERROR));
}

/**
* @param Collection<int, string> $destinationCountryCodes
* @param Collection<int, string> $nationalityCountryCodes
*/
private function loadContent(Collection $destinationCountryCodes, Collection $nationalityCountryCodes, string $language): PassolutionContentResponse
{
try {
return PassolutionConnector::make()
->send(new GetContentRequest($destinationCountryCodes, $nationalityCountryCodes, $language))
->dto();
} catch (Throwable $throwable) {
report($throwable);

return PassolutionContentResponse::fromPayload([]);
}
}

private function normalizeCountryCode(string $country): string
{
return str($country)
->trim()
->before('-')
->upper()
->toString();
}

/**
* @param Collection<int, string>|array<int, string> $destinationCountries
* @return Collection<int, string>
*/
private function destinationCodes(Collection|array $destinationCountries): Collection
{
return collect($destinationCountries)
->filter(fn (mixed $country): bool => is_string($country) && trim($country) !== '')
->map(fn (string $country): string => $this->normalizeCountryCode($country))
->unique()
->values();
}

/**
* @return Collection<int, string>
*/
private function nationalityCodes(Checkout $checkout): Collection
{
return $checkout->getPaxInfo()
->map(fn ($pax): ?string => $pax->nationalityCountryCode ?: $pax->nationality)
->filter(fn (mixed $country): bool => is_string($country) && trim($country) !== '')
->map(fn (string $country): string => $this->normalizeCountryCode($country))
->unique()
->values();
}
}
48 changes: 48 additions & 0 deletions src/Dtos/TravelInformation/TravelInformationCombination.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Nezasa\Checkout\Dtos\TravelInformation;

use Nezasa\Checkout\Integrations\Passolution\Dtos\Responses\PassolutionRecordResponse;

class TravelInformationCombination
{
public function __construct(
public readonly string $destinationCountryCode,
public readonly string $nationalityCountryCode,
public readonly ?PassolutionRecordResponse $record = null,
) {}

public function title(): string
{
if ($this->record instanceof PassolutionRecordResponse && $this->record->title !== null) {
return $this->record->title;
}

return trans('checkout::page.trip_details.travel_information_combination_title', [
'destination' => $this->destinationCountryCode,
'nationality' => $this->nationalityCountryCode,
]);
}

public function health(): ?string
{
return $this->record?->healthContent();
}

public function entry(): ?string
{
return $this->record?->entryContent();
}

public function visa(): ?string
{
return $this->record?->visaContent();
}

public function transitVisa(): ?string
{
return $this->record?->transitVisaContent();
}
}
3 changes: 3 additions & 0 deletions src/Enums/Section.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* @method bool isInsurance()
* @method bool isActivity()
* @method bool isTermsAndConditions()
* @method bool isTravelInformation()
*/
enum Section: string
{
Expand All @@ -32,6 +33,7 @@ enum Section: string
case Insurance = 'insurance';
case Activity = 'activity';
case TermsAndConditions = 'terms-and-conditions';
case TravelInformation = 'travel-information';

/**
* Customize the labels of the enum values.
Expand All @@ -50,6 +52,7 @@ protected static function setLabels(): array
self::Insurance->value => trans('checkout::page.trip_details.Insurance'),
self::Activity->value => trans('checkout::page.trip_details.activities'),
self::TermsAndConditions->value => trans('checkout::page.trip_details.important_information'),
self::TravelInformation->value => trans('checkout::page.trip_details.general_entry_requirements'),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\Entities;

use Nezasa\Checkout\Dtos\BaseDto;

class TravelInformationResponseEntity extends BaseDto
{
/**
* Create a new instance of the TravelInformationResponseEntity.
*/
public function __construct(
public bool $confirmationEnabled = false,
public ?string $title = null,
public ?string $intro = null,
public ?string $checkboxText = null,
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Nezasa\Checkout\Dtos\BaseDto;
use Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\Entities\EuPrrlResponseEntity;
use Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\Entities\OnRequestResponseEntity;
use Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\Entities\TravelInformationResponseEntity;

class RegulatoryInformationResponse extends BaseDto
{
Expand All @@ -19,9 +20,10 @@ public function __construct(
public ?string $paymentExplainer = null,
public ?EuPrrlResponseEntity $euPrrl = null,
public ?OnRequestResponseEntity $onRequest = null,

public ?TravelInformationResponseEntity $travelInformation = null,
) {
$this->onRequest = new OnRequestResponseEntity;
$this->onRequest ??= new OnRequestResponseEntity;
$this->travelInformation ??= new TravelInformationResponseEntity;
}

/**
Expand Down
41 changes: 41 additions & 0 deletions src/Integrations/Passolution/Connectors/PassolutionConnector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Nezasa\Checkout\Integrations\Passolution\Connectors;

use Illuminate\Support\Facades\Config;
use Saloon\Http\Auth\TokenAuthenticator;
use Saloon\Http\Connector;
use Saloon\Traits\Makeable;
use Saloon\Traits\Plugins\HasTimeout;

class PassolutionConnector extends Connector
{
use HasTimeout;
use Makeable;

protected int $connectTimeout = 30;

protected int $requestTimeout = 30;

public function resolveBaseUrl(): string
{
return rtrim(Config::string('checkout.integrations.passolution.base_url'), '/');
}

/**
* @return array<string, string>
*/
protected function defaultHeaders(): array
{
return [
'Accept' => 'application/json',
];
}

protected function defaultAuth(): TokenAuthenticator
{
return new TokenAuthenticator(Config::string('checkout.integrations.passolution.token'));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Nezasa\Checkout\Integrations\Passolution\Dtos\Responses;

use Illuminate\Support\Collection;
use Nezasa\Checkout\Dtos\BaseDto;

class PassolutionContentResponse extends BaseDto
{
/**
* @param Collection<int, PassolutionRecordResponse> $records
*/
public function __construct(
public Collection $records = new Collection,
) {}

/**
* @param array<string, mixed> $payload
*/
public static function fromPayload(array $payload): self
{
$records = $payload['records'] ?? [];

return new self(
records: collect(is_array($records) ? $records : [])
->filter(fn (mixed $record): bool => is_array($record))
->map(fn (array $record): PassolutionRecordResponse => PassolutionRecordResponse::fromPayload($record))
->values()
);
}

public function recordForCombination(string $destinationCountryCode, string $nationalityCountryCode): ?PassolutionRecordResponse
{
return $this->records->first(
fn (PassolutionRecordResponse $record): bool => $record->destination === $destinationCountryCode
&& $record->nationality === $nationalityCountryCode
);
}
}
Loading
Loading