feat: re-implement contact form submissions api endpoint for website

This commit is contained in:
Björn Fromme
2026-03-16 12:00:55 +01:00
parent ab18e5f134
commit b567b6a840
11 changed files with 182 additions and 10 deletions
+16
View File
@@ -68,3 +68,19 @@ GET {{base_url}}/api/products
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API contact form
POST {{base_url}}/api/contactform
Accept: application/json
Content-Type: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
{
"lastName": "Duschen",
"firstName": "Isolde",
"gender": "f",
"street": "Fooroad 1",
"zipCode": "12345",
"city": "Bartown",
"email": "[email protected]",
"phone": "12345"
}
+6 -6
View File
@@ -7,8 +7,8 @@
"Grant Type": "Authorization Code",
"Client ID": "{{oauth2_client_id}}",
"Client Secret": "{{oauth2_client_secret}}",
"Auth URL": "https://myep-next.ddev.site/authorize",
"Token URL": "https://myep-next.ddev.site/token",
"Auth URL": "https://myep-next-booking.ddev.site/authorize",
"Token URL": "https://myep-next-booking.ddev.site/token",
"Redirect URL": "https://myep-team.ddev.site/auth/check",
"Scope": "email"
},
@@ -17,8 +17,8 @@
"Grant Type": "Authorization Code",
"Client ID": "{{oauth2_client_id}}",
"Client Secret": "{{oauth2_client_secret}}",
"Auth URL": "https://myep-next.ddev.site/authorize",
"Token URL": "https://myep-next.ddev.site/token",
"Auth URL": "https://myep-next-booking.ddev.site/authorize",
"Token URL": "https://myep-next-booking.ddev.site/token",
"Redirect URL": "https://myep-team.ddev.site/auth/check",
"Scope": "email profile"
},
@@ -27,8 +27,8 @@
"Grant Type": "Client Credentials",
"Client ID": "{{oauth2_api_client_id}}",
"Client Secret": "{{oauth2_api_client_secret}}",
"Auth URL": "https://myep-next.ddev.site/authorize",
"Token URL": "https://myep-next.ddev.site/token",
"Auth URL": "https://myep-next-booking.ddev.site/authorize",
"Token URL": "https://myep-next-booking.ddev.site/token",
"Scope": "api"
}
}
+20
View File
@@ -9,6 +9,7 @@ use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\BookingResponse;
use App\BusProNet\Model\BookingUpdate;
use App\BusProNet\Model\ContactFormResponse;
use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
@@ -137,6 +138,25 @@ class ApiClient
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data, [], $debug);
}
/**
* @throws ApiClientException
*/
public function createAddress(
PersonalData $personalData,
bool $debug = false,
): ContactFormResponse|Notification {
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
'art' => 'Adresse_Neu',
'adressdaten' => $personalData->toPayload(),
'ohnemailversand' => 'True',
];
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data, [], $debug);
}
/**
* @throws ApiClientException
*/
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
class ContactFormResponse
{
public function __construct(
public readonly int $addressId,
public readonly int $personId,
public readonly bool $isNewRecord,
) {
}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
class ContactFormSubmission
{
public ?string $lastName = null;
public ?string $firstName = null;
public ?string $gender = null;
public ?string $street = null;
public ?string $zipCode = null;
public ?string $city = null;
public ?string $email = null;
public ?string $phone = null;
}
+19
View File
@@ -94,6 +94,25 @@ class PersonalData
];
}
public static function fromContactFormSubmission(ContactFormSubmission $dto): self
{
$gender = null !== $dto->gender ? strtoupper($dto->gender) : null;
if ('F' === $gender) {
$gender = 'W';
}
$instance = new self();
$instance->name = $dto->lastName;
$instance->firstName = $dto->firstName;
$instance->gender = $gender;
$instance->address->street = $dto->street;
$instance->address->postCode = $dto->zipCode;
$instance->address->city = $dto->city;
$instance->communication->email = $dto->email;
$instance->communication->phone = $dto->phone;
return $instance;
}
/**
* Extracts user claims for authentication and profile information.
*
+3
View File
@@ -48,6 +48,9 @@ class HotelLoader extends AbstractLoader
return null;
}
/**
* @throws HotelNotFoundException
*/
public function loadById(int $id, ?string $filename = 'hotel.xml'): Hotel
{
$hotels = $this->loadAll($filename);
@@ -36,6 +36,8 @@ class ApiResponseParser extends AbstractParser
case 'Adressdaten_Ändern':
case 'Newsletter':
return (new PersonalDataParser())->parse($resultNode);
case 'Adresse_Neu':
return (new ContactFormResponseParser())->parse($resultNode);
case 'SelektionCRM':
case 'SelektionCRM_Ändern':
return (new CrmAttributesResponseParser())->parse($resultNode);
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\XmlParser;
use App\BusProNet\Model\ContactFormResponse;
use Symfony\Component\DomCrawler\Crawler;
class ContactFormResponseParser extends AbstractParser
{
public function parse(Crawler $node): ContactFormResponse
{
$addressId = (int) $node->filterXPath('//idadresse')->text();
$personId = (int) $node->filterXPath('//idperson')->text();
$isNewRecord = $this->getBoolValue($node->filterXPath('//neuanlage'));
return new ContactFormResponse(
addressId: $addressId,
personId: $personId,
isNewRecord: $isNewRecord,
);
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\ContactFormSubmission;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/api')]
#[IsGranted('ROLE_OAUTH2_API')]
class ContactFormController extends AbstractController
{
public function __construct(private readonly ApiClient $apiClient)
{
}
#[Route('/contactform', name: 'api_contacform', methods: ['POST'])]
public function index(#[MapRequestPayload] ContactFormSubmission $dto): JsonResponse
{
$personalData = PersonalData::fromContactFormSubmission($dto);
try {
$result = $this->apiClient->createAddress($personalData);
if ($result instanceof Notification) {
return new JsonResponse([
'success' => false,
'message' => $result->message,
], Response::HTTP_CONFLICT);
}
return $this->json([
'success' => true,
'addressId' => $result->addressId,
'personId' => $result->personId,
'isNewRecord' => $result->isNewRecord,
], Response::HTTP_CREATED);
} catch (ApiClientException $e) {
return new JsonResponse([
'success' => false,
'message' => $e->getMessage(),
], Response::HTTP_BAD_REQUEST);
}
}
}
+4 -3
View File
@@ -3,6 +3,7 @@
namespace App\Controller\Api;
use App\BusProNet\XmlLoader\HotelLoader;
use App\Exception\HotelNotFoundException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
@@ -28,12 +29,12 @@ class HotelController extends AbstractController
#[Route('/hotels/{id}', name: 'api_hotels_single')]
public function single(int $id): JsonResponse
{
try {
$hotel = $this->xmlLoader->loadById($id);
if (null === $hotel) {
return $this->json($hotel, Response::HTTP_OK, [], ['groups' => 'api:single']);
} catch (HotelNotFoundException $e) {
return new JsonResponse(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
}
return $this->json($hotel, Response::HTTP_OK, [], ['groups' => 'api:single']);
}
}