Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];
Comment on lines +36 to +44

@coderabbitai coderabbitai Bot Jun 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate each bulk element as an array before nested wildcard checks.

Line 39 validates nested keys, but each top-level element is not required to be an array. A malformed object-shaped payload can bypass validation and then fail at Line 1289 in app/Services/Model/Imp/SummitSponsorService.php when $item['sponsor_id'] is accessed, producing a 500 instead of a 412 validation response.

Suggested fix
     public static function buildForBulkUpdate(array $payload = []): array
     {
         return [
+            '*'               => 'required|array',
             '*.sponsor_id'    => 'required|integer',
             '*.forms_qty'     => 'sometimes|integer',
             '*.purchases_qty' => 'sometimes|integer',
             '*.pages_qty'     => 'sometimes|integer',
             '*.documents_qty' => 'sometimes|integer',
         ];
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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',
];
public static function buildForBulkUpdate(array $payload = []): array
{
return [
'*' => 'required|array',
'*.sponsor_id' => 'required|integer',
'*.forms_qty' => 'sometimes|integer',
'*.purchases_qty' => 'sometimes|integer',
'*.pages_qty' => 'sometimes|integer',
'*.documents_qty' => 'sometimes|integer',
];
}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 36-36: Avoid unused parameters such as '$payload'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/Http/Controllers/Apis/Protected/Summit/Factories/SponsorServicesStatisticsValidationRulesFactory.php`
around lines 36 - 44, The buildForBulkUpdate validation allows nested keys but
doesn't require each top-level bulk item to be an array; add a rule forcing each
element to be an array so malformed non-array items fail validation. In
buildForBulkUpdate (method buildForBulkUpdate) add a rule '*' => 'array' (or
'*'=> ['array']) before the '*.sponsor_id' / '*.forms_qty' etc. entries so each
bulk element is validated as an array prior to the nested wildcard checks.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romanetar please review

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
}
}
8 changes: 8 additions & 0 deletions app/Services/Model/ISummitSponsorService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
23 changes: 23 additions & 0 deletions app/Services/Model/Imp/SummitSponsorService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
}
}
81 changes: 81 additions & 0 deletions database/migrations/config/Version20260611155110.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php namespace Database\Migrations\Config;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use App\Models\Foundation\Main\IGroup;
use App\Security\SummitScopes;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Migration to seed the bulk-update-sponsor-services-statistics endpoint.
*
* Adds:
* - 1 api_endpoints row (bulk-update-sponsor-services-statistics)
* - 1 endpoint_api_scopes association (WriteSummitData)
* - 3 endpoint_api_authz_groups rows (SuperAdmins, Administrators, SummitAdministrators)
*
* All INSERTs are idempotent via WHERE NOT EXISTS.
*/
final class Version20260611155110 extends AbstractMigration
{
use APIEndpointsMigrationHelper;

private const API_NAME = 'summits';
private const ENDPOINT_NAME = 'bulk-update-sponsor-services-statistics';
private const ENDPOINT_ROUTE = '/api/v1/summits/{id}/sponsors/all/sponsorservices-statistics/bulk';

private const AUTHZ_GROUPS = [
IGroup::SuperAdmins,
IGroup::Administrators,
IGroup::SummitAdministrators,
];

public function getDescription(): string
{
return 'Seed bulk-update-sponsor-services-statistics endpoint with scope and authz groups.';
}

/**
* @param Schema $schema
*/
public function up(Schema $schema): void
{
$this->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));
}
}
13 changes: 13 additions & 0 deletions database/seeders/ApiEndpointsSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions routes/api_v1.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {

Expand Down
71 changes: 71 additions & 0 deletions tests/oauth2/OAuth2SummitSponsorApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment on lines +1703 to +1729

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Address unused variable and verify error response structure.

The $response variable is assigned but never used. Additionally, the test only verifies the HTTP 412 status but doesn't validate the error response structure. Based on the error412 helper pattern, the response should contain message, errors, and code fields.

🧪 Proposed fix
     $response = $this->action(
         "PUT",
         "OAuth2SummitSponsorApiController@bulkUpdateSponsorServicesStatistics",
         $params,
         [],
         [],
         [],
         $this->getAuthHeaders(),
         json_encode($data)
     );

+    $content = $response->getContent();
     $this->assertResponseStatus(412);
+    $error = json_decode($content);
+    $this->assertNotNull($error);
+    $this->assertEquals('Validation Failed', $error->message);
+    $this->assertNotEmpty($error->errors);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
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)
);
$content = $response->getContent();
$this->assertResponseStatus(412);
$error = json_decode($content);
$this->assertNotNull($error);
$this->assertEquals('Validation Failed', $error->message);
$this->assertNotEmpty($error->errors);
}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 1717-1717: Avoid unused local variables such as '$response'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/oauth2/OAuth2SummitSponsorApiTest.php` around lines 1703 - 1729, The
testBulkUpdateSponsorServicesStatisticsValidationError assigns $response but
never uses it and only asserts HTTP 412; update the test to consume the response
from the action call and assert the error412 response structure (ensure the JSON
contains top-level keys "message", "errors", and "code" per the error412 helper
pattern) instead of leaving $response unused—locate the action invocation to
OAuth2SummitSponsorApiController@bulkUpdateSponsorServicesStatistics and add
assertions that decode the response body and verify those fields.

}
Loading