Compare commits
6
Commits
b41820c39d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f08d9a9f40 | ||
|
|
7b49375efa | ||
|
|
91522ab32d | ||
|
|
a3e8c737de | ||
|
|
67c685f6c1 | ||
|
|
7aad54e6f2 |
Generated
+232
-228
File diff suppressed because it is too large
Load Diff
+13
@@ -87,6 +87,19 @@ host('prod')
|
||||
->set('rsync_src', __DIR__)
|
||||
->set('rsync', $rsyncOptions)
|
||||
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/public/ --web-url=https://my.ep-reisen.de')
|
||||
;
|
||||
|
||||
host('staging')
|
||||
->setHostname('dedi10193.your-server.de')
|
||||
->setRemoteUser('myepsf')
|
||||
->setForwardAgent(true)
|
||||
->setSshMultiplexing(true)
|
||||
->setDeployPath('/usr/home/myepsf/public_html/staging')
|
||||
->set('bin/php', '/usr/bin/php')
|
||||
->set('http_user', 'myepsf')
|
||||
->set('rsync_src', __DIR__)
|
||||
->set('rsync', $rsyncOptions)
|
||||
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/public/ --web-url=https://my.ep-reisen.net')
|
||||
->add('shared_files', [
|
||||
'public/.htpasswd',
|
||||
])
|
||||
|
||||
@@ -96,7 +96,7 @@ class BookingPayloadBuilder
|
||||
foreach ($bookingData->participants as $index => $participant) {
|
||||
$participantPayload = [
|
||||
'@id' => $index + 1,
|
||||
'status' => $bookingData->participantsStatus[$index],
|
||||
'status' => $bookingData->participantsStatus[$index] ?? null,
|
||||
...$participant->toPayload(),
|
||||
];
|
||||
|
||||
|
||||
@@ -49,11 +49,13 @@ class BookingParser extends AbstractParser
|
||||
|
||||
$booking->applicant = $this->parsePersonalData($node->filterXPath('//anmelder'));
|
||||
|
||||
$participantsStatus = $this->getArrayValue($node->filterXPath('//status_teilnehmer'), '/');
|
||||
$booking->participantsStatus = array_combine(range(0, count($participantsStatus) - 1), $participantsStatus);
|
||||
|
||||
$booking->participants = $this->parseParticipants($node->filterXPath('//teilnehmerliste/teilnehmer'));
|
||||
|
||||
$booking->participantsStatus = $this->mapParticipantsStatus(
|
||||
array_keys($booking->participants),
|
||||
$this->getArrayValue($node->filterXPath('//status_teilnehmer'), '/'),
|
||||
);
|
||||
|
||||
$paymentData = $node->filterXPath('//zahlung');
|
||||
$booking->paymentId = $this->getAttrOrNullValue($paymentData, 'idzahlungsart');
|
||||
$booking->paymentLabel = $this->getAttrOrNullValue($paymentData, 'bezeichnung');
|
||||
@@ -129,6 +131,38 @@ class BookingParser extends AbstractParser
|
||||
return $participants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the positional status_teilnehmer list onto the participant keys.
|
||||
*
|
||||
* BusPro sends the statuses as a slash-separated list in teilnehmerliste order and
|
||||
* without ids, while participants are keyed by "BusPro participant id - 1" (see
|
||||
* parseParticipants()). Zipping the two in document order keeps both arrays on the same
|
||||
* keys even when the ids are not contiguous from 1, which BookingPayloadBuilder relies
|
||||
* on when it reads participantsStatus[$index] while iterating participants, and which
|
||||
* gates editing in ParticipantController::isParticipantCanceled().
|
||||
*
|
||||
* Surplus entries on either side are dropped rather than shifting the remaining ones
|
||||
* onto the wrong participant.
|
||||
*
|
||||
* @param list<int> $participantKeys Participant array keys, in document order
|
||||
* @param list<string> $statusValues Status codes, in document order
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function mapParticipantsStatus(array $participantKeys, array $statusValues): array
|
||||
{
|
||||
$count = min(count($participantKeys), count($statusValues));
|
||||
|
||||
if (0 === $count) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_combine(
|
||||
array_slice($participantKeys, 0, $count),
|
||||
array_slice($statusValues, 0, $count),
|
||||
);
|
||||
}
|
||||
|
||||
private function parsePersonalData(Crawler $node): PersonalData
|
||||
{
|
||||
$personalData = new PersonalData();
|
||||
|
||||
@@ -18,8 +18,9 @@ use App\Form\Model\ParticipantDto;
|
||||
*
|
||||
* Service selections are gated by travel-level mutability flags (additionalServicesMutable,
|
||||
* transportationServicesMutable, pickupsMutable). Insurance is always applied regardless
|
||||
* of mutability. Merge strategy (only apply if resolves to a valid service) is used for
|
||||
* single-select fields; overwrite strategy is used for multi-select and boolean fields.
|
||||
* of mutability. Merge strategy (only apply if it resolves against the travel data) is used
|
||||
* for single-select fields, room assignment included; overwrite strategy is used for
|
||||
* multi-select, boolean and free-text fields.
|
||||
*/
|
||||
class BookingEditDraftMerger
|
||||
{
|
||||
@@ -52,7 +53,7 @@ class BookingEditDraftMerger
|
||||
|
||||
// Room assignment
|
||||
if (true === isset($data['roomAssignment']) && true === is_array($data['roomAssignment'])) {
|
||||
$this->applyRoomAssignment($participant, $data['roomAssignment']);
|
||||
$this->applyRoomAssignment($participant, $data['roomAssignment'], $travel);
|
||||
}
|
||||
|
||||
// License plate
|
||||
@@ -156,12 +157,27 @@ class BookingEditDraftMerger
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data */
|
||||
private function applyRoomAssignment(ParticipantDto $participant, array $data): void
|
||||
/**
|
||||
* Applies the drafted room assignment to the participant.
|
||||
*
|
||||
* The room id uses the same merge strategy as single-select services: it is only
|
||||
* applied when it resolves to a room of the current travel. A draft may therefore move
|
||||
* a participant to another room, but never unassign one. Edit mode renders the room as
|
||||
* static text and never submits assignedRoomId, so a null in the draft is only ever an
|
||||
* artefact of a snapshot taken before BusPro reported the assignment - replaying it
|
||||
* would drop the participant's room, both in the UI and in the outbound zuordnung.
|
||||
*
|
||||
* remarksRoom keeps overwrite semantics: it is free text the user can deliberately clear.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function applyRoomAssignment(ParticipantDto $participant, array $data, Travel $travel): void
|
||||
{
|
||||
if (true === array_key_exists('assignedRoomId', $data)) {
|
||||
$participant->assignedRoomId = $data['assignedRoomId'];
|
||||
$assignedRoomId = $data['assignedRoomId'] ?? null;
|
||||
if (null !== $assignedRoomId && null !== $travel->getRoomById((int) $assignedRoomId)) {
|
||||
$participant->assignedRoomId = (int) $assignedRoomId;
|
||||
}
|
||||
|
||||
if (true === array_key_exists('remarksRoom', $data)) {
|
||||
$participant->remarksRoom = $data['remarksRoom'];
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ class BpnXmlAnonymizer
|
||||
{
|
||||
private const SAFE_EMAIL_DOMAIN = 'example.com';
|
||||
|
||||
/** Elements holding a person id, in the order resolvePersonId() prefers them. */
|
||||
private const PERSON_ID_TAGS = ['personid', 'idperson', 'idadresseperson'];
|
||||
|
||||
private const ADDRESS_ID_TAG = 'idadresse';
|
||||
|
||||
private Generator $faker;
|
||||
|
||||
/**
|
||||
@@ -20,6 +25,24 @@ class BpnXmlAnonymizer
|
||||
|
||||
private int $fallbackIdentityCounter = 0;
|
||||
|
||||
/**
|
||||
* Maps a person id as it appears in the source to its replacement.
|
||||
*
|
||||
* Person ids show up in several places for the same person - the root <idperson>, the
|
||||
* anmelder's <idadresseperson>, each participant's own - and referential integrity only
|
||||
* survives if every occurrence of one source value maps to the same replacement.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $personIdReplacements = [];
|
||||
|
||||
/**
|
||||
* Maps an address id as it appears in the source to its replacement.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $addressIdReplacements = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->faker = Factory::create('de_DE');
|
||||
@@ -46,14 +69,24 @@ class BpnXmlAnonymizer
|
||||
throw new \RuntimeException('Unable to query person nodes.');
|
||||
}
|
||||
|
||||
// Resolving runs to completion before anything is replaced. resolvePersonId() falls
|
||||
// back to walking up the ancestors when a node carries no person id of its own, so a
|
||||
// single interleaved pass could read an id that an earlier replacement had already
|
||||
// faked and split one person into two identities.
|
||||
$resolved = [];
|
||||
|
||||
foreach ($personNodes as $index => $personNode) {
|
||||
if (false === $personNode instanceof \DOMElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$personId = $this->resolvePersonId($personNode, (int) $index);
|
||||
$identity = $this->getOrCreateIdentity($personId);
|
||||
$identity = $this->getOrCreateIdentity($this->resolvePersonId($personNode, (int) $index));
|
||||
$this->collectIdReplacements($personNode, $identity);
|
||||
|
||||
$resolved[] = [$personNode, $identity];
|
||||
}
|
||||
|
||||
foreach ($resolved as [$personNode, $identity]) {
|
||||
if ('anfrage' === $personNode->tagName) {
|
||||
$this->replaceRequestFields($personNode, $identity);
|
||||
continue;
|
||||
@@ -67,6 +100,10 @@ class BpnXmlAnonymizer
|
||||
$this->replaceMetadataFields($personNode, $identity);
|
||||
}
|
||||
|
||||
// Person and address ids are swept document-wide rather than per person node: the
|
||||
// same ids also appear outside any of them, as direct children of <ergebnis>.
|
||||
$this->replaceIdReferences($xpath);
|
||||
|
||||
$result = $document->saveXML();
|
||||
if (false === $result) {
|
||||
throw new \RuntimeException('Unable to serialize anonymized XML.');
|
||||
@@ -84,6 +121,8 @@ class BpnXmlAnonymizer
|
||||
{
|
||||
$this->identities = [];
|
||||
$this->fallbackIdentityCounter = 0;
|
||||
$this->personIdReplacements = [];
|
||||
$this->addressIdReplacements = [];
|
||||
}
|
||||
|
||||
private function resolvePersonId(\DOMElement $personNode, int $nodeIndex): string
|
||||
@@ -419,16 +458,22 @@ class BpnXmlAnonymizer
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces identifying attributes on the person node.
|
||||
*
|
||||
* teilnehmer/@id is deliberately left alone: it is not an identifier but the
|
||||
* participant's 1-based slot within the booking, and zuordnung, status_teilnehmer,
|
||||
* einzelpreis and the outbound payload all index against it. Rewriting it makes the
|
||||
* dump self-contradictory - room and service assignments then resolve to nobody - so
|
||||
* anonymized dumps stop being usable for debugging.
|
||||
*
|
||||
* kunde/@id is a real customer id and is replaced.
|
||||
*
|
||||
* @param array<string, string> $identity
|
||||
*/
|
||||
private function replaceMetadataFields(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
if (true === $personNode->hasAttribute('id')) {
|
||||
if ('kunde' === $personNode->tagName) {
|
||||
$this->replaceAttributeValue($personNode, 'id', $identity['customerId']);
|
||||
} elseif ('teilnehmer' === $personNode->tagName || 'anmelder' === $personNode->tagName) {
|
||||
$this->replaceAttributeValue($personNode, 'id', $identity['personId']);
|
||||
}
|
||||
if ('kunde' === $personNode->tagName) {
|
||||
$this->replaceAttributeValue($personNode, 'id', $identity['customerId']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,15 +521,120 @@ class BpnXmlAnonymizer
|
||||
$node->setAttribute($attributeName, $replacement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records how this person's ids must be replaced wherever they occur.
|
||||
*
|
||||
* Every id the node carries itself is claimed. When it carries none, the nearest
|
||||
* ancestor's is claimed instead - which is the same rule resolvePersonId() used to pick
|
||||
* this node's identity in the first place, so the two can never disagree about who a
|
||||
* given id belongs to. A response whose only person sits in <adressdaten> keeps its ids
|
||||
* at the <ergebnis> level, and this is what ties them together.
|
||||
*
|
||||
* @param array<string, string> $identity
|
||||
*/
|
||||
private function collectIdReplacements(\DOMElement $personNode, array $identity): void
|
||||
{
|
||||
$ownPersonIds = [];
|
||||
|
||||
foreach (self::PERSON_ID_TAGS as $tagName) {
|
||||
$value = $this->getDirectChildValue($personNode, $tagName);
|
||||
if (null !== $value) {
|
||||
$ownPersonIds[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ([] === $ownPersonIds) {
|
||||
$inherited = $this->findPersonIdInAncestors($personNode);
|
||||
if (null !== $inherited) {
|
||||
$ownPersonIds[] = $inherited;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($ownPersonIds as $value) {
|
||||
$this->personIdReplacements[$value] ??= $identity['personId'];
|
||||
}
|
||||
|
||||
$addressId = $this->getDirectChildValue($personNode, self::ADDRESS_ID_TAG)
|
||||
?? $this->findIdInAncestors($personNode, [self::ADDRESS_ID_TAG]);
|
||||
|
||||
if (null !== $addressId) {
|
||||
$this->addressIdReplacements[$addressId] ??= $identity['addressId'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces every person and address id in the document.
|
||||
*
|
||||
* Runs after all person nodes have been resolved, so the lookup keys are the untouched
|
||||
* source values. An id belonging to nobody the person-node query reached still gets a
|
||||
* replacement of its own rather than being left in place - it identifies a real person
|
||||
* either way.
|
||||
*/
|
||||
private function replaceIdReferences(\DOMXPath $xpath): void
|
||||
{
|
||||
$tagNames = [...self::PERSON_ID_TAGS, self::ADDRESS_ID_TAG];
|
||||
$nodes = $xpath->query('//'.implode(' | //', $tagNames));
|
||||
|
||||
if (false === $nodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
if (false === $node instanceof \DOMElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = trim($node->textContent);
|
||||
if ('' === $value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isAddressId = self::ADDRESS_ID_TAG === $node->tagName;
|
||||
$replacements = $isAddressId ? $this->addressIdReplacements : $this->personIdReplacements;
|
||||
|
||||
if (false === isset($replacements[$value])) {
|
||||
$identity = $this->getOrCreateIdentity(sprintf('%s:%s', $node->tagName, $value));
|
||||
$replacements[$value] = $isAddressId ? $identity['addressId'] : $identity['personId'];
|
||||
|
||||
if (true === $isAddressId) {
|
||||
$this->addressIdReplacements[$value] = $replacements[$value];
|
||||
} else {
|
||||
$this->personIdReplacements[$value] = $replacements[$value];
|
||||
}
|
||||
}
|
||||
|
||||
$this->replaceNodeValue($node, $replacements[$value]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getDirectChildValue(\DOMElement $parent, string $name): ?string
|
||||
{
|
||||
$child = $this->getDirectChild($parent, $name);
|
||||
if (null === $child) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim($child->textContent);
|
||||
|
||||
return '' === $value ? null : $value;
|
||||
}
|
||||
|
||||
private function findPersonIdInAncestors(\DOMElement $node): ?string
|
||||
{
|
||||
foreach (['personid', 'idperson', 'idadresseperson'] as $tagName) {
|
||||
$child = $this->getDirectChild($node, $tagName);
|
||||
if (null !== $child) {
|
||||
$value = trim($child->textContent);
|
||||
if ('' !== $value) {
|
||||
return $value;
|
||||
}
|
||||
return $this->findIdInAncestors($node, self::PERSON_ID_TAGS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the nearest id of the given kind on the node or one of its ancestors.
|
||||
*
|
||||
* @param list<string> $tagNames
|
||||
*/
|
||||
private function findIdInAncestors(\DOMElement $node, array $tagNames): ?string
|
||||
{
|
||||
foreach ($tagNames as $tagName) {
|
||||
$value = $this->getDirectChildValue($node, $tagName);
|
||||
if (null !== $value) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +643,7 @@ class BpnXmlAnonymizer
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findPersonIdInAncestors($parent);
|
||||
return $this->findIdInAncestors($parent, $tagNames);
|
||||
}
|
||||
|
||||
private function getDirectChild(\DOMElement $parent, string $name): ?\DOMElement
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\XmlCrawlerFactory;
|
||||
use App\BusProNet\XmlParser\BookingParser;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests that participantsStatus stays aligned with the participants array.
|
||||
*
|
||||
* Participants are keyed by "BusPro participant id - 1" while status_teilnehmer is an
|
||||
* id-less, slash-separated list in document order. BookingPayloadBuilder reads
|
||||
* participantsStatus[$index] while iterating participants, and
|
||||
* ParticipantController::isParticipantCanceled() gates editing on it, so the two arrays
|
||||
* must share their keys even when BusPro's ids are not contiguous from 1.
|
||||
*/
|
||||
class BookingParserParticipantsStatusTest extends TestCase
|
||||
{
|
||||
private BookingParser $parser;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->parser = new BookingParser();
|
||||
}
|
||||
|
||||
public function testStatusesAreKeyedLikeParticipantsForContiguousIds(): void
|
||||
{
|
||||
$booking = $this->parse('F/S/F', [1, 2, 3]);
|
||||
|
||||
$this->assertSame([0, 1, 2], array_keys($booking->participants));
|
||||
$this->assertSame([0 => 'F', 1 => 'S', 2 => 'F'], $booking->participantsStatus);
|
||||
}
|
||||
|
||||
public function testStatusesFollowParticipantIdsWhenIdsAreNotContiguous(): void
|
||||
{
|
||||
$booking = $this->parse('F/S/F', [1, 4, 7]);
|
||||
|
||||
$this->assertSame([0, 3, 6], array_keys($booking->participants));
|
||||
$this->assertSame([0 => 'F', 3 => 'S', 6 => 'F'], $booking->participantsStatus);
|
||||
|
||||
// Every participant the payload builder iterates must find its own status.
|
||||
foreach ($booking->participants as $index => $participant) {
|
||||
$this->assertArrayHasKey($index, $booking->participantsStatus);
|
||||
}
|
||||
}
|
||||
|
||||
public function testSurplusStatusEntriesAreDroppedInsteadOfShifting(): void
|
||||
{
|
||||
$booking = $this->parse('F/S/F/F', [1, 2, 3]);
|
||||
|
||||
$this->assertSame([0 => 'F', 1 => 'S', 2 => 'F'], $booking->participantsStatus);
|
||||
}
|
||||
|
||||
public function testMissingStatusEntriesLeaveTheRemainingParticipantsAligned(): void
|
||||
{
|
||||
$booking = $this->parse('F/S', [1, 2, 3]);
|
||||
|
||||
$this->assertSame([0 => 'F', 1 => 'S'], $booking->participantsStatus);
|
||||
$this->assertArrayNotHasKey(2, $booking->participantsStatus);
|
||||
}
|
||||
|
||||
public function testStatusListSurroundedByWhitespaceIsParsed(): void
|
||||
{
|
||||
// BusPro pretty-prints the element, so the text node carries newlines and indentation.
|
||||
$booking = $this->parse("\n F/S/F\n ", [1, 2, 3]);
|
||||
|
||||
$this->assertSame([0 => 'F', 1 => 'S', 2 => 'F'], $booking->participantsStatus);
|
||||
}
|
||||
|
||||
public function testEmptyStatusListDoesNotFail(): void
|
||||
{
|
||||
$booking = $this->parse('', [1, 2]);
|
||||
|
||||
$this->assertSame([], $booking->participantsStatus);
|
||||
$this->assertCount(2, $booking->participants);
|
||||
}
|
||||
|
||||
/** @param list<int> $participantIds */
|
||||
private function parse(string $statusList, array $participantIds): \App\BusProNet\Model\Booking
|
||||
{
|
||||
$participants = '';
|
||||
foreach ($participantIds as $id) {
|
||||
$participants .= sprintf(
|
||||
'<teilnehmer id="%d"><name>Teilnehmer %1$d</name><vorname>Test</vorname></teilnehmer>',
|
||||
$id,
|
||||
);
|
||||
}
|
||||
|
||||
$xml = sprintf(
|
||||
'<?xml version="1.0" encoding="utf-8"?>
|
||||
<ergebnis>
|
||||
<satz typ="KUNDENKONTO" />
|
||||
<art>Vorgang_Details</art>
|
||||
<idbuchung>98787</idbuchung>
|
||||
<status_teilnehmer>%s</status_teilnehmer>
|
||||
<zahlung idzahlungsart="1" bezeichnung="Überweisung" art="U" />
|
||||
<teilnehmerliste>%s</teilnehmerliste>
|
||||
</ergebnis>',
|
||||
$statusList,
|
||||
$participants,
|
||||
);
|
||||
|
||||
return $this->parser->parse(XmlCrawlerFactory::create($xml)->filterXPath('//ergebnis'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Service\BookingEditDraftMerger;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests that draft restoration never unassigns a room.
|
||||
*
|
||||
* Edit mode renders the room as static text and never submits assignedRoomId, so a null in
|
||||
* the draft payload is always an artefact of a snapshot taken before BusPro reported the
|
||||
* assignment - typically for seats added to a group booking shortly before the draft was
|
||||
* saved. Replaying such a null would strip the room from the UI and from the outbound
|
||||
* zuordnung, so the room id uses the same merge strategy as single-select services.
|
||||
*/
|
||||
class BookingEditDraftMergerRoomAssignmentTest extends TestCase
|
||||
{
|
||||
private BookingEditDraftMerger $merger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->merger = new BookingEditDraftMerger();
|
||||
}
|
||||
|
||||
public function testDraftDoesNotUnassignRoomWhenDraftValueIsNull(): void
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->mutable = true;
|
||||
$participant->assignedRoomId = 74;
|
||||
|
||||
$travel = $this->createTravel([74 => $this->createRoom(74, 'Bett im Mehrbettzimmer')]);
|
||||
|
||||
$this->apply($travel, $participant, ['assignedRoomId' => null]);
|
||||
|
||||
$this->assertSame(74, $participant->assignedRoomId);
|
||||
}
|
||||
|
||||
public function testDraftAppliesRoomThatResolvesAgainstTravel(): void
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->mutable = true;
|
||||
$participant->assignedRoomId = 74;
|
||||
|
||||
$travel = $this->createTravel([
|
||||
74 => $this->createRoom(74, 'Bett im Mehrbettzimmer'),
|
||||
75 => $this->createRoom(75, 'Doppelzimmer'),
|
||||
]);
|
||||
|
||||
$this->apply($travel, $participant, ['assignedRoomId' => 75]);
|
||||
|
||||
$this->assertSame(75, $participant->assignedRoomId);
|
||||
}
|
||||
|
||||
public function testDraftSkipsRoomThatIsUnknownToTravel(): void
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->mutable = true;
|
||||
$participant->assignedRoomId = 74;
|
||||
|
||||
$travel = $this->createTravel([74 => $this->createRoom(74, 'Bett im Mehrbettzimmer')]);
|
||||
|
||||
$this->apply($travel, $participant, ['assignedRoomId' => 999]);
|
||||
|
||||
$this->assertSame(74, $participant->assignedRoomId);
|
||||
}
|
||||
|
||||
public function testDraftClearsRoomRemarks(): void
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->mutable = true;
|
||||
$participant->assignedRoomId = 74;
|
||||
$participant->remarksRoom = 'Bitte mit Lisa';
|
||||
|
||||
$travel = $this->createTravel([74 => $this->createRoom(74, 'Bett im Mehrbettzimmer')]);
|
||||
|
||||
$this->apply($travel, $participant, ['assignedRoomId' => null, 'remarksRoom' => null]);
|
||||
|
||||
$this->assertNull($participant->remarksRoom);
|
||||
$this->assertSame(74, $participant->assignedRoomId);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $roomAssignment */
|
||||
private function apply(Travel $travel, ParticipantDto $participant, array $roomAssignment): void
|
||||
{
|
||||
$dto = new BookingDto($travel, 1);
|
||||
$dto->participants = [1 => $participant];
|
||||
|
||||
$this->merger->apply($dto, 1, $participant, ['roomAssignment' => $roomAssignment], $travel);
|
||||
}
|
||||
|
||||
/** @param array<int, Room> $rooms */
|
||||
private function createTravel(array $rooms): Travel
|
||||
{
|
||||
$travel = new Travel();
|
||||
$travel->rooms = $rooms;
|
||||
|
||||
return $travel;
|
||||
}
|
||||
|
||||
private function createRoom(int $id, string $label): Room
|
||||
{
|
||||
$room = new Room();
|
||||
$room->id = $id;
|
||||
$room->label = $label;
|
||||
|
||||
return $room;
|
||||
}
|
||||
}
|
||||
@@ -205,11 +205,127 @@ XML;
|
||||
$this->assertSame('170', $responseHeight);
|
||||
$this->assertSame('70', $responseWeight);
|
||||
$this->assertSame('42', $responseShoeSize);
|
||||
$this->assertSame('123456', $responseRootPersonId);
|
||||
$this->assertSame('654321', $responseRootAddressId);
|
||||
// The root ids identify the same person as the <adressdaten> block, so they must be
|
||||
// replaced too - and with that person's replacements, not fresh ones.
|
||||
$this->assertSame('700001', $responseRootPersonId);
|
||||
$this->assertSame('800001', $responseRootAddressId);
|
||||
$this->assertSame(1, $anonymizer->getLastIdentityCount());
|
||||
}
|
||||
|
||||
public function testAnonymizePreservesParticipantSlotIds(): void
|
||||
{
|
||||
$xml = <<<'XML'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ergebnis>
|
||||
<satz typ="KUNDENKONTO" />
|
||||
<art>Vorgang_Details</art>
|
||||
<status_teilnehmer>F/F/S</status_teilnehmer>
|
||||
<teilnehmerliste>
|
||||
<teilnehmer id="1">
|
||||
<name>Alpha</name>
|
||||
<vorname>Ada</vorname>
|
||||
<idadresseperson>111111</idadresseperson>
|
||||
</teilnehmer>
|
||||
<teilnehmer id="2">
|
||||
<name>Beta</name>
|
||||
<vorname>Ben</vorname>
|
||||
<idadresseperson>222222</idadresseperson>
|
||||
</teilnehmer>
|
||||
<teilnehmer id="3">
|
||||
<name>Gamma</name>
|
||||
<vorname>Cem</vorname>
|
||||
<idadresseperson>333333</idadresseperson>
|
||||
</teilnehmer>
|
||||
</teilnehmerliste>
|
||||
<ferienzielunterbringungen>
|
||||
<ferienzielunterbringung idzimmer="74" anzahl="3" zuordnung="1,2,3" einzelpreis="439,00/439,00/439,00" />
|
||||
</ferienzielunterbringungen>
|
||||
</ergebnis>
|
||||
XML;
|
||||
|
||||
$anonymizer = new BpnXmlAnonymizer();
|
||||
$result = $anonymizer->anonymize($xml);
|
||||
|
||||
$document = new \DOMDocument();
|
||||
$this->assertTrue($document->loadXML($result));
|
||||
|
||||
$xpath = new \DOMXPath($document);
|
||||
|
||||
// Names must be replaced - this is still an anonymizer.
|
||||
$this->assertNotSame('Alpha', trim((string) $xpath->evaluate('string(//teilnehmer[1]/name)')));
|
||||
|
||||
// Slot ids must survive: zuordnung, status_teilnehmer and einzelpreis all index
|
||||
// against them, so rewriting them would make the dump self-contradictory.
|
||||
$this->assertSame('1', trim((string) $xpath->evaluate('string(//teilnehmer[1]/@id)')));
|
||||
$this->assertSame('2', trim((string) $xpath->evaluate('string(//teilnehmer[2]/@id)')));
|
||||
$this->assertSame('3', trim((string) $xpath->evaluate('string(//teilnehmer[3]/@id)')));
|
||||
|
||||
$this->assertSame('1,2,3', trim((string) $xpath->evaluate('string(//ferienzielunterbringung/@zuordnung)')));
|
||||
$this->assertSame('F/F/S', trim((string) $xpath->evaluate('string(//status_teilnehmer)')));
|
||||
}
|
||||
|
||||
public function testAnonymizeReplacesPersonAndAddressIdsConsistently(): void
|
||||
{
|
||||
$xml = <<<'XML'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ergebnis>
|
||||
<satz typ="KUNDENKONTO" />
|
||||
<art>Vorgang_Details</art>
|
||||
<idadresse>162648</idadresse>
|
||||
<idperson>275987</idperson>
|
||||
<anmelder>
|
||||
<name>Alpha</name>
|
||||
<vorname>Ada</vorname>
|
||||
<idadresse>162648</idadresse>
|
||||
<idadresseperson>275987</idadresseperson>
|
||||
</anmelder>
|
||||
<teilnehmerliste>
|
||||
<teilnehmer id="1">
|
||||
<name>Alpha</name>
|
||||
<vorname>Ada</vorname>
|
||||
<idadresseperson>275987</idadresseperson>
|
||||
</teilnehmer>
|
||||
<teilnehmer id="2">
|
||||
<name>Beta</name>
|
||||
<vorname>Ben</vorname>
|
||||
<idadresseperson>413732</idadresseperson>
|
||||
</teilnehmer>
|
||||
</teilnehmerliste>
|
||||
</ergebnis>
|
||||
XML;
|
||||
|
||||
$anonymizer = new BpnXmlAnonymizer();
|
||||
$result = $anonymizer->anonymize($xml);
|
||||
|
||||
$this->assertStringNotContainsString('275987', $result);
|
||||
$this->assertStringNotContainsString('413732', $result);
|
||||
$this->assertStringNotContainsString('162648', $result);
|
||||
|
||||
$document = new \DOMDocument();
|
||||
$this->assertTrue($document->loadXML($result));
|
||||
|
||||
$xpath = new \DOMXPath($document);
|
||||
|
||||
$rootPersonId = trim((string) $xpath->evaluate('string(/ergebnis/idperson)'));
|
||||
$rootAddressId = trim((string) $xpath->evaluate('string(/ergebnis/idadresse)'));
|
||||
$applicantPersonId = trim((string) $xpath->evaluate('string(//anmelder/idadresseperson)'));
|
||||
$applicantAddressId = trim((string) $xpath->evaluate('string(//anmelder/idadresse)'));
|
||||
$firstPersonId = trim((string) $xpath->evaluate('string(//teilnehmer[1]/idadresseperson)'));
|
||||
$secondPersonId = trim((string) $xpath->evaluate('string(//teilnehmer[2]/idadresseperson)'));
|
||||
|
||||
// One source id maps to one replacement wherever it occurs, so the applicant stays
|
||||
// recognisable as participant 1 and as the owner of the root ids.
|
||||
$this->assertSame($applicantPersonId, $rootPersonId);
|
||||
$this->assertSame($applicantPersonId, $firstPersonId);
|
||||
$this->assertSame($applicantAddressId, $rootAddressId);
|
||||
|
||||
// Distinct people keep distinct ids.
|
||||
$this->assertNotSame($applicantPersonId, $secondPersonId);
|
||||
|
||||
// The applicant and participant 1 are one person, so only two identities exist.
|
||||
$this->assertSame(2, $anonymizer->getLastIdentityCount());
|
||||
}
|
||||
|
||||
public function testAnonymizeReplacesKundeAttributes(): void
|
||||
{
|
||||
$xml = <<<'XML'
|
||||
@@ -526,8 +642,10 @@ XML;
|
||||
$this->assertSame('170', $height);
|
||||
$this->assertSame('70', $weight);
|
||||
$this->assertSame('42', $shoeSize);
|
||||
$this->assertSame('123456', $personId);
|
||||
$this->assertSame('654321', $addressIdRoot);
|
||||
// Same here: <adressdaten> carries no ids of its own, so the ones at the <ergebnis>
|
||||
// level are its own and must map onto this person's replacements.
|
||||
$this->assertSame('700001', $personId);
|
||||
$this->assertSame('800001', $addressIdRoot);
|
||||
$this->assertSame(1, $anonymizer->getLastIdentityCount());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user