From 4eab47cd2da08f1ee99266150923c76d561ff803 Mon Sep 17 00:00:00 2001 From: smarcet Date: Mon, 29 Sep 2025 20:47:50 -0300 Subject: [PATCH 1/7] fix: improve ticket csv serializer performance fix: improve ticket repository performance remove not needed extra joins fix: set fetchJoinCollection to false for ticket repo fix: get always owner to avoid N+1 on ticket repository fix: improve get tickets generic pagination --- .../OAuth2SummitTicketApiController.php | 371 +++++++++--------- ...DoctrineSummitAttendeeTicketRepository.php | 2 +- 2 files changed, 186 insertions(+), 187 deletions(-) diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitTicketApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitTicketApiController.php index 9ee2fb37f..8d7f07036 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitTicketApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitTicketApiController.php @@ -28,6 +28,7 @@ use models\oauth2\IResourceServerContext; use models\summit\IOrderConstants; use models\summit\ISummitAttendeeRepository; +use models\summit\ISummitAttendeeRepository; use models\summit\ISummitAttendeeTicketRepository; use models\summit\ISummitRepository; use models\summit\Summit; @@ -289,157 +290,156 @@ function () { */ public function getAllBySummitCSV($summit_id) { - return $this->withReplica(function() use ($summit_id) { - $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->getResourceServerContext())->find($summit_id); - if (is_null($summit)) return $this->error404(); - $questions = $summit->getOrderExtraQuestionsByUsage(SummitOrderExtraQuestionTypeConstants::TicketQuestionUsage); - return $this->_getAllCSV( - function () { - return [ - 'id' => ['=='], - 'not_id' => ['=='], - 'number' => ['@@', '=@', '=='], - 'order_number' => ['@@', '=@', '=='], - 'owner_name' => ['@@', '=@', '=='], - 'owner_first_name' => ['@@', '=@', '=='], - 'owner_last_name' => ['@@', '=@', '=='], - 'owner_email' => ['@@', '=@', '=='], - 'owner_company' => ['@@', '=@', '=='], - 'summit_id' => ['=='], - 'owner_id' => ['=='], - 'order_id' => ['=='], - 'status' => ['==', '<>'], - 'is_active' => ['=='], - 'has_requested_refund_requests' => ['=='], - 'access_level_type_name' => ['=='], - 'access_level_type_id' => ['=='], - 'ticket_type_id' => ['=='], - 'has_owner' => ['=='], - 'owner_status' => ['=='], - 'has_badge' => ['=='], - 'view_type_id' => ['=='], - 'promo_code_id' => ['=='], - 'promo_code' => ['==', '@@', '=@'], - 'promo_code_tag' => ['==', '@@', '=@'], - 'promo_code_tag_id' => ['=='], - 'promo_code_description' => ['@@', '=@'], - 'final_amount' => ['==', '<>', '>=', '>'], - 'is_printable' => ['=='], - 'badge_type_id' => ['=='], - 'has_badge_prints' => ['=='], - 'badge_prints_count' => ['==', '>=', '<=', '>', '<'], - 'has_owner_company' => ['=='], - 'exclude_is_printable_free_unassigned' => ['=='], - ]; - }, - function () { - return [ - 'id' => 'sometimes|integer', - 'not_id' => 'sometimes|integer', - 'status' => sprintf('sometimes|in:%s', implode(',', IOrderConstants::ValidStatus)), - 'number' => 'sometimes|string', - 'order_number' => 'sometimes|string', - 'owner_name' => 'sometimes|string', - 'owner_first_name' => 'sometimes|string', - 'owner_last_name' => 'sometimes|string', - 'owner_email' => 'sometimes|string', - 'owner_company' => 'sometimes|string', - 'summit_id' => 'sometimes|integer', - 'owner_id' => 'sometimes|integer', - 'order_id' => 'sometimes|integer', - 'is_active' => 'sometimes|boolean', - 'has_requested_refund_requests' => 'sometimes|boolean', - 'access_level_type_name' => 'sometimes|string', - 'access_level_type_id' => 'sometimes|integer', - 'ticket_type_id' => 'sometimes|integer', - 'has_owner' => 'sometimes|boolean', - 'owner_status' => 'sometimes|string|in:' . implode(',', SummitAttendee::AllowedStatus), - 'has_badge' => 'sometimes|boolean', - 'view_type_id' => 'sometimes|integer', - 'promo_code_id' => 'sometimes|integer', - 'promo_code' => 'sometimes|string', - 'promo_code_tag' => 'sometimes|string', - 'promo_code_description' => 'sometimes|string', - 'promo_code_tag_id' => 'sometimes|integer', - 'final_amount' => 'sometimes|numeric', - 'is_printable' => ['sometimes', new Boolean()], - 'badge_type_id' => 'sometimes|integer', - 'has_badge_prints' => ['sometimes', new Boolean()], - 'badge_prints_count' => 'sometimes|integer', - 'has_owner_company' => ['sometimes', new Boolean()], - 'exclude_is_printable_free_unassigned' => ['sometimes', new Boolean()], - ]; - }, - function () { - return [ - 'id', - 'number', - 'status', - 'owner_name', - 'owner_first_name', - 'owner_last_name', - 'ticket_type', - 'final_amount', - 'owner_email', - 'owner_company', - 'promo_code', - 'bought_date', - 'refunded_amount', - 'final_amount_adjusted', - 'badge_type_id', - 'badge_type', - 'badge_prints_count', - 'created', - ]; - }, - function ($filter) use ($summit) { - if ($filter instanceof Filter) { - $filter->addFilterCondition(FilterElement::makeEqual('summit_id', $summit->getId())); - } - return $filter; - }, - function () { - return SerializerRegistry::SerializerType_CSV; - }, - function () use($summit) { - return [ - 'created' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), - 'last_edited' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), - 'purchase_date' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), - 'attendee_checked_in' => new BooleanCellFormatter(), - 'is_active' => new BooleanCellFormatter(), - ]; - }, - function () use ($summit) { - $allowed_columns = [ - 'id', - 'created', - 'last_edited', - 'number', - 'status', - 'attendee_id', - 'attendee_first_name', - 'attendee_last_name', - 'attendee_email', - 'attendee_company', - 'external_order_id', - 'external_attendee_id', - 'purchase_date', - 'ticket_type_id', - 'ticket_type_name', - 'order_id', - 'badge_id', - 'promo_code_id', - 'promo_code', - 'raw_cost', - 'final_amount', - 'discount', - 'refunded_amount', - 'currency', - 'badge_type_id', - 'badge_type_name', - 'promo_code_tags', - ]; + $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->getResourceServerContext())->find($summit_id); + if (is_null($summit)) return $this->error404(); + $questions = $summit->getOrderExtraQuestionsByUsage(SummitOrderExtraQuestionTypeConstants::TicketQuestionUsage); + return $this->_getAllCSV( + function () { + return [ + 'id' => ['=='], + 'not_id' => ['=='], + 'number' => ['@@', '=@', '=='], + 'order_number' => ['@@', '=@', '=='], + 'owner_name' => ['@@', '=@', '=='], + 'owner_first_name' => ['@@', '=@', '=='], + 'owner_last_name' => ['@@', '=@', '=='], + 'owner_email' => ['@@', '=@', '=='], + 'owner_company' => ['@@', '=@', '=='], + 'summit_id' => ['=='], + 'owner_id' => ['=='], + 'order_id' => ['=='], + 'status' => ['==', '<>'], + 'is_active' => ['=='], + 'has_requested_refund_requests' => ['=='], + 'access_level_type_name' => ['=='], + 'access_level_type_id' => ['=='], + 'ticket_type_id' => ['=='], + 'has_owner' => ['=='], + 'owner_status' => ['=='], + 'has_badge' => ['=='], + 'view_type_id' => ['=='], + 'promo_code_id' => ['=='], + 'promo_code' => ['==', '@@', '=@'], + 'promo_code_tag' => ['==', '@@', '=@'], + 'promo_code_tag_id' => ['=='], + 'promo_code_description' => ['@@', '=@'], + 'final_amount' => ['==', '<>', '>=', '>'], + 'is_printable' => ['=='], + 'badge_type_id' => ['=='], + 'has_badge_prints' => ['=='], + 'badge_prints_count' => ['==', '>=', '<=', '>', '<'], + 'has_owner_company' => ['=='], + 'exclude_is_printable_free_unassigned' => ['=='], + ]; + }, + function () { + return [ + 'id' => 'sometimes|integer', + 'not_id' => 'sometimes|integer', + 'status' => sprintf('sometimes|in:%s', implode(',', IOrderConstants::ValidStatus)), + 'number' => 'sometimes|string', + 'order_number' => 'sometimes|string', + 'owner_name' => 'sometimes|string', + 'owner_first_name' => 'sometimes|string', + 'owner_last_name' => 'sometimes|string', + 'owner_email' => 'sometimes|string', + 'owner_company' => 'sometimes|string', + 'summit_id' => 'sometimes|integer', + 'owner_id' => 'sometimes|integer', + 'order_id' => 'sometimes|integer', + 'is_active' => 'sometimes|boolean', + 'has_requested_refund_requests' => 'sometimes|boolean', + 'access_level_type_name' => 'sometimes|string', + 'access_level_type_id' => 'sometimes|integer', + 'ticket_type_id' => 'sometimes|integer', + 'has_owner' => 'sometimes|boolean', + 'owner_status' => 'sometimes|string|in:' . implode(',', SummitAttendee::AllowedStatus), + 'has_badge' => 'sometimes|boolean', + 'view_type_id' => 'sometimes|integer', + 'promo_code_id' => 'sometimes|integer', + 'promo_code' => 'sometimes|string', + 'promo_code_tag' => 'sometimes|string', + 'promo_code_description' => 'sometimes|string', + 'promo_code_tag_id' => 'sometimes|integer', + 'final_amount' => 'sometimes|numeric', + 'is_printable' => ['sometimes', new Boolean()], + 'badge_type_id' => 'sometimes|integer', + 'has_badge_prints' => ['sometimes', new Boolean()], + 'badge_prints_count' => 'sometimes|integer', + 'has_owner_company' => ['sometimes', new Boolean()], + 'exclude_is_printable_free_unassigned' => ['sometimes', new Boolean()], + ]; + }, + function () { + return [ + 'id', + 'number', + 'status', + 'owner_name', + 'owner_first_name', + 'owner_last_name', + 'ticket_type', + 'final_amount', + 'owner_email', + 'owner_company', + 'promo_code', + 'bought_date', + 'refunded_amount', + 'final_amount_adjusted', + 'badge_type_id', + 'badge_type', + 'badge_prints_count', + 'created', + ]; + }, + function ($filter) use ($summit) { + if ($filter instanceof Filter) { + $filter->addFilterCondition(FilterElement::makeEqual('summit_id', $summit->getId())); + } + return $filter; + }, + function () { + return SerializerRegistry::SerializerType_CSV; + }, + function () use($summit) { + return [ + 'created' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), + 'last_edited' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), + 'purchase_date' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), + 'attendee_checked_in' => new BooleanCellFormatter(), + 'is_active' => new BooleanCellFormatter(), + ]; + }, + function () use ($summit) { + $allowed_columns = [ + 'id', + 'created', + 'last_edited', + 'number', + 'status', + 'attendee_id', + 'attendee_first_name', + 'attendee_last_name', + 'attendee_email', + 'attendee_company', + 'external_order_id', + 'external_attendee_id', + 'purchase_date', + 'ticket_type_id', + 'ticket_type_name', + 'order_id', + 'badge_id', + 'promo_code_id', + 'promo_code', + 'raw_cost', + 'final_amount', + 'discount', + 'refunded_amount', + 'currency', + 'badge_type_id', + 'badge_type_name', + 'promo_code_tags', + ]; foreach ($summit->getBadgeFeaturesTypes() as $featuresType) { $allowed_columns[] = $featuresType->getName(); @@ -449,42 +449,41 @@ function () use ($summit) { $allowed_columns[] = $question->getLabel(); } - $columns_param = Request::input("columns", ""); - $columns = []; - if (!empty($columns_param)) - $columns = explode(',', $columns_param); - $diff = array_diff($columns, $allowed_columns); - if (count($diff) > 0) { - throw new ValidationException(sprintf("columns %s are not allowed!", implode(",", $diff))); - } - if (empty($columns)) - $columns = $allowed_columns; - return $columns; - }, - sprintf('tickets-%s-', $summit_id), - [ - 'features_types' => $summit->getBadgeFeaturesTypes(), - 'ticket_questions' => $questions - ], - null, - function($data, $serializerParams) use($questions){ - - $owners = []; - foreach ($data->getItems() as $t){ - if ($t->hasOwner()) $owners[] = $t->getOwner()->getId(); - } - $questionIds = []; - foreach ($questions as $q) { - $questionIds[] = $q->getId(); - } - $questionIds = array_values(array_unique($questionIds)); - $owners = array_values(array_unique($owners)); - - $serializerParams['answers_by_owner'] = $this->attendee_repository->getExtraQuestionAnswersByOwners($owners, $questionIds); - return $serializerParams; + $columns_param = Request::input("columns", ""); + $columns = []; + if (!empty($columns_param)) + $columns = explode(',', $columns_param); + $diff = array_diff($columns, $allowed_columns); + if (count($diff) > 0) { + throw new ValidationException(sprintf("columns %s are not allowed!", implode(",", $diff))); } - ); - }); + if (empty($columns)) + $columns = $allowed_columns; + return $columns; + }, + sprintf('tickets-%s-', $summit_id), + [ + 'features_types' => $summit->getBadgeFeaturesTypes(), + 'ticket_questions' => $questions + ], + null, + function($data, $serializerParams) use($questions){ + + $owners = []; + foreach ($data->getItems() as $t){ + if ($t->hasOwner()) $owners[] = $t->getOwner()->getId(); + } + $questionIds = []; + foreach ($questions as $q) { + $questionIds[] = $q->getId(); + } + $questionIds = array_values(array_unique($questionIds)); + $owners = array_values(array_unique($owners)); + + $serializerParams['answers_by_owner'] = $this->attendee_repository->getExtraQuestionAnswersByOwners($owners, $questionIds); + return $serializerParams; + } + ); } /** diff --git a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php index 9629a92fd..a2b303fdc 100644 --- a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php +++ b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php @@ -781,4 +781,4 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord ); } -} +} \ No newline at end of file From b66fc08f325b40757202e00947bc7e68e5b78f7a Mon Sep 17 00:00:00 2001 From: smarcet Date: Tue, 30 Sep 2025 13:28:05 -0300 Subject: [PATCH 2/7] chore: refactor DoctrineSummitEventRepository to suppport 2 phase paging chore: fix .gitmessage.txt chore: increase header size to 150 --- .../Summit/DoctrineSummitAttendeeTicketRepository.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php index a2b303fdc..e4dd31ade 100644 --- a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php +++ b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php @@ -745,6 +745,7 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord $start = time(); Log::debug(sprintf('DoctrineSummitAttendeeTicketRepository::getAllByPage')); $total = $this->getFastCount($filter, $order); + if(!$total) return new PagingResponse(0, $paging_info->getPerPage(), $paging_info->getCurrentPage(), 0, []); $ids = $this->getAllIdsByPage($paging_info, $filter, $order); $query = $this->getEntityManager()->createQueryBuilder() ->select('e, a, o, tt, pc, b, bt, a_c, m') @@ -764,6 +765,10 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord $byId = []; foreach ($rows as $e) $byId[$e->getId()] = $e; + $rows = $query->getQuery()->getResult(); + $byId = []; + foreach ($rows as $e) $byId[$e->getId()] = $e; + $data = []; foreach ($ids as $id) { if (isset($byId[$id])) $data[] = $byId[$id]; From a52b555268b0abaae5dcdb8ef7277d6085396acd Mon Sep 17 00:00:00 2001 From: Matias Perrone Date: Tue, 7 Oct 2025 18:48:24 -0300 Subject: [PATCH 3/7] feat: Add OpenAPI documentation to "getAll" method - Add controller's response to OpenAPI schema --- .../Main/OAuth2ChunkedFilesApiController.php | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php b/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php index b30e80ced..a3a500f6f 100644 --- a/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php +++ b/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php @@ -12,8 +12,10 @@ * limitations under the License. **/ use Illuminate\Http\JsonResponse; +use Illuminate\Http\Response; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; +use OpenApi\Attributes as OA; use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException; use Pion\Laravel\ChunkUpload\Handler\AbstractHandler; use Pion\Laravel\ChunkUpload\Receiver\FileReceiver; @@ -33,6 +35,63 @@ class OAuth2ChunkedFilesApiController extends UploadController * @throws UploadMissingFileException * */ + #[OA\Post( + path: "/api/public/v1/files/upload", + description: "Upload files using chunked upload mechanism. Supports large file uploads by splitting them into smaller chunks. The endpoint handles both complete uploads and chunked progress updates.", + summary: 'Upload file with chunked upload support', + operationId: 'uploadChunkedFile', + tags: ['Files'], + requestBody: new OA\RequestBody( + required: true, + content: new OA\MediaType( + mediaType: 'multipart/form-data', + schema: new OA\Schema( + required: ['file'], + properties: [ + new OA\Property( + property: 'file', + type: 'string', + format: 'binary', + description: 'File to upload (can be a chunk of a larger file)' + ), + new OA\Property( + property: 'resumableChunkNumber', + type: 'integer', + description: 'Current chunk number (for resumable.js library)', + example: 1 + ), + new OA\Property( + property: 'resumableTotalChunks', + type: 'integer', + description: 'Total number of chunks (for resumable.js library)', + example: 5 + ), + new OA\Property( + property: 'resumableIdentifier', + type: 'string', + description: 'Unique identifier for the file upload session (for resumable.js library)', + example: '12345-myfile-jpg' + ), + ] + ) + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Success - Upload in progress (chunk uploaded)', + content: new OA\JsonContent(ref: '#/components/schemas/ChunkedFileUploadProgressResponse') + ), + new OA\Response( + response: 201, + description: 'Success - Upload complete (all chunks received)', + content: new OA\JsonContent(ref: '#/components/schemas/ChunkedFileUploadCompleteResponse') + ), + new OA\Response(response: Response::HTTP_BAD_REQUEST, description: "Bad Request - Invalid file or missing parameters"), + new OA\Response(response: Response::HTTP_UNPROCESSABLE_ENTITY, description: "Unprocessable Entity - Upload missing file exception"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error - Upload failed") + ] + )] public function uploadFile(FileReceiver $receiver) { // check if the upload is success, throw exception or return response you need @@ -57,4 +116,4 @@ public function uploadFile(FileReceiver $receiver) ]); } -} \ No newline at end of file +} From ccf372abbbd8667a72e364f8065ecfd0c193e071 Mon Sep 17 00:00:00 2001 From: Matias Perrone Date: Tue, 7 Oct 2025 18:49:03 -0300 Subject: [PATCH 4/7] chore: Add schemas to the main file --- app/Swagger/schemas.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/Swagger/schemas.php b/app/Swagger/schemas.php index 1eff77226..d4310f15b 100644 --- a/app/Swagger/schemas.php +++ b/app/Swagger/schemas.php @@ -351,3 +351,23 @@ class RSVPUpdateRequestSchema_{ ] )] class RSVPAdminAddRequestSchema {} + +#[OA\Schema( + schema: 'ChunkedFileUploadProgressResponse', + type: 'object', + properties: [ + new OA\Property(property: 'done', type: 'number', format: 'float', example: 45.5, description: 'Upload progress percentage (0-100)'), + ] +)] +class ChunkedFileUploadProgressResponseSchema {} + +#[OA\Schema( + schema: 'ChunkedFileUploadCompleteResponse', + type: 'object', + properties: [ + new OA\Property(property: 'path', type: 'string', example: 'upload/image-jpeg/2025-09-30/', description: 'Directory path where the file was saved'), + new OA\Property(property: 'name', type: 'string', example: 'myfile_abc123def456.jpg', description: 'Generated filename with timestamp hash'), + new OA\Property(property: 'mime_type', type: 'string', example: 'image-jpeg', description: 'MIME type of the uploaded file (slashes replaced with hyphens)'), + ] +)] +class ChunkedFileUploadCompleteResponseSchema {} From fa895d52cdcdf17f6c3ce7e07c5c8322fcef9840 Mon Sep 17 00:00:00 2001 From: Matias Perrone Date: Wed, 8 Oct 2025 16:56:20 -0300 Subject: [PATCH 5/7] fix: conflicts issue with main --- .../Main/OAuth2ChunkedFilesApiController.php | 2 +- .../OAuth2SummitTicketApiController.php | 371 +++++++++--------- ...DoctrineSummitAttendeeTicketRepository.php | 5 - 3 files changed, 187 insertions(+), 191 deletions(-) diff --git a/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php b/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php index a3a500f6f..e5012c30e 100644 --- a/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php +++ b/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php @@ -116,4 +116,4 @@ public function uploadFile(FileReceiver $receiver) ]); } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitTicketApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitTicketApiController.php index 8d7f07036..9ee2fb37f 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitTicketApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitTicketApiController.php @@ -28,7 +28,6 @@ use models\oauth2\IResourceServerContext; use models\summit\IOrderConstants; use models\summit\ISummitAttendeeRepository; -use models\summit\ISummitAttendeeRepository; use models\summit\ISummitAttendeeTicketRepository; use models\summit\ISummitRepository; use models\summit\Summit; @@ -290,156 +289,157 @@ function () { */ public function getAllBySummitCSV($summit_id) { - $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->getResourceServerContext())->find($summit_id); - if (is_null($summit)) return $this->error404(); - $questions = $summit->getOrderExtraQuestionsByUsage(SummitOrderExtraQuestionTypeConstants::TicketQuestionUsage); - return $this->_getAllCSV( - function () { - return [ - 'id' => ['=='], - 'not_id' => ['=='], - 'number' => ['@@', '=@', '=='], - 'order_number' => ['@@', '=@', '=='], - 'owner_name' => ['@@', '=@', '=='], - 'owner_first_name' => ['@@', '=@', '=='], - 'owner_last_name' => ['@@', '=@', '=='], - 'owner_email' => ['@@', '=@', '=='], - 'owner_company' => ['@@', '=@', '=='], - 'summit_id' => ['=='], - 'owner_id' => ['=='], - 'order_id' => ['=='], - 'status' => ['==', '<>'], - 'is_active' => ['=='], - 'has_requested_refund_requests' => ['=='], - 'access_level_type_name' => ['=='], - 'access_level_type_id' => ['=='], - 'ticket_type_id' => ['=='], - 'has_owner' => ['=='], - 'owner_status' => ['=='], - 'has_badge' => ['=='], - 'view_type_id' => ['=='], - 'promo_code_id' => ['=='], - 'promo_code' => ['==', '@@', '=@'], - 'promo_code_tag' => ['==', '@@', '=@'], - 'promo_code_tag_id' => ['=='], - 'promo_code_description' => ['@@', '=@'], - 'final_amount' => ['==', '<>', '>=', '>'], - 'is_printable' => ['=='], - 'badge_type_id' => ['=='], - 'has_badge_prints' => ['=='], - 'badge_prints_count' => ['==', '>=', '<=', '>', '<'], - 'has_owner_company' => ['=='], - 'exclude_is_printable_free_unassigned' => ['=='], - ]; - }, - function () { - return [ - 'id' => 'sometimes|integer', - 'not_id' => 'sometimes|integer', - 'status' => sprintf('sometimes|in:%s', implode(',', IOrderConstants::ValidStatus)), - 'number' => 'sometimes|string', - 'order_number' => 'sometimes|string', - 'owner_name' => 'sometimes|string', - 'owner_first_name' => 'sometimes|string', - 'owner_last_name' => 'sometimes|string', - 'owner_email' => 'sometimes|string', - 'owner_company' => 'sometimes|string', - 'summit_id' => 'sometimes|integer', - 'owner_id' => 'sometimes|integer', - 'order_id' => 'sometimes|integer', - 'is_active' => 'sometimes|boolean', - 'has_requested_refund_requests' => 'sometimes|boolean', - 'access_level_type_name' => 'sometimes|string', - 'access_level_type_id' => 'sometimes|integer', - 'ticket_type_id' => 'sometimes|integer', - 'has_owner' => 'sometimes|boolean', - 'owner_status' => 'sometimes|string|in:' . implode(',', SummitAttendee::AllowedStatus), - 'has_badge' => 'sometimes|boolean', - 'view_type_id' => 'sometimes|integer', - 'promo_code_id' => 'sometimes|integer', - 'promo_code' => 'sometimes|string', - 'promo_code_tag' => 'sometimes|string', - 'promo_code_description' => 'sometimes|string', - 'promo_code_tag_id' => 'sometimes|integer', - 'final_amount' => 'sometimes|numeric', - 'is_printable' => ['sometimes', new Boolean()], - 'badge_type_id' => 'sometimes|integer', - 'has_badge_prints' => ['sometimes', new Boolean()], - 'badge_prints_count' => 'sometimes|integer', - 'has_owner_company' => ['sometimes', new Boolean()], - 'exclude_is_printable_free_unassigned' => ['sometimes', new Boolean()], - ]; - }, - function () { - return [ - 'id', - 'number', - 'status', - 'owner_name', - 'owner_first_name', - 'owner_last_name', - 'ticket_type', - 'final_amount', - 'owner_email', - 'owner_company', - 'promo_code', - 'bought_date', - 'refunded_amount', - 'final_amount_adjusted', - 'badge_type_id', - 'badge_type', - 'badge_prints_count', - 'created', - ]; - }, - function ($filter) use ($summit) { - if ($filter instanceof Filter) { - $filter->addFilterCondition(FilterElement::makeEqual('summit_id', $summit->getId())); - } - return $filter; - }, - function () { - return SerializerRegistry::SerializerType_CSV; - }, - function () use($summit) { - return [ - 'created' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), - 'last_edited' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), - 'purchase_date' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), - 'attendee_checked_in' => new BooleanCellFormatter(), - 'is_active' => new BooleanCellFormatter(), - ]; - }, - function () use ($summit) { - $allowed_columns = [ - 'id', - 'created', - 'last_edited', - 'number', - 'status', - 'attendee_id', - 'attendee_first_name', - 'attendee_last_name', - 'attendee_email', - 'attendee_company', - 'external_order_id', - 'external_attendee_id', - 'purchase_date', - 'ticket_type_id', - 'ticket_type_name', - 'order_id', - 'badge_id', - 'promo_code_id', - 'promo_code', - 'raw_cost', - 'final_amount', - 'discount', - 'refunded_amount', - 'currency', - 'badge_type_id', - 'badge_type_name', - 'promo_code_tags', - ]; + return $this->withReplica(function() use ($summit_id) { + $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->getResourceServerContext())->find($summit_id); + if (is_null($summit)) return $this->error404(); + $questions = $summit->getOrderExtraQuestionsByUsage(SummitOrderExtraQuestionTypeConstants::TicketQuestionUsage); + return $this->_getAllCSV( + function () { + return [ + 'id' => ['=='], + 'not_id' => ['=='], + 'number' => ['@@', '=@', '=='], + 'order_number' => ['@@', '=@', '=='], + 'owner_name' => ['@@', '=@', '=='], + 'owner_first_name' => ['@@', '=@', '=='], + 'owner_last_name' => ['@@', '=@', '=='], + 'owner_email' => ['@@', '=@', '=='], + 'owner_company' => ['@@', '=@', '=='], + 'summit_id' => ['=='], + 'owner_id' => ['=='], + 'order_id' => ['=='], + 'status' => ['==', '<>'], + 'is_active' => ['=='], + 'has_requested_refund_requests' => ['=='], + 'access_level_type_name' => ['=='], + 'access_level_type_id' => ['=='], + 'ticket_type_id' => ['=='], + 'has_owner' => ['=='], + 'owner_status' => ['=='], + 'has_badge' => ['=='], + 'view_type_id' => ['=='], + 'promo_code_id' => ['=='], + 'promo_code' => ['==', '@@', '=@'], + 'promo_code_tag' => ['==', '@@', '=@'], + 'promo_code_tag_id' => ['=='], + 'promo_code_description' => ['@@', '=@'], + 'final_amount' => ['==', '<>', '>=', '>'], + 'is_printable' => ['=='], + 'badge_type_id' => ['=='], + 'has_badge_prints' => ['=='], + 'badge_prints_count' => ['==', '>=', '<=', '>', '<'], + 'has_owner_company' => ['=='], + 'exclude_is_printable_free_unassigned' => ['=='], + ]; + }, + function () { + return [ + 'id' => 'sometimes|integer', + 'not_id' => 'sometimes|integer', + 'status' => sprintf('sometimes|in:%s', implode(',', IOrderConstants::ValidStatus)), + 'number' => 'sometimes|string', + 'order_number' => 'sometimes|string', + 'owner_name' => 'sometimes|string', + 'owner_first_name' => 'sometimes|string', + 'owner_last_name' => 'sometimes|string', + 'owner_email' => 'sometimes|string', + 'owner_company' => 'sometimes|string', + 'summit_id' => 'sometimes|integer', + 'owner_id' => 'sometimes|integer', + 'order_id' => 'sometimes|integer', + 'is_active' => 'sometimes|boolean', + 'has_requested_refund_requests' => 'sometimes|boolean', + 'access_level_type_name' => 'sometimes|string', + 'access_level_type_id' => 'sometimes|integer', + 'ticket_type_id' => 'sometimes|integer', + 'has_owner' => 'sometimes|boolean', + 'owner_status' => 'sometimes|string|in:' . implode(',', SummitAttendee::AllowedStatus), + 'has_badge' => 'sometimes|boolean', + 'view_type_id' => 'sometimes|integer', + 'promo_code_id' => 'sometimes|integer', + 'promo_code' => 'sometimes|string', + 'promo_code_tag' => 'sometimes|string', + 'promo_code_description' => 'sometimes|string', + 'promo_code_tag_id' => 'sometimes|integer', + 'final_amount' => 'sometimes|numeric', + 'is_printable' => ['sometimes', new Boolean()], + 'badge_type_id' => 'sometimes|integer', + 'has_badge_prints' => ['sometimes', new Boolean()], + 'badge_prints_count' => 'sometimes|integer', + 'has_owner_company' => ['sometimes', new Boolean()], + 'exclude_is_printable_free_unassigned' => ['sometimes', new Boolean()], + ]; + }, + function () { + return [ + 'id', + 'number', + 'status', + 'owner_name', + 'owner_first_name', + 'owner_last_name', + 'ticket_type', + 'final_amount', + 'owner_email', + 'owner_company', + 'promo_code', + 'bought_date', + 'refunded_amount', + 'final_amount_adjusted', + 'badge_type_id', + 'badge_type', + 'badge_prints_count', + 'created', + ]; + }, + function ($filter) use ($summit) { + if ($filter instanceof Filter) { + $filter->addFilterCondition(FilterElement::makeEqual('summit_id', $summit->getId())); + } + return $filter; + }, + function () { + return SerializerRegistry::SerializerType_CSV; + }, + function () use($summit) { + return [ + 'created' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), + 'last_edited' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), + 'purchase_date' => new EpochCellFormatter(EpochCellFormatter::DefaultFormat, $summit->getTimeZone()), + 'attendee_checked_in' => new BooleanCellFormatter(), + 'is_active' => new BooleanCellFormatter(), + ]; + }, + function () use ($summit) { + $allowed_columns = [ + 'id', + 'created', + 'last_edited', + 'number', + 'status', + 'attendee_id', + 'attendee_first_name', + 'attendee_last_name', + 'attendee_email', + 'attendee_company', + 'external_order_id', + 'external_attendee_id', + 'purchase_date', + 'ticket_type_id', + 'ticket_type_name', + 'order_id', + 'badge_id', + 'promo_code_id', + 'promo_code', + 'raw_cost', + 'final_amount', + 'discount', + 'refunded_amount', + 'currency', + 'badge_type_id', + 'badge_type_name', + 'promo_code_tags', + ]; foreach ($summit->getBadgeFeaturesTypes() as $featuresType) { $allowed_columns[] = $featuresType->getName(); @@ -449,41 +449,42 @@ function () use ($summit) { $allowed_columns[] = $question->getLabel(); } - $columns_param = Request::input("columns", ""); - $columns = []; - if (!empty($columns_param)) - $columns = explode(',', $columns_param); - $diff = array_diff($columns, $allowed_columns); - if (count($diff) > 0) { - throw new ValidationException(sprintf("columns %s are not allowed!", implode(",", $diff))); - } - if (empty($columns)) - $columns = $allowed_columns; - return $columns; - }, - sprintf('tickets-%s-', $summit_id), - [ - 'features_types' => $summit->getBadgeFeaturesTypes(), - 'ticket_questions' => $questions - ], - null, - function($data, $serializerParams) use($questions){ - - $owners = []; - foreach ($data->getItems() as $t){ - if ($t->hasOwner()) $owners[] = $t->getOwner()->getId(); - } - $questionIds = []; - foreach ($questions as $q) { - $questionIds[] = $q->getId(); - } - $questionIds = array_values(array_unique($questionIds)); - $owners = array_values(array_unique($owners)); + $columns_param = Request::input("columns", ""); + $columns = []; + if (!empty($columns_param)) + $columns = explode(',', $columns_param); + $diff = array_diff($columns, $allowed_columns); + if (count($diff) > 0) { + throw new ValidationException(sprintf("columns %s are not allowed!", implode(",", $diff))); + } + if (empty($columns)) + $columns = $allowed_columns; + return $columns; + }, + sprintf('tickets-%s-', $summit_id), + [ + 'features_types' => $summit->getBadgeFeaturesTypes(), + 'ticket_questions' => $questions + ], + null, + function($data, $serializerParams) use($questions){ + + $owners = []; + foreach ($data->getItems() as $t){ + if ($t->hasOwner()) $owners[] = $t->getOwner()->getId(); + } + $questionIds = []; + foreach ($questions as $q) { + $questionIds[] = $q->getId(); + } + $questionIds = array_values(array_unique($questionIds)); + $owners = array_values(array_unique($owners)); - $serializerParams['answers_by_owner'] = $this->attendee_repository->getExtraQuestionAnswersByOwners($owners, $questionIds); - return $serializerParams; - } - ); + $serializerParams['answers_by_owner'] = $this->attendee_repository->getExtraQuestionAnswersByOwners($owners, $questionIds); + return $serializerParams; + } + ); + }); } /** diff --git a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php index e4dd31ade..a2b303fdc 100644 --- a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php +++ b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php @@ -745,7 +745,6 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord $start = time(); Log::debug(sprintf('DoctrineSummitAttendeeTicketRepository::getAllByPage')); $total = $this->getFastCount($filter, $order); - if(!$total) return new PagingResponse(0, $paging_info->getPerPage(), $paging_info->getCurrentPage(), 0, []); $ids = $this->getAllIdsByPage($paging_info, $filter, $order); $query = $this->getEntityManager()->createQueryBuilder() ->select('e, a, o, tt, pc, b, bt, a_c, m') @@ -765,10 +764,6 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord $byId = []; foreach ($rows as $e) $byId[$e->getId()] = $e; - $rows = $query->getQuery()->getResult(); - $byId = []; - foreach ($rows as $e) $byId[$e->getId()] = $e; - $data = []; foreach ($ids as $id) { if (isset($byId[$id])) $data[] = $byId[$id]; From e78c2c25c8d53d79a71e78240d2f4d22120739e8 Mon Sep 17 00:00:00 2001 From: Matias Perrone Date: Tue, 14 Oct 2025 14:05:02 -0300 Subject: [PATCH 6/7] chore: Revert to main version due to conflicts with rebase --- .../Summit/DoctrineSummitAttendeeTicketRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php index a2b303fdc..9629a92fd 100644 --- a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php +++ b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php @@ -781,4 +781,4 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord ); } -} \ No newline at end of file +} From 23ac9fe81dd3ee82a713ee968b3cc8df28aaec88 Mon Sep 17 00:00:00 2001 From: Matias Perrone Date: Fri, 7 Nov 2025 18:09:41 +0000 Subject: [PATCH 7/7] chore: Create a schema for request body validation --- .../Main/OAuth2ChunkedFilesApiController.php | 35 +++---------------- app/Swagger/schemas.php | 33 +++++++++++++++++ 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php b/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php index e5012c30e..d30391b89 100644 --- a/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php +++ b/app/Http/Controllers/Apis/Protected/Main/OAuth2ChunkedFilesApiController.php @@ -1,4 +1,5 @@ -