From 7600a9b6d236aa0ab8a6ceefdd5f85ddb5d22e33 Mon Sep 17 00:00:00 2001 From: smarcet Date: Thu, 15 Oct 2026 14:23:23 -0300 Subject: [PATCH 1/2] fix(tickets): use SIZE(b.prints) for badge prints filters has_badge_prints and badge_prints_count mapped to SIZE(prt), but DQL SIZE() needs a collection-valued path expression, not a joined alias, so both filters failed with a semantical error (500) on GET /summits/{id}/tickets. badge_prints_count also never added the badge join, leaving prt undefined. Map both filters to SIZE(b.prints) and join the badge for either filter; the prints join is kept only for ordering by badge_prints_count. --- ...DoctrineSummitAttendeeTicketRepository.php | 13 +++- tests/oauth2/OAuth2SummitTicketsApiTest.php | 76 +++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php index 516516877..939ee6305 100644 --- a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php +++ b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php @@ -169,7 +169,12 @@ private function requiredAliases(?Filter $filter, ?Order $order): array $need['b'] = $need['bt'] = $need['bf'] = $need['bt_bf'] = true; } - if ($has('has_badge_prints') || $ord('badge_prints_count')) { + // filters use SIZE(b.prints): DQL SIZE() needs a collection path, not the joined prt alias + if ($has('has_badge_prints') || $has('badge_prints_count')) { + $need['b'] = true; + } + + if ($ord('badge_prints_count')) { $need['b'] = $need['prt'] = true; } @@ -380,15 +385,15 @@ protected function getFilterMappings() 'has_badge_prints' => new DoctrineSwitchFilterMapping([ '1' => new DoctrineCaseFilterMapping( 'true', - "SIZE(prt) > 0" + "SIZE(b.prints) > 0" ), '0' => new DoctrineCaseFilterMapping( 'false', - "SIZE(prt) = 0" + "SIZE(b.prints) = 0" ), ] ), - 'badge_prints_count' => 'SIZE(prt) :operator :value', + 'badge_prints_count' => 'SIZE(b.prints) :operator :value', 'exclude_is_printable_free_unassigned' => new DoctrineSwitchFilterMapping([ '1' => new DoctrineCaseFilterMapping( 'true', diff --git a/tests/oauth2/OAuth2SummitTicketsApiTest.php b/tests/oauth2/OAuth2SummitTicketsApiTest.php index 555617c4a..95fd78767 100644 --- a/tests/oauth2/OAuth2SummitTicketsApiTest.php +++ b/tests/oauth2/OAuth2SummitTicketsApiTest.php @@ -1035,6 +1035,82 @@ public function testGetAllTicketsWithoutPromoCode() return $tickets; } + /** + * badge_prints_count filter maps to SIZE(prt), so the badge/prints joins must be added + * for the filter too (not only for the order), otherwise the count query fails with + * a DQL semantical error on the undefined prt alias. + */ + public function testGetAllTicketsByBadgePrintsCount() + { + foreach ([ + ['badge_prints_count>1'], + ['badge_prints_count==3,badge_prints_count==4,badge_prints_count==5'], + ['badge_prints_count==0'], + ] as $filter) { + $params = [ + 'id' => self::$summit->getId(), + 'page' => 1, + 'per_page' => 10, + 'filter' => $filter, + 'order' => '+id' + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitTicketApiController@getAllBySummit", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $tickets = json_decode($content); + $this->assertTrue(!is_null($tickets)); + } + } + + public function testGetAllTicketsByHasBadgePrints() + { + $params = [ + 'id' => self::$summit->getId(), + 'page' => 1, + 'per_page' => 10, + 'filter' => [ + 'has_badge_prints==1', + ], + 'order' => '+id' + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitTicketApiController@getAllBySummit", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $tickets = json_decode($content); + $this->assertTrue(!is_null($tickets)); + return $tickets; + } + public function testGetAttendeeBadge() { // Use the testCreateAttendeeBadge helper to ensure a badge exists From f694975756e27774d4b308fb90806b1a24684291 Mon Sep 17 00:00:00 2001 From: smarcet Date: Thu, 15 Oct 2026 14:30:03 -0300 Subject: [PATCH 2/2] fix(tickets): compute list total without the order Ordering the tickets list by badge_prints_count, refunded_amount or final_amount_adjusted adds GROUP BY e.id through applyExtraSelects, and getFastCount inherited it, so COUNT(DISTINCT e.id) returned one row per ticket (NonUniqueResultException) or none (NoResultException) -> 500. The total does not depend on the order, so count with the filter only. --- ...DoctrineSummitAttendeeTicketRepository.php | 4 +- tests/oauth2/OAuth2SummitTicketsApiTest.php | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php index 939ee6305..96a50ceb8 100644 --- a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php +++ b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php @@ -762,7 +762,9 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord $start = time(); Log::debug(sprintf('DoctrineSummitAttendeeTicketRepository::getAllByPage')); - $total = $this->getFastCount($filter, $order); + // the total does not depend on the order; aggregate orders (badge_prints_count, refunded_amount, + // final_amount_adjusted) add GROUP BY e.id, which breaks the single scalar COUNT + $total = $this->getFastCount($filter); $ids = $this->getAllIdsByPage($paging_info, $filter, $order); $query = $this->getEntityManager()->createQueryBuilder() ->select('e, a, o, tt, pc, b, bt, a_c, m') diff --git a/tests/oauth2/OAuth2SummitTicketsApiTest.php b/tests/oauth2/OAuth2SummitTicketsApiTest.php index 95fd78767..cd22b5e90 100644 --- a/tests/oauth2/OAuth2SummitTicketsApiTest.php +++ b/tests/oauth2/OAuth2SummitTicketsApiTest.php @@ -1077,6 +1077,52 @@ public function testGetAllTicketsByBadgePrintsCount() } } + /** + * Orders backed by aggregates add GROUP BY e.id; the total count must not inherit it, + * otherwise COUNT(DISTINCT e.id) yields one row per ticket (or none) and the request 500s. + */ + public function testGetAllTicketsOrderedByAggregates() + { + foreach ([ + [[], '-badge_prints_count'], + [['badge_prints_count>=1'], '-badge_prints_count'], + [[], '+refunded_amount'], + [[], '-final_amount_adjusted'], + ] as [$filter, $order]) { + $params = [ + 'id' => self::$summit->getId(), + 'page' => 1, + 'per_page' => 10, + 'filter' => $filter, + 'order' => $order + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitTicketApiController@getAllBySummit", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $tickets = json_decode($content); + $this->assertTrue(!is_null($tickets)); + if (empty($filter)) { + $this->assertGreaterThan(1, $tickets->total); + $this->assertCount(min(10, $tickets->total), $tickets->data); + } + } + } + public function testGetAllTicketsByHasBadgePrints() { $params = [