From 7e442262cea4678847f12360f40d1fbc83d052cf Mon Sep 17 00:00:00 2001 From: romanetar Date: Thu, 8 Oct 2026 15:19:07 +0200 Subject: [PATCH 1/4] fix(sponsors): never reject badge scan uploads over mandatory questions and block deleting extra questions A device can hold answers the server has not seen yet, so the server must not judge uploads against the current question set, and must not delete questions or options a device may still be using. addBadgeScan, the retry merge and updateBadgeScan threw a 412 "You neglected to fill in all mandatory questions" when an answer was missing. In addBadgeScan this happened inside the transaction that creates the scan, so a scan captured offline and answered before reconnecting was lost entirely. They now log a warning and keep whatever answers were sent; "mandatory" is an instruction to the device, not a server rule. deleteSponsorExtraQuestion and deleteExtraQuestionValue now always refuse with a 412, whether or not the server has answers. This is an interim measure until archiving replaces delete. Sponsor::MaxExtraQuestionCount stays at 5, so until then a question can no longer be removed to free a slot. The sponsor API tests used DELETE to make room under that cap; they now remove the question on the model instead. Refs SUP-86bcez3pw --- .../Model/Imp/SponsorUserInfoGrantService.php | 8 +- .../Model/Imp/SummitSponsorService.php | 33 ++------ ...OAuth2SummitBadgeScanApiControllerTest.php | 76 +++++++++++++++++++ tests/oauth2/OAuth2SummitSponsorApiTest.php | 52 +++++-------- 4 files changed, 109 insertions(+), 60 deletions(-) diff --git a/app/Services/Model/Imp/SponsorUserInfoGrantService.php b/app/Services/Model/Imp/SponsorUserInfoGrantService.php index 82fc7865ec..525ab03f46 100644 --- a/app/Services/Model/Imp/SponsorUserInfoGrantService.php +++ b/app/Services/Model/Imp/SponsorUserInfoGrantService.php @@ -409,9 +409,11 @@ private function addBadgeScanLocked( $extra_questions = $data['extra_questions'] ?? []; if (count($extra_questions)) { + // "mandatory" is an instruction to the device, not a server rule: + // save whatever answers were sent and never roll back the scan. $res = $scan->hadCompletedExtraQuestions($extra_questions); if (!$res) { - throw new ValidationException("You neglected to fill in all mandatory questions for the badge scan."); + Log::warning("SponsorUserInfoGrantService::addBadgeScan badge scan was saved with missing mandatory answers."); } } @@ -487,7 +489,7 @@ private function mergeRetryIntoExistingScan(SponsorBadgeScan $existing, array $d $extra_questions = $data['extra_questions'] ?? []; if (count($extra_questions) && !$existing->hadCompletedExtraQuestions($extra_questions)) - throw new ValidationException("You neglected to fill in all mandatory questions for the badge scan."); + Log::warning("SponsorUserInfoGrantService::mergeRetryIntoExistingScan badge scan was saved with missing mandatory answers."); return $existing; } @@ -524,7 +526,7 @@ public function updateBadgeScan(Summit $summit, Member $current_member, int $sca if (count($extra_questions)) { $res = $scan->hadCompletedExtraQuestions($extra_questions); if (!$res) { - throw new ValidationException("You neglected to fill in all mandatory questions for the badge scan."); + Log::warning("SponsorUserInfoGrantService::updateBadgeScan badge scan was saved with missing mandatory answers."); } } diff --git a/app/Services/Model/Imp/SummitSponsorService.php b/app/Services/Model/Imp/SummitSponsorService.php index 282e1a1461..b464a0279d 100644 --- a/app/Services/Model/Imp/SummitSponsorService.php +++ b/app/Services/Model/Imp/SummitSponsorService.php @@ -1084,18 +1084,10 @@ public function updateSponsorExtraQuestion(Summit $summit, int $sponsor_id, int */ public function deleteSponsorExtraQuestion(Summit $summit, int $sponsor_id, int $extra_question_id): void { - $this->tx_service->transaction(function () use ($summit, $sponsor_id, $extra_question_id) { - $summit_sponsor = $summit->getSummitSponsorById($sponsor_id); - if (is_null($summit_sponsor)) - throw new EntityNotFoundException("Sponsor not found."); - - $extra_question = $summit_sponsor->getExtraQuestionById($extra_question_id); - - if(!$extra_question instanceof SummitSponsorExtraQuestionType) - throw new EntityNotFoundException("Sponsor extra question not found."); - - $summit_sponsor->removeExtraQuestion($extra_question); - }); + // Always refused: a device may hold answers for this question that the server has not seen yet. + throw new ValidationException( + "Questions can't be deleted once created, because devices may already be using them. You can edit the wording, turn off Mandatory, or contact support." + ); } /** @@ -1160,19 +1152,10 @@ public function updateExtraQuestionValue(Summit $summit, int $sponsor_id, int $q */ public function deleteExtraQuestionValue(Summit $summit, int $sponsor_id, int $question_id, int $value_id): void { - $this->tx_service->transaction(function () use ($summit, $sponsor_id, $question_id, $value_id) { - $summit_sponsor = $summit->getSummitSponsorById($sponsor_id); - if (is_null($summit_sponsor)) - throw new EntityNotFoundException("Sponsor not found."); - - $extra_question = $summit_sponsor->getExtraQuestionById($question_id); - - if(!$extra_question instanceof SummitSponsorExtraQuestionType) - throw new EntityNotFoundException("Sponsor extra question not found."); - - - parent::_deleteExtraQuestionValue($extra_question, $value_id); - }); + // Always refused: a device may hold answers for this option that the server has not seen yet. + throw new ValidationException( + "Answer options can't be deleted once created, because devices may already be using them. You can edit the wording, or contact support." + ); } /** diff --git a/tests/oauth2/OAuth2SummitBadgeScanApiControllerTest.php b/tests/oauth2/OAuth2SummitBadgeScanApiControllerTest.php index 4f761eafb1..7274313957 100644 --- a/tests/oauth2/OAuth2SummitBadgeScanApiControllerTest.php +++ b/tests/oauth2/OAuth2SummitBadgeScanApiControllerTest.php @@ -1493,4 +1493,80 @@ public function testAddBadgeScanUniqueViolationAppliesNotesToTheWinningScan(){ $this->assertEquals('hot lead, follow up', $this->reloadScan($winner_id)->getNotes(), "notes of the request that lost the race must be persisted on the winning scan"); } + + /** + * "Mandatory" is an instruction to the device, not a server rule: sets the + * sponsor's first two questions as mandatory and returns their ids. + * @return array + */ + private function makeFirstTwoSponsorQuestionsMandatory(): array + { + $sponsor = self::$summit->getSummitSponsors()[0]; + $ids = []; + foreach ([0, 1] as $idx) { + $question = $sponsor->getExtraQuestions()[$idx]; + if (!$question instanceof SummitSponsorExtraQuestionType) self::fail(); + $question->setMandatory(true); + $ids[] = $question->getId(); + } + self::$em->persist($sponsor); + self::$em->flush(); + return $ids; + } + + /** + * A scan captured offline and answered before reconnecting reaches the + * server as a first POST that carries answers. A mandatory question left + * unanswered must neither reject it nor roll the scan back. + */ + public function testAddBadgeScanWithMissingMandatoryAnswerIsSavedWithItsOtherAnswers(){ + [$answered_id, ] = $this->makeFirstTwoSponsorQuestionsMandatory(); + + $service = App::make(ISponsorUserInfoGrantService::class); + $scan = $service->addBadgeScan(self::$summit, self::$member, $this->badgeScanPayload([ + 'extra_questions' => [ + ['question_id' => $answered_id, 'answer' => 'None'], + ], + ])); + + $answers = $this->reloadScan($scan->getId())->getExtraQuestionAnswers(); + $this->assertCount(1, $answers); + $this->assertEquals($answered_id, $answers->first()->getQuestionId()); + } + + public function testAddBadgeScanRetryWithMissingMandatoryAnswerIsSaved(){ + [$answered_id, ] = $this->makeFirstTwoSponsorQuestionsMandatory(); + + $service = App::make(ISponsorUserInfoGrantService::class); + $first = $service->addBadgeScan(self::$summit, self::$member, $this->badgeScanPayload()); + $first_id = $first->getId(); + + $service->addBadgeScan(self::$summit, self::$member, $this->badgeScanPayload([ + 'extra_questions' => [ + ['question_id' => $answered_id, 'answer' => 'None'], + ], + ])); + + $answers = $this->reloadScan($first_id)->getExtraQuestionAnswers(); + $this->assertCount(1, $answers); + $this->assertEquals($answered_id, $answers->first()->getQuestionId()); + } + + public function testUpdateBadgeScanWithMissingMandatoryAnswerIsSaved(){ + [$answered_id, ] = $this->makeFirstTwoSponsorQuestionsMandatory(); + + $service = App::make(ISponsorUserInfoGrantService::class); + $scan = $service->addBadgeScan(self::$summit, self::$member, $this->badgeScanPayload()); + $scan_id = $scan->getId(); + + $service->updateBadgeScan(self::$summit, self::$member, $scan_id, [ + 'extra_questions' => [ + ['question_id' => $answered_id, 'answer' => 'None'], + ], + ]); + + $answers = $this->reloadScan($scan_id)->getExtraQuestionAnswers(); + $this->assertCount(1, $answers); + $this->assertEquals($answered_id, $answers->first()->getQuestionId()); + } } diff --git a/tests/oauth2/OAuth2SummitSponsorApiTest.php b/tests/oauth2/OAuth2SummitSponsorApiTest.php index 374c2de7c2..6fbf2aa0e2 100644 --- a/tests/oauth2/OAuth2SummitSponsorApiTest.php +++ b/tests/oauth2/OAuth2SummitSponsorApiTest.php @@ -522,23 +522,21 @@ public function testAddSponsorUserMember(){ return $sponsor; } + /** + * Sponsors are seeded with the maximum number of extra questions and the API + * no longer deletes them, so make room directly on the model. + * using last() keeps order numbering compact so new question order == count + */ + private function removeLastSponsorExtraQuestion(){ + $sponsor = self::$sponsors[0]; + $sponsor->removeExtraQuestion($sponsor->getExtraQuestions()->last()); + self::$em->persist($sponsor); + self::$em->flush(); + } + public function testAddSponsorExtraQuestions(){ - // remove the last extra question first (sponsors already have 5, the max) - // using last() keeps order numbering compact so new question order == count - $existingQuestion = self::$sponsors[0]->getExtraQuestions()->last(); - $this->action( - "DELETE", - "OAuth2SummitSponsorApiController@deleteExtraQuestion", - [ - 'id' => self::$summit->getId(), - 'sponsor_id' => self::$sponsors[0]->getId(), - 'extra_question_id' => $existingQuestion->getId() - ], - [], [], [], - $this->getAuthHeaders() - ); - $this->assertResponseStatus(204); + $this->removeLastSponsorExtraQuestion(); $params = [ 'id' => self::$summit->getId(), @@ -711,7 +709,9 @@ public function testDeleteSponsorExtraQuestionsBySponsor(){ $this->getAuthHeaders() ); - $this->assertResponseStatus(204); + // questions can never be deleted: devices may hold answers the server has not seen + $this->assertResponseStatus(412); + $this->assertStringContainsString("Questions can't be deleted once created", $response->getContent()); } public function testAddLeadReportSettings(){ @@ -1499,21 +1499,7 @@ public function testDeleteSocialNetwork(){ // ---- Extra Question Values CRUD ---- private function createComboBoxExtraQuestion(){ - // remove the last extra question first (sponsors already have 5, the max) - $existingQuestion = self::$sponsors[0]->getExtraQuestions()->last(); - $this->action( - "DELETE", - "OAuth2SummitSponsorApiController@deleteExtraQuestion", - [ - 'id' => self::$summit->getId(), - 'sponsor_id' => self::$sponsors[0]->getId(), - 'extra_question_id' => $existingQuestion->getId() - ], - [], [], [], - $this->getAuthHeaders() - ); - $this->assertResponseStatus(204); - + $this->removeLastSponsorExtraQuestion(); $params = [ 'id' => self::$summit->getId(), 'sponsor_id' => self::$sponsors[0]->getId(), @@ -1630,7 +1616,9 @@ public function testDeleteExtraQuestionValue(){ $this->getAuthHeaders() ); - $this->assertResponseStatus(204); + // answer options can never be deleted: devices may hold answers the server has not seen + $this->assertResponseStatus(412); + $this->assertStringContainsString("Answer options can't be deleted once created", $response->getContent()); } // ---- Public API ---- From 4cb489c5c74fe00898b655cba9e57385acef5434 Mon Sep 17 00:00:00 2001 From: romanetar Date: Thu, 8 Oct 2026 16:59:47 +0200 Subject: [PATCH 2/4] fix(sponsors): keep badge scans whose answers do not match the question options A mandatory list question (ComboBox, CheckBoxList, RadioButtonList) throws a ValidationException from isAnswered() when an answer matches no option id, e.g. after a sponsor switches a mandatory Text question to a ComboBox while devices still hold free-text answers. That escaped hadCompletedExtraQuestions() and rolled the whole scan back with a 412 the app treats as a permanent error. addBadgeScan, the retry merge and updateBadgeScan now share applyExtraQuestionAnswers(), which logs that exception instead of rethrowing it. Answers are added before validation and nothing is flushed yet, so the scan and the answers are both kept. --- .../Model/Imp/SponsorUserInfoGrantService.php | 48 +++++++---- ...OAuth2SummitBadgeScanApiControllerTest.php | 80 +++++++++++++++++++ 2 files changed, 111 insertions(+), 17 deletions(-) diff --git a/app/Services/Model/Imp/SponsorUserInfoGrantService.php b/app/Services/Model/Imp/SponsorUserInfoGrantService.php index 525ab03f46..791bfb3142 100644 --- a/app/Services/Model/Imp/SponsorUserInfoGrantService.php +++ b/app/Services/Model/Imp/SponsorUserInfoGrantService.php @@ -408,14 +408,8 @@ private function addBadgeScanLocked( // extra questions $extra_questions = $data['extra_questions'] ?? []; - if (count($extra_questions)) { - // "mandatory" is an instruction to the device, not a server rule: - // save whatever answers were sent and never roll back the scan. - $res = $scan->hadCompletedExtraQuestions($extra_questions); - if (!$res) { - Log::warning("SponsorUserInfoGrantService::addBadgeScan badge scan was saved with missing mandatory answers."); - } - } + if (count($extra_questions)) + $this->applyExtraQuestionAnswers($scan, $extra_questions, 'addBadgeScan'); return $scan; }); @@ -479,7 +473,6 @@ private function addBadgeScanLocked( * @param SponsorBadgeScan $existing * @param array $data * @return SponsorBadgeScan - * @throws ValidationException */ private function mergeRetryIntoExistingScan(SponsorBadgeScan $existing, array $data): SponsorBadgeScan { @@ -488,12 +481,37 @@ private function mergeRetryIntoExistingScan(SponsorBadgeScan $existing, array $d $existing->setNotes($notes); $extra_questions = $data['extra_questions'] ?? []; - if (count($extra_questions) && !$existing->hadCompletedExtraQuestions($extra_questions)) - Log::warning("SponsorUserInfoGrantService::mergeRetryIntoExistingScan badge scan was saved with missing mandatory answers."); + if (count($extra_questions)) + $this->applyExtraQuestionAnswers($existing, $extra_questions, 'mergeRetryIntoExistingScan'); return $existing; } + /** + * "Mandatory" and the allowed options are instructions to the device, not + * server rules: a device can hold answers captured under a question setup + * the server no longer has (e.g. a mandatory Text question switched to a + * ComboBox, so a free-text answer matches no option). Save whatever answers + * were sent and never reject the upload or roll the scan back over them. + * + * hadCompletedExtraQuestions adds every answer before validating them and + * nothing is flushed yet, so catching its ValidationException keeps both + * the scan and the answers. + * @param SponsorBadgeScan $scan + * @param array $extra_questions + * @param string $caller + */ + private function applyExtraQuestionAnswers(SponsorBadgeScan $scan, array $extra_questions, string $caller): void + { + try { + if (!$scan->hadCompletedExtraQuestions($extra_questions)) + Log::warning(sprintf("SponsorUserInfoGrantService::%s badge scan was saved with missing mandatory answers.", $caller)); + } + catch (ValidationException $ex) { + Log::warning(sprintf("SponsorUserInfoGrantService::%s badge scan was saved with answers that do not match the question setup: %s", $caller, $ex->getMessage())); + } + } + /** * @param Summit $summit * @param Member $current_member @@ -523,12 +541,8 @@ public function updateBadgeScan(Summit $summit, Member $current_member, int $sca // extra questions $extra_questions = $payload['extra_questions'] ?? []; - if (count($extra_questions)) { - $res = $scan->hadCompletedExtraQuestions($extra_questions); - if (!$res) { - Log::warning("SponsorUserInfoGrantService::updateBadgeScan badge scan was saved with missing mandatory answers."); - } - } + if (count($extra_questions)) + $this->applyExtraQuestionAnswers($scan, $extra_questions, 'updateBadgeScan'); return $scan; }); diff --git a/tests/oauth2/OAuth2SummitBadgeScanApiControllerTest.php b/tests/oauth2/OAuth2SummitBadgeScanApiControllerTest.php index 7274313957..f8e1e66dda 100644 --- a/tests/oauth2/OAuth2SummitBadgeScanApiControllerTest.php +++ b/tests/oauth2/OAuth2SummitBadgeScanApiControllerTest.php @@ -12,6 +12,8 @@ * limitations under the License. **/ +use App\Models\Foundation\ExtraQuestions\ExtraQuestionTypeConstants; +use App\Models\Foundation\ExtraQuestions\ExtraQuestionTypeValue; use App\Models\Foundation\Main\IGroup; use App\Models\Foundation\Summit\ExtraQuestions\SummitSponsorExtraQuestionType; use App\Models\Foundation\Summit\Repositories\ISponsorRepository; @@ -1569,4 +1571,82 @@ public function testUpdateBadgeScanWithMissingMandatoryAnswerIsSaved(){ $this->assertCount(1, $answers); $this->assertEquals($answered_id, $answers->first()->getQuestionId()); } + + /** + * The sponsor switches the first question (Text) to a mandatory ComboBox + * while devices still hold free-text answers for it. Returns its id. + * @return int + */ + private function switchFirstSponsorQuestionToMandatoryComboBox(): int + { + $sponsor = self::$summit->getSummitSponsors()[0]; + $question = $sponsor->getExtraQuestions()[0]; + if (!$question instanceof SummitSponsorExtraQuestionType) self::fail(); + $question->setType(ExtraQuestionTypeConstants::ComboBoxQuestionType); + $question->setMandatory(true); + $value = new ExtraQuestionTypeValue(); + $value->setLabel('Yes'); + $value->setValue('Yes'); + $question->addValue($value); + self::$em->persist($sponsor); + self::$em->flush(); + return $question->getId(); + } + + /** + * A free-text answer ("Yes") matches no option id of the ComboBox, which + * makes the question validation throw; it must neither reject the upload + * nor roll the scan back. + */ + public function testAddBadgeScanWithAnswerNotMatchingTheQuestionOptionsIsSaved(){ + $question_id = $this->switchFirstSponsorQuestionToMandatoryComboBox(); + + $service = App::make(ISponsorUserInfoGrantService::class); + $scan = $service->addBadgeScan(self::$summit, self::$member, $this->badgeScanPayload([ + 'extra_questions' => [ + ['question_id' => $question_id, 'answer' => 'Yes'], + ], + ])); + + $answers = $this->reloadScan($scan->getId())->getExtraQuestionAnswers(); + $this->assertCount(1, $answers); + $this->assertEquals($question_id, $answers->first()->getQuestionId()); + $this->assertEquals('Yes', $answers->first()->getValue()); + } + + public function testAddBadgeScanRetryWithAnswerNotMatchingTheQuestionOptionsIsSaved(){ + $question_id = $this->switchFirstSponsorQuestionToMandatoryComboBox(); + + $service = App::make(ISponsorUserInfoGrantService::class); + $first = $service->addBadgeScan(self::$summit, self::$member, $this->badgeScanPayload()); + $first_id = $first->getId(); + + $service->addBadgeScan(self::$summit, self::$member, $this->badgeScanPayload([ + 'extra_questions' => [ + ['question_id' => $question_id, 'answer' => 'Yes'], + ], + ])); + + $answers = $this->reloadScan($first_id)->getExtraQuestionAnswers(); + $this->assertCount(1, $answers); + $this->assertEquals('Yes', $answers->first()->getValue()); + } + + public function testUpdateBadgeScanWithAnswerNotMatchingTheQuestionOptionsIsSaved(){ + $question_id = $this->switchFirstSponsorQuestionToMandatoryComboBox(); + + $service = App::make(ISponsorUserInfoGrantService::class); + $scan = $service->addBadgeScan(self::$summit, self::$member, $this->badgeScanPayload()); + $scan_id = $scan->getId(); + + $service->updateBadgeScan(self::$summit, self::$member, $scan_id, [ + 'extra_questions' => [ + ['question_id' => $question_id, 'answer' => 'Yes'], + ], + ]); + + $answers = $this->reloadScan($scan_id)->getExtraQuestionAnswers(); + $this->assertCount(1, $answers); + $this->assertEquals('Yes', $answers->first()->getValue()); + } } From 10eb26751f8506c2c8ef8dab99839a3764dcc5f7 Mon Sep 17 00:00:00 2001 From: romanetar Date: Thu, 8 Oct 2026 17:38:51 +0200 Subject: [PATCH 3/4] fix(sponsors): log ids when a badge scan is saved with incomplete answers After A1 these warnings are the only server-side trace that a lead was saved with mandatory answers missing or not matching the question options, so they now carry the scan, sponsor, badge and member ids, the scan date and the ids of the mandatory questions left unanswered. A new scan has no id until the transaction flushes and is logged as scan 0; the other fields identify it. --- .../Model/Imp/SponsorUserInfoGrantService.php | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/app/Services/Model/Imp/SponsorUserInfoGrantService.php b/app/Services/Model/Imp/SponsorUserInfoGrantService.php index 791bfb3142..f4c47bdf5a 100644 --- a/app/Services/Model/Imp/SponsorUserInfoGrantService.php +++ b/app/Services/Model/Imp/SponsorUserInfoGrantService.php @@ -505,13 +505,58 @@ private function applyExtraQuestionAnswers(SponsorBadgeScan $scan, array $extra_ { try { if (!$scan->hadCompletedExtraQuestions($extra_questions)) - Log::warning(sprintf("SponsorUserInfoGrantService::%s badge scan was saved with missing mandatory answers.", $caller)); + Log::warning( + sprintf( + "SponsorUserInfoGrantService::%s badge scan was saved with missing mandatory answers - %s", + $caller, + $this->describeScanForLog($scan) + ) + ); } catch (ValidationException $ex) { - Log::warning(sprintf("SponsorUserInfoGrantService::%s badge scan was saved with answers that do not match the question setup: %s", $caller, $ex->getMessage())); + Log::warning( + sprintf( + "SponsorUserInfoGrantService::%s badge scan was saved with answers that do not match the question setup (%s) - %s", + $caller, + $ex->getMessage(), + $this->describeScanForLog($scan) + ) + ); } } + /** + * Ids support needs to find a scan saved with incomplete answers. A new + * scan has no id until the transaction flushes (logged as 0); sponsor, + * badge, member and scan date identify it. + * @param SponsorBadgeScan $scan + * @return string + */ + private function describeScanForLog(SponsorBadgeScan $scan): string + { + $answered = []; + foreach ($scan->getExtraQuestionAnswers() as $answer) { + if ($answer->hasValue()) + $answered[$answer->getQuestionId()] = true; + } + + $unanswered_mandatory = []; + foreach ($scan->getExtraQuestions() as $question) { + if ($question->isMandatory() && !isset($answered[$question->getId()])) + $unanswered_mandatory[] = $question->getId(); + } + + return sprintf( + "scan %s sponsor %s badge %s member %s scan_date %s unanswered mandatory questions [%s]", + $scan->getId(), + $scan->getSponsor()->getId(), + $scan->getBadge()->getId(), + $scan->getUser()->getId(), + $scan->getScanDate()->getTimestamp(), + implode(",", $unanswered_mandatory) + ); + } + /** * @param Summit $summit * @param Member $current_member From e84eb85ca610ff838ce928be1e29c094887392ff Mon Sep 17 00:00:00 2001 From: smarcet Date: Thu, 8 Oct 2026 13:02:49 -0300 Subject: [PATCH 4/4] docs(sponsors): document the always-refused extra question and option deletes Both DELETE endpoints now refuse with a 412 regardless of state, but their OpenAPI annotations still advertised a 204 success and listed no 412. Replace the 204 with the 412 response and say in the summary and description that the delete is always refused, so the published Swagger matches the contract. --- .../Summit/OAuth2SummitSponsorApiController.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php index 7a3ed703e1..b121c8e78d 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php @@ -3684,8 +3684,8 @@ public function updateExtraQuestion($summit_id, $sponsor_id, $extra_question_id) #[OA\Delete( path: "/api/v1/summits/{id}/sponsors/{sponsor_id}/extra-questions/{extra_question_id}", - description: "required-groups " . IGroup::SuperAdmins . ", " . IGroup::Administrators . ", " . IGroup::SummitAdministrators . ", " . IGroup::Sponsors . ", " . IGroup::SponsorExternalUsers, - summary: 'Delete Sponsor Extra Question', + summary: 'Delete Sponsor Extra Question (always refused)', + description: "Always refused with 412: devices may hold answers for this question that the server has not seen yet. Edit the wording or turn off Mandatory instead. required-groups " . IGroup::SuperAdmins . ", " . IGroup::Administrators . ", " . IGroup::SummitAdministrators . ", " . IGroup::Sponsors . ", " . IGroup::SponsorExternalUsers, operationId: 'deleteSponsorExtraQuestion', tags: ['Sponsors'], x: [ @@ -3730,8 +3730,8 @@ public function updateExtraQuestion($summit_id, $sponsor_id, $extra_question_id) ], responses: [ new OA\Response( - response: Response::HTTP_NO_CONTENT, - description: 'Extra question deleted successfully' + response: Response::HTTP_PRECONDITION_FAILED, + description: "Validation Error - always returned: questions can't be deleted once created, because devices may already be using them" ), new OA\Response(response: Response::HTTP_UNAUTHORIZED, description: "Unauthorized"), new OA\Response(response: Response::HTTP_NOT_FOUND, description: "Not Found"), @@ -3979,8 +3979,8 @@ function ($value_id, $payload, $summit, $sponsor_id, $extra_question_id) { use ParametrizedDeleteEntity; #[OA\Delete( path: "/api/v1/summits/{id}/sponsors/{sponsor_id}/extra-questions/{extra_question_id}/values/{value_id}", - description: "required-groups " . IGroup::SuperAdmins . ", " . IGroup::Administrators . ", " . IGroup::SummitAdministrators . ", " . IGroup::Sponsors . ", " . IGroup::SponsorExternalUsers, - summary: 'Delete Extra Question Value', + description: "Always refused with 412: devices may hold answers for this option that the server has not seen yet. Edit the wording instead. required-groups " . IGroup::SuperAdmins . ", " . IGroup::Administrators . ", " . IGroup::SummitAdministrators . ", " . IGroup::Sponsors . ", " . IGroup::SponsorExternalUsers, + summary: 'Delete Extra Question Value (always refused)', operationId: 'deleteSponsorExtraQuestionValue', tags: ['Sponsors'], x: [ @@ -4032,8 +4032,8 @@ function ($value_id, $payload, $summit, $sponsor_id, $extra_question_id) { ], responses: [ new OA\Response( - response: Response::HTTP_NO_CONTENT, - description: 'Question value deleted successfully' + response: Response::HTTP_PRECONDITION_FAILED, + description: "Validation Error - always returned: answer options can't be deleted once created, because devices may already be using them" ), new OA\Response(response: Response::HTTP_UNAUTHORIZED, description: "Unauthorized"), new OA\Response(response: Response::HTTP_NOT_FOUND, description: "Not Found"),