From 66f4be386cec4040aab0ba9ac4ac9e975bb2481a Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sun, 4 Oct 2026 12:26:23 +0200 Subject: [PATCH] Seed sample events and bookings for fixture members --- lib/GaletteEvents/Fixtures.php | 306 +++++++++++++++++++ tests/GaletteEvents/tests/units/Fixtures.php | 130 ++++++++ 2 files changed, 436 insertions(+) create mode 100644 lib/GaletteEvents/Fixtures.php create mode 100644 tests/GaletteEvents/tests/units/Fixtures.php diff --git a/lib/GaletteEvents/Fixtures.php b/lib/GaletteEvents/Fixtures.php new file mode 100644 index 0000000..24b31a9 --- /dev/null +++ b/lib/GaletteEvents/Fixtures.php @@ -0,0 +1,306 @@ + + * @phpstan-type EventData array{ + * name: string, + * town: string, + * zip: string, + * address: string, + * begin: int, + * days: int, + * color: string, + * activities: array, + * bookings: int, + * group?: int, + * closed?: bool, + * comment?: string + * } + */ +class Fixtures implements FixturesProviderInterface +{ + /** @var array Activities, with their comment */ + private const array ACTIVITIES = [ + 'Repas' => 'Repas pris en commun', + 'Hébergement' => 'Nuitée en gîte ou en auberge', + 'Visite guidée' => 'Visite avec un guide local', + 'Covoiturage' => 'Départ groupé depuis le local', + ]; + + /** @var list Events; begin in days from today, group and bookings members by position */ + private const array EVENTS = [ + ['name' => 'Pique-nique de rentrée', 'town' => 'Bordeaux', 'zip' => '33000', + 'address' => 'Parc bordelais', 'begin' => -35, 'days' => 0, 'color' => '#2e7d32', + 'activities' => ['Repas' => Activity::REQUIRED], 'bookings' => 12], + ['name' => 'Atelier réparation de vélos', 'town' => 'Rennes', 'zip' => '35000', + 'address' => '12 rue de la Soif', 'begin' => -20, 'days' => 0, 'color' => '#6d4c41', + 'activities' => [], 'bookings' => 6], + ['name' => 'Conseil d\'administration', 'town' => 'Lyon', 'zip' => '69002', + 'address' => 'Salle du conseil', 'begin' => -60, 'days' => 0, 'color' => '#455a64', + 'activities' => [], 'bookings' => 4, 'group' => 1], + ['name' => 'Forum des associations', 'town' => 'Lille', 'zip' => '59000', + 'address' => 'Grand Palais', 'begin' => 7, 'days' => 1, 'color' => '#1565c0', + 'activities' => ['Covoiturage' => Activity::YES], 'bookings' => 8, + 'comment' => 'Venez tenir le stand avec nous !'], + ['name' => 'Réunion du bureau', 'town' => 'Paris', 'zip' => '75011', + 'address' => 'Local associatif', 'begin' => 10, 'days' => 0, 'color' => '#455a64', + 'activities' => [], 'bookings' => 5, 'group' => 0], + ['name' => 'Assemblée générale', 'town' => 'Paris', 'zip' => '75011', + 'address' => 'Salle des fêtes', 'begin' => 21, 'days' => 0, 'color' => '#c62828', + 'activities' => ['Repas' => Activity::YES], 'bookings' => 15, + 'comment' => 'Ordre du jour envoyé par courriel.'], + ['name' => 'Sortie vélo en Camargue', 'town' => 'Arles', 'zip' => '13200', + 'address' => 'Place de la République', 'begin' => 45, 'days' => 2, 'color' => '#ef6c00', + 'activities' => [ + 'Hébergement' => Activity::REQUIRED, + 'Repas' => Activity::YES, + 'Covoiturage' => Activity::YES, + ], 'bookings' => 10], + ['name' => 'Fête de fin d\'année', 'town' => 'Nantes', 'zip' => '44000', + 'address' => 'Hangar à bananes', 'begin' => 80, 'days' => 0, 'color' => '#ad1457', + 'activities' => ['Repas' => Activity::REQUIRED], 'bookings' => 14], + ['name' => 'Week-end découverte à Strasbourg', 'town' => 'Strasbourg', 'zip' => '67000', + 'address' => 'Place Kléber', 'begin' => 120, 'days' => 1, 'color' => '#6a1b9a', + 'activities' => [ + 'Hébergement' => Activity::YES, + 'Visite guidée' => Activity::YES, + 'Repas' => Activity::NO, + ], 'bookings' => 7], + ['name' => 'Voyage à Rome', 'town' => 'Rome', 'zip' => '00184', + 'address' => 'Colisée', 'begin' => 150, 'days' => 4, 'color' => '#f9a825', + 'activities' => ['Hébergement' => Activity::REQUIRED, 'Visite guidée' => Activity::REQUIRED], + 'bookings' => 9, 'closed' => true, 'comment' => 'Complet, inscriptions closes.'], + ]; + + /** @var array Activities identifiers, by name */ + private array $activities = []; + + /** + * Create activities, events and their bookings + * + * @param FixturesContext $context Fixtures context + */ + public function seedFixtures(FixturesContext $context): string + { + foreach (self::ACTIVITIES as $name => $comment) { + $this->activities[$name] = $this->getActivityId($context, $name, $comment); + } + + $bookings = 0; + foreach (self::EVENTS as $position => $data) { + $event = $this->addEvent($context, $data); + $bookings += $this->addBookings($context, $event, $data, $position); + } + + return sprintf('Created %d events and %d bookings', count(self::EVENTS), $bookings); + } + + /** + * Get activity identifier, create it if missing + * + * @param FixturesContext $context Fixtures context + * @param string $name Activity name + * @param string $comment Activity comment + */ + private function getActivityId(FixturesContext $context, string $name, string $comment): int + { + $select = $context->zdb->select(EVENTS_PREFIX . Activity::TABLE) + ->columns([Activity::PK]) + ->where(['name' => $name]) + ->limit(1); + $row = $context->zdb->execute($select)->current(); + if ($row) { + return (int)$row->{Activity::PK}; + } + + $activity = new Activity($context->zdb, $context->history); + if (!$activity->check(['name' => $name, 'comment' => $comment, 'active' => '1'])) { + throw new RuntimeException(sprintf('Invalid fixture activity %s', $name)); + } + $activity->store(); + + //created a while ago + $update = $context->zdb->update(EVENTS_PREFIX . Activity::TABLE) + ->set(['creation_date' => $this->getDate(-400)]) + ->where([Activity::PK => $activity->getId()]); + $context->zdb->execute($update); + + return (int)$activity->getId(); + } + + /** + * Create an event + * + * @param FixturesContext $context Fixtures context + * @param EventData $data Event data + */ + private function addEvent(FixturesContext $context, array $data): Event + { + $values = [ + 'name' => $data['name'], + 'address' => $data['address'], + 'zip' => $data['zip'], + 'town' => $data['town'], + 'begin_date' => $this->getDate($data['begin']), + 'end_date' => $this->getDate($data['begin'] + $data['days']), + 'color' => $data['color'], + 'comment' => $data['comment'] ?? '', + 'activities_ids' => [], + 'activities_status' => [], + ]; + if (!($data['closed'] ?? false)) { + $values['open'] = '1'; + } + if (isset($data['group'])) { + $values['group'] = (string)$context->getGroupId($data['group']); + } + foreach ($data['activities'] as $name => $status) { + $values['activities_ids'][] = (string)$this->activities[$name]; + $values['activities_status'][] = (string)$status; + } + + $event = new Event($context->zdb, $context->login, $context->history); + if (!$event->check($values)) { + throw new RuntimeException(sprintf( + 'Invalid fixture event %s: %s', + $data['name'], + implode(', ', $event->getErrors()) + )); + } + $event->store(); + + //announced a while before it begins + $update = $context->zdb->update(EVENTS_PREFIX . Event::TABLE) + ->set(['creation_date' => $this->getDate(min(-1, $data['begin'] - 60))]) + ->where([Event::PK => $event->getId()]); + $context->zdb->execute($update); + + return $event; + } + + /** + * Book event for some fixture members + * + * @param FixturesContext $context Fixtures context + * @param Event $event Event + * @param EventData $data Event data + * @param int $position Event position + * + * @return int Number of bookings created + */ + private function addBookings(FixturesContext $context, Event $event, array $data, int $position): int + { + $members = []; + for ($i = 0; $i < min($data['bookings'], count($context->members)); ++$i) { + $member = $context->getMemberId($position * 5 + $i * 3); + if ($member !== null) { + $members[$member] = $i; + } + } + + //booked between the announce and the event, or today + $announced = min(-1, $data['begin'] - 60); + $latest = min(0, $data['begin'] - 1); + foreach ($members as $member => $i) { + $values = [ + 'event' => (string)$event->getId(), + 'member' => (string)$member, + 'booking_date' => $this->getDate($announced + (($i * 7) % max(1, $latest - $announced + 1))), + 'number_people' => (string)(1 + ($i % 3 === 0 ? 1 : 0)), + 'activities' => [], + ]; + foreach ($data['activities'] as $name => $status) { + if ($status === Activity::REQUIRED || ($status === Activity::YES && $i % 2 === 0)) { + $values['activities'][] = (string)$this->activities[$name]; + } + } + //activities cost 15 each, a third of bookings are not paid yet + $amount = 15 * count($values['activities']) * (int)$values['number_people']; + if ($amount > 0) { + $values['amount'] = (string)$amount; + if ($i % 3 !== 2) { + $values['paid'] = '1'; + $values['payment_method'] = (string)($i % 2 === 0 ? PaymentType::CHECK : PaymentType::CASH); + } + } + + $booking = new Booking($context->zdb, $context->login, $context->history); + if (!$booking->check($values)) { + throw new RuntimeException(sprintf( + 'Invalid fixture booking for %s: %s', + $data['name'], + implode(', ', $booking->getErrors()) + )); + } + $booking->store(); + + $update = $context->zdb->update(EVENTS_PREFIX . Booking::TABLE) + ->set(['creation_date' => $values['booking_date']]) + ->where([Booking::PK => $booking->getId()]); + $context->zdb->execute($update); + } + + return count($members); + } + + /** + * Get date from a number of days relative to today + * + * @param int $days Days, negative in the past + */ + private function getDate(int $days): string + { + return (new \DateTimeImmutable('today'))->modify(sprintf('%+d days', $days))->format('Y-m-d'); + } + + /** + * Remove fixture events, with their bookings, then activities nothing uses anymore + * + * @param FixturesContext $context Fixtures context + */ + public function cleanFixtures(FixturesContext $context): void + { + $zdb = $context->zdb; + + //bookings and their activities follow + $delete = $zdb->delete(EVENTS_PREFIX . Event::TABLE); + $delete->where->in('name', array_column(self::EVENTS, 'name')); + $zdb->execute($delete); + + $select = $zdb->select(EVENTS_PREFIX . 'activitiesevents') + ->columns([Activity::PK]) + ->quantifier('DISTINCT'); + $used = []; + foreach ($zdb->execute($select) as $row) { + $used[] = (int)$row->{Activity::PK}; + } + + $delete = $zdb->delete(EVENTS_PREFIX . Activity::TABLE); + $delete->where->in('name', array_keys(self::ACTIVITIES)); + if ($used !== []) { + $delete->where->notIn(Activity::PK, $used); + } + $zdb->execute($delete); + } +} diff --git a/tests/GaletteEvents/tests/units/Fixtures.php b/tests/GaletteEvents/tests/units/Fixtures.php new file mode 100644 index 0000000..ab6bb38 --- /dev/null +++ b/tests/GaletteEvents/tests/units/Fixtures.php @@ -0,0 +1,130 @@ + + */ +class Fixtures extends GaletteTestCase +{ + use EventsFixtures; + + protected int $seed = 20261004150000; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->login->logout(); + $this->cleanEvents(); + parent::tearDown(); + } + + /** + * Count rows of a plugin table + * + * @param string $table Table name, without prefixes + */ + private function countRows(string $table): int + { + return $this->zdb->execute($this->zdb->select(EVENTS_PREFIX . $table))->count(); + } + + /** + * Test seeding and cleaning fixtures + */ + public function testFixtures(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $bureau = $this->createGroup('Fixtures bureau'); + $conseil = $this->createGroup('Fixtures conseil'); + $this->logSuperAdmin(); + + //an event of the user, using an activity fixtures also use + $activity = new \GaletteEvents\Activity($this->zdb, $this->history); + $this->assertTrue($activity->check(['name' => 'Repas', 'active' => '1'])); + $activity->store(); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); + $this->assertTrue($event->check([ + 'name' => 'User event', + 'town' => 'Lille', + 'begin_date' => date('Y-m-d', strtotime('+3 days')), + 'activities_ids' => [(string)$activity->getId()], + 'activities_status' => ['1'], + ])); + $event->store(); + + $context = new FixturesContext( + zdb: $this->zdb, + login: $this->login, + preferences: $this->preferences, + history: $this->history, + plugins: $this->plugins, + members: ['one' => $member_one->id, 'two' => $member_two->id], + groups: ['bureau' => (int)$bureau->getId(), 'conseil' => (int)$conseil->getId()] + ); + + $fixtures = new \GaletteEvents\Fixtures(); + $summary = $fixtures->seedFixtures($context); + $this->assertSame(sprintf('Created 10 events and %d bookings', $this->countRows('bookings')), $summary); + $this->assertSame(11, $this->countRows('events')); + //existing activity is reused + $this->assertSame(4, $this->countRows('activities')); + + $today = date('Y-m-d'); + $select = $this->zdb->select(EVENTS_PREFIX . \GaletteEvents\Event::TABLE); + $select->where->lessThan('begin_date', $today); + $this->assertSame(3, $this->zdb->execute($select)->count()); + + //events are announced, and bookings made, before they begin + foreach ($this->zdb->execute($this->zdb->select(EVENTS_PREFIX . \GaletteEvents\Event::TABLE)) as $row) { + if ($row->name === 'User event') { + continue; + } + $this->assertLessThan($row->begin_date, $row->creation_date, $row->name); + $this->assertLessThan($today, $row->creation_date, $row->name); + } + $select = $this->zdb->select(EVENTS_PREFIX . \GaletteEvents\Booking::TABLE, 'b') + ->join(['e' => PREFIX_DB . EVENTS_PREFIX . \GaletteEvents\Event::TABLE], 'b.id_event = e.id_event', ['begin_date']); + $bookings = $this->zdb->execute($select); + $this->assertGreaterThan(0, $bookings->count()); + foreach ($bookings as $row) { + $this->assertContains((int)$row->id_adh, [$member_one->id, $member_two->id]); + $this->assertLessThanOrEqual($today, $row->booking_date); + $this->assertLessThanOrEqual($row->begin_date, $row->booking_date); + } + + $select = $this->zdb->select(EVENTS_PREFIX . \GaletteEvents\Event::TABLE) + ->where(['name' => 'Réunion du bureau']); + $this->assertSame($bureau->getId(), (int)$this->zdb->execute($select)->current()->id_group); + + $select = $this->zdb->select(EVENTS_PREFIX . \GaletteEvents\Event::TABLE) + ->where(['name' => 'Voyage à Rome']); + $this->assertFalse((bool)$this->zdb->execute($select)->current()->is_open); + + $fixtures->cleanFixtures($context); + //only the user event and its activity remain + $this->assertSame(1, $this->countRows('events')); + $this->assertSame(0, $this->countRows('bookings')); + $this->assertSame(1, $this->countRows('activities')); + $this->assertSame($activity->getId(), (int)$this->zdb->execute( + $this->zdb->select(EVENTS_PREFIX . \GaletteEvents\Activity::TABLE) + )->current()->id_activity); + } +}