feat: read BusProNet base data from the MyE&P API

This commit is contained in:
Björn Fromme authored and Björn Fromme committed 2026-09-29 11:08:04 +02:00
1 parent 1505f9b50b
commit 2e9328fc51
37 files changed
+2302 -148

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
<?php
namespace App\Tests\MyEp\Import;
use App\BusProNet\Model\Pickup;
use App\MyEp\Import\DestinationRow;
use App\MyEp\Import\InvalidRowException;
use PHPUnit\Framework\TestCase;
class DestinationRowTest extends TestCase
{
public function testARowIsMapped(): void
{
$row = DestinationRow::fromArray($this->getRowData());
$this->assertEquals(11886, $row->busProId);
$this->assertEquals('LPJPGJ040426', $row->code);
$this->assertEquals('Lenzerheide - Sportclub Jenatsch', $row->product);
$this->assertEquals('2026-04-04', $row->dateFrom->format('Y-m-d'));
$this->assertEquals('2026-04-11', $row->dateTo->format('Y-m-d'));
$this->assertEquals(163959, $row->hotelBusProId);
$this->assertEquals('LPJGR', $row->hotelCode);
$this->assertEquals('Sportclub Jenatsch', $row->hotelName);
$this->assertEquals('4711', $row->costUnit);
$this->assertEquals('ch', $row->country);
}
public function testTheIsoCountryCodeIsStoredRatherThanTheRawToken(): void
{
$row = DestinationRow::fromArray($this->getRowData([
'hotel' => ['busProId' => 1, 'country' => 'AUT', 'countryCode' => 'AT'],
]));
$this->assertEquals('at', $row->country);
}
public function testAnUnresolvedCountryCodeIsStoredAsNull(): void
{
$row = DestinationRow::fromArray($this->getRowData([
'hotel' => ['busProId' => 1, 'country' => 'SLO'],
]));
$this->assertNull($row->country);
}
public function testTheDatesAreReadAsWholeDays(): void
{
$row = DestinationRow::fromArray($this->getRowData());
$this->assertEquals('00:00:00', $row->dateFrom->format('H:i:s'));
$this->assertEquals('00:00:00', $row->dateTo->format('H:i:s'));
}
public function testPickupsKeepTheShapeTheDestinationTableStores(): void
{
$row = DestinationRow::fromArray($this->getRowData());
$this->assertEquals([[
'busProId' => 4,
'city' => 'Karlsruhe',
'street' => 'A5 - Autohof Bruchsal',
'time' => '23:59',
]], $row->pickups);
}
public function testStoredPickupsCanBeReadBackAsModels(): void
{
$row = DestinationRow::fromArray($this->getRowData());
$pickup = Pickup::fromArray(json_decode(json_encode($row->pickups), true)[0]);
$this->assertEquals(4, $pickup->getId());
$this->assertEquals('Karlsruhe', $pickup->getCity());
$this->assertEquals('23:59', $pickup->getTime());
}
public function testPickupsWithoutAnIdAreDropped(): void
{
$row = DestinationRow::fromArray($this->getRowData(['pickups' => [
['city' => 'Karlsruhe'],
['busProId' => 0, 'city' => 'Köln'],
['busProId' => 27, 'city' => 'Bonn', 'street' => 'ZOB', 'time' => '02:15'],
]]));
$this->assertCount(1, $row->pickups);
$this->assertEquals(27, $row->pickups[0]['busProId']);
}
public function testARowWithoutPickupsIsAccepted(): void
{
$row = DestinationRow::fromArray($this->getRowData(['pickups' => []]));
$this->assertEquals([], $row->pickups);
}
public function testARowWithoutAHotelIsRejected(): void
{
$this->expectException(InvalidRowException::class);
DestinationRow::fromArray($this->getRowData(['hotel' => null]));
}
public function testARowWithoutIdsIsRejected(): void
{
$this->expectException(InvalidRowException::class);
DestinationRow::fromArray($this->getRowData(['busProId' => 0]));
}
public function testARowWithAnUnreadableDateIsRejected(): void
{
$this->expectException(InvalidRowException::class);
DestinationRow::fromArray($this->getRowData(['dateFrom' => '04.04.2026']));
}
public function testTheInsertPayloadCarriesEveryColumn(): void
{
$row = DestinationRow::fromArray($this->getRowData());
$payload = $row->toInsertPayload(new \DateTimeImmutable('2026-08-13 03:00:00'));
$this->assertEquals([
'code' => 'LPJPGJ040426',
'bus_pro_id' => 11886,
'date_from' => '2026-04-04',
'date_to' => '2026-04-11',
'product' => 'Lenzerheide - Sportclub Jenatsch',
'hotel' => 'Sportclub Jenatsch',
'hotel_code' => 'LPJGR',
'hotel_bus_pro_id' => 163959,
'pickups' => '[{"busProId":4,"city":"Karlsruhe","street":"A5 - Autohof Bruchsal","time":"23:59"}]',
'cost_unit' => '4711',
'country' => 'ch',
'created_at' => '2026-08-13 03:00:00',
], $payload);
}
public function testTheUpdatePayloadRefreshesTheHotelName(): void
{
// a renamed house has to reach existing rows, or it drops out of the house matching
$row = DestinationRow::fromArray($this->getRowData());
$payload = $row->toUpdatePayload(new \DateTimeImmutable('2026-08-13 03:00:00'));
$this->assertEquals('Sportclub Jenatsch', $payload['hotel']);
$this->assertEquals('LPJGR', $payload['hotel_code']);
$this->assertEquals('2026-08-13 03:00:00', $payload['updated_at']);
$this->assertArrayNotHasKey('created_at', $payload);
$this->assertArrayNotHasKey('bus_pro_id', $payload);
$this->assertArrayNotHasKey('hotel_bus_pro_id', $payload);
}
private function getRowData(array $overrides = []): array
{
return array_merge([
'busProId' => 11886,
'code' => 'LPJPGJ040426',
'product' => 'Lenzerheide - Sportclub Jenatsch',
'dateFrom' => '2026-04-04',
'dateTo' => '2026-04-11',
'hotel' => [
'busProId' => 163959,
'code' => 'LPJGR',
'name' => 'Sportclub Jenatsch',
'costUnit' => '4711',
'country' => 'CH',
'countryCode' => 'ch',
],
'pickups' => [[
'busProId' => 4,
'code' => '298',
'city' => 'Karlsruhe',
'street' => 'A5 - Autohof Bruchsal',
'time' => '23:59',
'datetime' => '2026-04-04T23:59:00+02:00',
]],
], $overrides);
}
}
@@ -0,0 +1,138 @@
<?php
namespace App\Tests\MyEp\Import;
use App\MyEp\Api\AccessTokenProvider;
use App\MyEp\Api\ApiClient;
use App\MyEp\Api\ApiException;
use App\MyEp\Import\TravelExportClient;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
class TravelExportClientTest extends TestCase
{
public function testEveryPageIsRead(): void
{
$httpClient = $this->mockClient([
$this->page([$this->row(1, 100), $this->row(2, 200)], 1, 2),
$this->page([$this->row(3, 300)], 2, 2),
]);
$rows = $this->getClientInstance($httpClient)->fetchAll();
$this->assertCount(3, $rows);
$this->assertEquals([1, 2, 3], array_map(fn ($row) => $row->busProId, $rows));
$this->assertEquals(2, $httpClient->getRequestsCount());
}
public function testASinglePageIsNotFollowedByAnother(): void
{
$httpClient = $this->mockClient([$this->page([$this->row(1, 100)], 1, 1)]);
$this->getClientInstance($httpClient)->fetchAll();
$this->assertEquals(1, $httpClient->getRequestsCount());
}
public function testTheRequestedPageIsPassedOn(): void
{
$urls = [];
$httpClient = new MockHttpClient(function (string $method, string $url) use (&$urls) {
$urls[] = $url;
return $this->page([$this->row(count($urls), 100)], count($urls), 2);
}, 'https://myep.example/');
$this->getClientInstance($httpClient)->fetchAll();
$this->assertStringContainsString('page=1', $urls[0]);
$this->assertStringContainsString('page=2', $urls[1]);
$this->assertStringContainsString('perPage=500', $urls[0]);
}
public function testAnUnusableRowIsSkippedRatherThanFatal(): void
{
$httpClient = $this->mockClient([
$this->page([
['busProId' => 1],
$this->row(2, 200),
], 1, 1),
]);
$rows = $this->getClientInstance($httpClient)->fetchAll();
$this->assertCount(1, $rows);
$this->assertEquals(2, $rows[0]->busProId);
}
public function testAnEmptyExportYieldsNoRows(): void
{
$httpClient = $this->mockClient([$this->page([], 1, 1)]);
$this->assertEquals([], $this->getClientInstance($httpClient)->fetchAll());
}
public function testAPayloadWithoutADataListFails(): void
{
$httpClient = $this->mockClient([new MockResponse('{"meta":{"totalPages":1}}')]);
$this->expectException(ApiException::class);
$this->getClientInstance($httpClient)->fetchAll();
}
private function row(int $busProId, int $hotelBusProId): array
{
return [
'busProId' => $busProId,
'code' => 'CODE'.$busProId,
'product' => 'Produkt '.$busProId,
'dateFrom' => '2026-04-04',
'dateTo' => '2026-04-11',
'hotel' => [
'busProId' => $hotelBusProId,
'code' => 'AAA',
'name' => 'Haus',
'costUnit' => null,
'country' => 'ch',
],
'pickups' => [],
];
}
private function page(array $rows, int $page, int $totalPages): MockResponse
{
return new MockResponse(json_encode([
'data' => $rows,
'meta' => [
'page' => $page,
'perPage' => 500,
'total' => count($rows),
'totalPages' => $totalPages,
],
]));
}
private function mockClient(array $responses): MockHttpClient
{
return new MockHttpClient($responses, 'https://myep.example/');
}
private function getClientInstance(MockHttpClient $httpClient): TravelExportClient
{
$tokenClient = new MockHttpClient(
fn () => new MockResponse('{"access_token":"token","expires_in":600}'),
'https://myep.example/',
);
$tokens = new AccessTokenProvider($tokenClient, new ArrayAdapter(), new NullLogger(), [
'client_id' => 'team-app',
'client_secret' => 'secret',
]);
return new TravelExportClient(new ApiClient($httpClient, $tokens, new NullLogger()), new NullLogger());
}
}