diff --git a/app/Http/Controllers/Apis/Protected/Summit/Factories/SponsorServicesStatisticsValidationRulesFactory.php b/app/Http/Controllers/Apis/Protected/Summit/Factories/SponsorServicesStatisticsValidationRulesFactory.php index 6e193563e..897584199 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/Factories/SponsorServicesStatisticsValidationRulesFactory.php +++ b/app/Http/Controllers/Apis/Protected/Summit/Factories/SponsorServicesStatisticsValidationRulesFactory.php @@ -32,4 +32,15 @@ public static function buildForUpdate(array $payload = []): array { return self::buildForAdd(); } + + public static function buildForBulkUpdate(array $payload = []): array + { + return [ + '*.sponsor_id' => 'required|integer', + '*.forms_qty' => 'sometimes|integer', + '*.purchases_qty' => 'sometimes|integer', + '*.pages_qty' => 'sometimes|integer', + '*.documents_qty' => 'sometimes|integer', + ]; + } } diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php index 46aeb0506..6bad79594 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php @@ -4378,4 +4378,73 @@ public function updateSponsorServicesStatistics($summit_id, $sponsor_id) { ); }); } + + #[OA\Put( + path: "/api/v1/summits/{id}/sponsors/all/sponsorservices-statistics/bulk", + description: "required-groups " . IGroup::SuperAdmins . ", " . IGroup::Administrators . ", " . IGroup::SummitAdministrators, + summary: 'Bulk upsert Sponsor Services Statistics', + operationId: 'bulkUpdateSponsorServicesStatistics', + tags: ['Sponsors'], + x: [ + 'required-groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + ] + ], + security: [ + [ + 'summit_sponsor_oauth2' => [ + SummitScopes::WriteSummitData, + ] + ] + ], + parameters: [ + new OA\Parameter( + name: 'id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer'), + description: 'The summit id' + ), + ], + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + type: 'array', + items: new OA\Items(ref: "#/components/schemas/SponsorServicesStatisticsUpsertRequest") + ) + ), + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: 'Sponsor Services Statistics bulk created/updated successfully' + ), + new OA\Response(response: Response::HTTP_UNAUTHORIZED, description: "Unauthorized"), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: "Not Found"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + new OA\Response(response: Response::HTTP_PRECONDITION_FAILED, description: "Validation Error") + ] + )] + public function bulkUpdateSponsorServicesStatistics($summit_id) { + return $this->processRequest(function () use ($summit_id) { + + $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->resource_server_context)->find($summit_id); + if (is_null($summit)) return $this->error404(); + + $payload = $this->getJsonPayload(SponsorServicesStatisticsValidationRulesFactory::buildForBulkUpdate(), true); + + $results = $this->service->bulkUpdateSponsorServicesStatistics($summit, $payload); + + return $this->ok(array_map(function ($statistics) { + return SerializerRegistry::getInstance() + ->getSerializer($statistics) + ->serialize( + SerializerUtils::getExpand(), + SerializerUtils::getFields(), + SerializerUtils::getRelations() + ); + }, $results)); + }); + } } diff --git a/app/Services/Model/ISummitSponsorService.php b/app/Services/Model/ISummitSponsorService.php index 1e78e344e..c64d0b16a 100644 --- a/app/Services/Model/ISummitSponsorService.php +++ b/app/Services/Model/ISummitSponsorService.php @@ -336,4 +336,12 @@ public function updateLeadReportSettings(Summit $summit, int $sponsor_id, array * @throws \Exception */ public function updateSponsorServicesStatistics(Summit $summit, int $sponsor_id, array $payload): SponsorStatistics; + + /** + * @param Summit $summit + * @param array $payload + * @return SponsorStatistics[] + * @throws \Exception + */ + public function bulkUpdateSponsorServicesStatistics(Summit $summit, array $payload): array; } diff --git a/app/Services/Model/Imp/SummitSponsorService.php b/app/Services/Model/Imp/SummitSponsorService.php index 0f5b00077..1bcb0d156 100644 --- a/app/Services/Model/Imp/SummitSponsorService.php +++ b/app/Services/Model/Imp/SummitSponsorService.php @@ -1280,4 +1280,27 @@ public function updateSponsorServicesStatistics(Summit $summit, int $sponsor_id, return SponsorServicesStatisticsFactory::populate($statistics, $payload); }); } + + public function bulkUpdateSponsorServicesStatistics(Summit $summit, array $payload): array + { + return $this->tx_service->transaction(function () use ($summit, $payload) { + $results = []; + foreach ($payload as $item) { + $sponsor_id = intval($item['sponsor_id']); + $summit_sponsor = $summit->getSummitSponsorById($sponsor_id); + if (is_null($summit_sponsor)) + throw new EntityNotFoundException(sprintf("Sponsor %d not found.", $sponsor_id)); + + $statistics = $summit_sponsor->getSponsorServicesStatistics(); + if (!$statistics) { + $statistics = SponsorServicesStatisticsFactory::build($item); + $summit_sponsor->setSponsorServicesStatistics($statistics); + } else { + SponsorServicesStatisticsFactory::populate($statistics, $item); + } + $results[] = $statistics; + } + return $results; + }); + } } diff --git a/database/migrations/config/Version20260611155110.php b/database/migrations/config/Version20260611155110.php new file mode 100644 index 000000000..ababf4487 --- /dev/null +++ b/database/migrations/config/Version20260611155110.php @@ -0,0 +1,81 @@ +addSql($this->insertEndpoint( + self::API_NAME, + self::ENDPOINT_NAME, + self::ENDPOINT_ROUTE, + 'PUT' + )); + + $this->addSql($this->insertEndpointScope(self::API_NAME, self::ENDPOINT_NAME, SummitScopes::WriteSummitData)); + + foreach (self::AUTHZ_GROUPS as $groupSlug) { + $this->addSql($this->insertEndpointAuthzGroup(self::API_NAME, self::ENDPOINT_NAME, $groupSlug)); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema): void + { + foreach (self::AUTHZ_GROUPS as $groupSlug) { + $this->addSql($this->deleteEndpointAuthzGroup(self::API_NAME, self::ENDPOINT_NAME, $groupSlug)); + } + + $this->addSql($this->deleteScopesEndpoints(self::API_NAME, [SummitScopes::WriteSummitData])); + + $this->addSql($this->deleteEndpoint(self::API_NAME, self::ENDPOINT_NAME)); + } +} diff --git a/database/seeders/ApiEndpointsSeeder.php b/database/seeders/ApiEndpointsSeeder.php index fb94f015d..460605129 100644 --- a/database/seeders/ApiEndpointsSeeder.php +++ b/database/seeders/ApiEndpointsSeeder.php @@ -2826,6 +2826,19 @@ private function seedSummitEndpoints() IGroup::SummitAdministrators, ] ], + [ + 'name' => 'bulk-update-sponsor-services-statistics', + 'route' => '/api/v1/summits/{id}/sponsors/all/sponsorservices-statistics/bulk', + 'http_method' => 'PUT', + 'scopes' => [ + SummitScopes::WriteSummitData, + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + ] + ], // Add-On types [ 'name' => 'get-add-ons-metadata', diff --git a/routes/api_v1.php b/routes/api_v1.php index fad69a60c..ad61e4e30 100644 --- a/routes/api_v1.php +++ b/routes/api_v1.php @@ -1227,6 +1227,10 @@ Route::put('send', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitPromoCodesApiController@sendSponsorPromoCodes']); }); }); + + Route::group(['prefix' => 'sponsorservices-statistics'], function () { + Route::put('bulk', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitSponsorApiController@bulkUpdateSponsorServicesStatistics']); + }); }); Route::group(['prefix' => '{sponsor_id}'], function () { diff --git a/tests/oauth2/OAuth2SummitSponsorApiTest.php b/tests/oauth2/OAuth2SummitSponsorApiTest.php index 45fdf2f80..374c2de7c 100644 --- a/tests/oauth2/OAuth2SummitSponsorApiTest.php +++ b/tests/oauth2/OAuth2SummitSponsorApiTest.php @@ -1656,4 +1656,75 @@ public function testGetAllSponsorsBySummitPublic(){ $this->assertNotNull($page); $this->assertGreaterThan(0, $page->total); } + + // ---- Bulk Sponsor Services Statistics ---- + + public function testBulkUpdateSponsorServicesStatistics() + { + $params = [ + 'id' => self::$summit->getId(), + ]; + + // sponsors[0] already has statistics (even index), sponsors[1] does not (odd index) + $data = [ + [ + 'sponsor_id' => self::$sponsors[0]->getId(), + 'forms_qty' => 100, + 'purchases_qty'=> 200, + 'pages_qty' => 300, + 'documents_qty'=> 400, + ], + [ + 'sponsor_id' => self::$sponsors[1]->getId(), + 'forms_qty' => 10, + ], + ]; + + $response = $this->action( + "PUT", + "OAuth2SummitSponsorApiController@bulkUpdateSponsorServicesStatistics", + $params, + [], + [], + [], + $this->getAuthHeaders(), + json_encode($data) + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $results = json_decode($content); + $this->assertIsArray($results); + $this->assertCount(2, $results); + $this->assertEquals(100, $results[0]->forms_qty); + $this->assertEquals(10, $results[1]->forms_qty); + } + + public function testBulkUpdateSponsorServicesStatisticsValidationError() + { + $params = [ + 'id' => self::$summit->getId(), + ]; + + // forms_qty is a string instead of integer — should fail validation + $data = [ + [ + 'sponsor_id' => self::$sponsors[0]->getId(), + 'forms_qty' => 'not-an-integer', + ], + ]; + + $response = $this->action( + "PUT", + "OAuth2SummitSponsorApiController@bulkUpdateSponsorServicesStatistics", + $params, + [], + [], + [], + $this->getAuthHeaders(), + json_encode($data) + ); + + $this->assertResponseStatus(412); + } }