- Move 29 display-only event fields (subtitles, audience, tags, prices, price types, organizer) from columns into the jsonData storage; fields used for filtering, joins and calculations stay real columns - Add EventFieldsToVirtualMigration, which copies the values and drops the columns on contao:migrate - Read and write the fields in the import, the preview and the booking through EventRowCodec; other bundles' keys in the storage are kept - Validate prices in a save callback instead of the percent check that rejected every price above 100; accept a decimal comma - Do not report events as changed after a backend save: ignore the spelling of HTML entities in teaser and topics and the order of the speakers Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
165 lines
7.3 KiB
PHP
165 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Mummert\ContaoWkSyncBundle\Tests\Import;
|
|
|
|
use Doctrine\DBAL\Connection;
|
|
use Mummert\ContaoWkSyncBundle\Category\Category;
|
|
use Mummert\ContaoWkSyncBundle\Category\CategoryKind;
|
|
use Mummert\ContaoWkSyncBundle\Category\CategoryRepository;
|
|
use Mummert\ContaoWkSyncBundle\Category\CategoryResolver;
|
|
use Mummert\ContaoWkSyncBundle\Import\CsvTable;
|
|
use Mummert\ContaoWkSyncBundle\Import\ImportPlanner;
|
|
use Mummert\ContaoWkSyncBundle\Import\MemberResolver;
|
|
use Mummert\ContaoWkSyncBundle\Import\PlannedEvent;
|
|
use Mummert\ContaoWkSyncBundle\Import\RowMapper;
|
|
use Mummert\ContaoWkSyncBundle\Sync\CalendarConfig;
|
|
use Mummert\ContaoWkSyncBundle\Sync\EventRowCodec;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class ImportPlannerTest extends TestCase
|
|
{
|
|
private string $sql = '';
|
|
|
|
public function testReimportWithoutMemberGroupsIsUnchanged(): void
|
|
{
|
|
$plan = $this->plan(existingInstructor: 0, existingSpeakers: [], memberIds: [], instructorGroup: 0, speakerGroup: 0);
|
|
|
|
self::assertSame(PlannedEvent::UNCHANGED, $plan->events[0]->action);
|
|
self::assertSame([], $plan->events[0]->changes);
|
|
}
|
|
|
|
public function testResolvedMembersAreComparedById(): void
|
|
{
|
|
$plan = $this->plan(existingInstructor: 11, existingSpeakers: [12], memberIds: ['Anna Muster' => 11, 'Bernd Beispiel' => 12], instructorGroup: 0, speakerGroup: 0);
|
|
|
|
self::assertSame(PlannedEvent::UNCHANGED, $plan->events[0]->action);
|
|
}
|
|
|
|
public function testMemberGroupsPlanMemberCreation(): void
|
|
{
|
|
$plan = $this->plan(existingInstructor: 0, existingSpeakers: [], memberIds: [], instructorGroup: 4, speakerGroup: 5);
|
|
|
|
self::assertSame(PlannedEvent::UPDATE, $plan->events[0]->action);
|
|
self::assertSame(['wkInstructor', 'wkSpeakers'], array_keys($plan->events[0]->changes));
|
|
self::assertSame(['', 'Anna Muster'], $plan->events[0]->changes['wkInstructor']);
|
|
}
|
|
|
|
public function testChangedTitleIsAnUpdate(): void
|
|
{
|
|
$plan = $this->plan(existingInstructor: 0, existingSpeakers: [], memberIds: [], instructorGroup: 0, speakerGroup: 0, existingOverrides: ['title' => 'Alter Titel']);
|
|
|
|
self::assertSame(PlannedEvent::UPDATE, $plan->events[0]->action);
|
|
self::assertSame(['Alter Titel', 'Testseminar'], $plan->events[0]->changes['title']);
|
|
}
|
|
|
|
public function testVirtualFieldsAreComparedFromTheJsonStorage(): void
|
|
{
|
|
$plan = $this->plan(existingInstructor: 0, existingSpeakers: [], memberIds: [], instructorGroup: 0, speakerGroup: 0, existingOverrides: ['wkSubTitle' => 'Alter Untertitel', 'wkPrice1' => '99.00']);
|
|
|
|
self::assertSame(PlannedEvent::UPDATE, $plan->events[0]->action);
|
|
self::assertSame(['wkSubTitle', 'wkPrice1'], array_keys($plan->events[0]->changes));
|
|
self::assertSame(['Alter Untertitel', 'Modul 1'], $plan->events[0]->changes['wkSubTitle']);
|
|
self::assertSame(['99.00', 216.0], $plan->events[0]->changes['wkPrice1']);
|
|
}
|
|
|
|
public function testEntitySpellingAfterABackendSaveIsNoChange(): void
|
|
{
|
|
$plan = $this->plan(
|
|
existingInstructor: 0,
|
|
existingSpeakers: [],
|
|
memberIds: [],
|
|
instructorGroup: 0,
|
|
speakerGroup: 0,
|
|
existingOverrides: ['teaser' => '<p>Er sagte "Hallo" & ging</p>'],
|
|
rowOverrides: ['kbpmemo' => 'Er sagte "Hallo" & ging'],
|
|
);
|
|
|
|
self::assertSame(PlannedEvent::UNCHANGED, $plan->events[0]->action);
|
|
}
|
|
|
|
public function testSpeakerOrderDoesNotMatter(): void
|
|
{
|
|
$plan = $this->plan(
|
|
existingInstructor: 11,
|
|
existingSpeakers: [13, 12],
|
|
memberIds: ['Anna Muster' => 11, 'Bernd Beispiel' => 12, 'Clara Chor' => 13],
|
|
instructorGroup: 0,
|
|
speakerGroup: 0,
|
|
rowOverrides: ['kbmref' => "Bernd Beispiel\nClara Chor"],
|
|
);
|
|
|
|
self::assertSame(PlannedEvent::UNCHANGED, $plan->events[0]->action);
|
|
}
|
|
|
|
public function testExistingRowsAreReadFromRealColumnsAndTheJsonStorage(): void
|
|
{
|
|
$this->plan(existingInstructor: 0, existingSpeakers: [], memberIds: [], instructorGroup: 0, speakerGroup: 0);
|
|
|
|
self::assertStringContainsString('`jsonData`', $this->sql);
|
|
self::assertStringContainsString('`wkState`', $this->sql);
|
|
self::assertStringNotContainsString('wkSubTitle', $this->sql);
|
|
self::assertStringNotContainsString('wkPrice1', $this->sql);
|
|
self::assertStringNotContainsString('wkOrganizer', $this->sql);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, int> $memberIds Name => Mitglieds-ID (nur diese Namen sind Mitglieder)
|
|
* @param list<int> $existingSpeakers
|
|
* @param array<string, mixed> $existingOverrides
|
|
* @param array<string, string> $rowOverrides abweichende Werte der W&K-Zeile
|
|
*/
|
|
private function plan(int $existingInstructor, array $existingSpeakers, array $memberIds, int $instructorGroup, int $speakerGroup, array $existingOverrides = [], array $rowOverrides = []): \Mummert\ContaoWkSyncBundle\Import\ImportPlan
|
|
{
|
|
$row = [
|
|
'kbnr' => '0001234',
|
|
'kbvsnr' => 'ABC-1',
|
|
'kbtitel' => 'Testseminar',
|
|
'kbanreise' => '01.03.2027',
|
|
'kbabreise' => '02.03.2027',
|
|
'kbstatus1' => '2',
|
|
'kbsgebiet' => 'Grundschule',
|
|
'kbkln1' => 'Anna Muster',
|
|
'kbmref' => "\nBernd Beispiel",
|
|
'kbutitel' => 'Modul 1',
|
|
'kbkp1' => '216,00',
|
|
'kbktxp1' => 'Kursgebühr',
|
|
...$rowOverrides,
|
|
];
|
|
|
|
$mapped = (new RowMapper())->map($row, 2);
|
|
|
|
// Bestand wie in der Datenbank: echte Spalten plus JSON-Feld mit den virtuellen Feldern
|
|
$existing = (new EventRowCodec())->combine([
|
|
...$mapped->data,
|
|
...$existingOverrides,
|
|
'id' => 7,
|
|
'published' => 1,
|
|
'wkCategories' => serialize(['3']),
|
|
'wkCategoryPrimary' => 3,
|
|
'wkInstructor' => $existingInstructor,
|
|
'wkSpeakers' => serialize(array_map(strval(...), $existingSpeakers)),
|
|
]);
|
|
|
|
$connection = $this->createStub(Connection::class);
|
|
$connection->method('fetchAllAssociative')->willReturnCallback(function (string $sql) use ($existing): array {
|
|
$this->sql = $sql;
|
|
|
|
return [$existing];
|
|
});
|
|
|
|
$categories = $this->createStub(CategoryRepository::class);
|
|
$categories->method('findAll')->willReturn([new Category(3, 'Grundschule', 'grundschule', CategoryKind::School, ['Grundschule'], 16)]);
|
|
|
|
$members = $this->createStub(MemberResolver::class);
|
|
$members->method('findId')->willReturnCallback(static fn (string $name): int|null => $memberIds[$name] ?? null);
|
|
$members->method('namesByIds')->willReturnCallback(static fn (array $ids): array => array_filter(array_flip($memberIds), static fn (int $id): bool => \in_array($id, $ids, true)));
|
|
|
|
$planner = new ImportPlanner($connection, new RowMapper(), $categories, new CategoryResolver($categories), $members);
|
|
$calendar = new CalendarConfig(1, 1, 'Kalender', 'Einstellungen', 0, true, 2, 8, 9, 0, 1, 31, ['TPI Moritzburg'], 1, $instructorGroup, $speakerGroup, [], 0, '', 0, []);
|
|
|
|
return $planner->plan(new CsvTable(array_keys($row), [$row + ['__record' => '2']], 'UTF-8'), $calendar);
|
|
}
|
|
}
|