diff --git a/app/Models/Foundation/Summit/SummitRegistrationStats.php b/app/Models/Foundation/Summit/SummitRegistrationStats.php index 37aad51aa..a49246b8a 100644 --- a/app/Models/Foundation/Summit/SummitRegistrationStats.php +++ b/app/Models/Foundation/Summit/SummitRegistrationStats.php @@ -599,77 +599,70 @@ public function getVirtualNonCheckedInAttendeesCount(?DateTime $startDate = nul return 0; } /** + * Counts tickets per badge feature type, including the features a badge inherits from its badge type + * (same semantics as SummitAttendeeBadge::getAllFeatures). + * @param bool $checked_in_only * @param DateTime|null $startDate * @param DateTime|null $endDate * @return array + * @throws \Exception */ - public function getActiveTicketsPerBadgeFeatureType(?DateTime $startDate = null, ?DateTime $endDate = null): array + private function getTicketsPerBadgeFeatureType(bool $checked_in_only, ?DateTime $startDate = null, ?DateTime $endDate = null): array { - try { + $checkin_join = $checked_in_only ? 'INNER JOIN SummitAttendee ON SummitAttendee.ID = SummitAttendeeTicket.OwnerID' : ''; + $checkin_filter = $checked_in_only ? ' AND SummitAttendee.SummitHallCheckedIn = 1' : ''; + + // one branch per level: the date filter is string concatenation on the WHERE clause, + // so it has to be applied to each branch and not to the combined query + $branch = function (string $feature_join) use ($checkin_join, $checkin_filter, $startDate, $endDate): string { $sql = <<prepareRawSQL($sql, ['summit_id' => $this->id]); - $res = $stmt->executeQuery(); - $res = $res->fetchAllAssociative(); - $res = count($res) > 0 ? $res : []; - return $res; - } catch (\Exception $ex) { + $badge_level = $branch(<<prepareRawSQL($sql, ['summit_id' => $this->id]); + return $stmt->executeQuery()->fetchAllAssociative(); + } - } - return []; + /** + * @param DateTime|null $startDate + * @param DateTime|null $endDate + * @return array + * @throws \Exception + */ + public function getActiveTicketsPerBadgeFeatureType(?DateTime $startDate = null, ?DateTime $endDate = null): array + { + return $this->getTicketsPerBadgeFeatureType(false, $startDate, $endDate); } /** * @param DateTime|null $startDate * @param DateTime|null $endDate * @return array + * @throws \Exception */ public function getAttendeesCheckinPerBadgeFeatureType(?DateTime $startDate = null, ?DateTime $endDate = null): array { - try { - $sql = <<prepareRawSQL($sql, ['summit_id' => $this->id]); - $res = $stmt->executeQuery(); - $res = $res->fetchAllAssociative(); - $res = count($res) > 0 ? $res : []; - return $res; - } catch (\Exception $ex) { - - } - return []; + return $this->getTicketsPerBadgeFeatureType(true, $startDate, $endDate); } /** diff --git a/tests/Repositories/RegistrationStatsBadgeFeatureTest.php b/tests/Repositories/RegistrationStatsBadgeFeatureTest.php new file mode 100644 index 000000000..21451f518 --- /dev/null +++ b/tests/Repositories/RegistrationStatsBadgeFeatureTest.php @@ -0,0 +1,137 @@ +current_group = IGroup::TrackChairs; + parent::setUp(); + self::insertSummitTestData(); + self::InsertOrdersTestData(); + + [$f1, $f2, $f3] = self::$badge_features; + self::$badge_type_2->addBadgeFeatureType($f2); + self::$badge_type_2->addBadgeFeatureType($f3); + + $layout = [ + [self::$default_badge_type, [$f1]], + [self::$badge_type_2, []], + [self::$badge_type_2, [$f3]], + [self::$badge_type_2, [$f1]], + ]; + + $checked_in = []; + foreach ($layout as $i => [$type, $features]) { + $ticket = self::$summit_orders[$i]->getTickets()->first(); + $badge = new SummitAttendeeBadge(); + $badge->setType($type); + $badge->setTicket($ticket); + foreach ($features as $f) { + $badge->addFeature($f); + } + $ticket->setBadge($badge); + + if ($i === 0) continue; + $attendee = new SummitAttendee(); + $attendee->setEmail(sprintf("badge-feature+%s@test.com", $i)); + $attendee->setFirstName("Attendee"); + $attendee->setSurname(sprintf("%s", $i)); + $attendee->addTicket($ticket); + self::$summit->addAttendee($attendee); + self::$em->persist($attendee); + self::$em->flush(); + if ($i !== 3) $checked_in[] = $attendee->getId(); + } + self::$em->flush(); + + DB::table('SummitAttendee')->whereIn('ID', $checked_in)->update(['SummitHallCheckedIn' => 1]); + + $id = self::$summit->getId(); + self::$em->clear(); + $this->stats_summit = self::$em->find(Summit::class, $id); + } + + protected function tearDown(): void + { + self::clearOrdersTestData(); + self::clearSummitTestData(); + parent::tearDown(); + } + + private static function byType(array $rows): array + { + $res = array_column($rows, 'qty', 'type'); + $res = array_map('intval', $res); + ksort($res); + return $res; + } + + public function testSoldQtyIncludesFeaturesInheritedFromTheBadgeType(): void + { + $this->assertSame( + ['FEATURE 1' => 2, 'FEATURE 2' => 3, 'FEATURE 3' => 3], + self::byType($this->stats_summit->getActiveTicketsPerBadgeFeatureType()) + ); + } + + public function testCheckinQtyIncludesFeaturesInheritedFromTheBadgeType(): void + { + $this->assertSame( + ['FEATURE 2' => 2, 'FEATURE 3' => 2], + self::byType($this->stats_summit->getAttendeesCheckinPerBadgeFeatureType()) + ); + } + + public function testDateFilterAppliesToBothBranches(): void + { + $future = new DateTime('+1 year', new DateTimeZone('UTC')); + $this->assertSame([], $this->stats_summit->getActiveTicketsPerBadgeFeatureType($future)); + $this->assertSame([], $this->stats_summit->getAttendeesCheckinPerBadgeFeatureType($future)); + + $past = new DateTime('-1 year', new DateTimeZone('UTC')); + $this->assertSame( + ['FEATURE 1' => 2, 'FEATURE 2' => 3, 'FEATURE 3' => 3], + self::byType($this->stats_summit->getActiveTicketsPerBadgeFeatureType($past)) + ); + } +}