diff --git a/api/app/Http/Controllers/Api/V1/FormBuilder/PublicFormSubmissionController.php b/api/app/Http/Controllers/Api/V1/FormBuilder/PublicFormSubmissionController.php index 07145024..17a34288 100644 --- a/api/app/Http/Controllers/Api/V1/FormBuilder/PublicFormSubmissionController.php +++ b/api/app/Http/Controllers/Api/V1/FormBuilder/PublicFormSubmissionController.php @@ -84,16 +84,23 @@ final class PublicFormSubmissionController extends Controller $submission = $this->loadSubmission($schema, $submissionId); $this->assertDraft($submission); - $values = (array) ($request->validated('values') ?? []); + $validated = $request->validated(); + + $values = (array) ($validated['values'] ?? []); if ($values !== []) { // saveDraft → upsertMany + auto_save_count++ + FormSubmissionDraftUpdated // event. FieldValidationException bubbles to the D6 envelope. $this->submissionService->saveDraft($submission, $values, null); } + $submission->refresh(); + if ($request->filled('first_interacted_at')) { - $submission->refresh(); - $submission->first_interacted_at ??= $request->validated('first_interacted_at'); + $submission->first_interacted_at ??= $validated['first_interacted_at']; + } + $this->applySubmitterDetails($submission, $validated); + + if ($submission->isDirty()) { $submission->save(); } @@ -124,7 +131,8 @@ final class PublicFormSubmissionController extends Controller ); } - $bodyValues = (array) ($request->validated('values') ?? []); + $validated = $request->validated(); + $bodyValues = (array) ($validated['values'] ?? []); $mergedValues = $this->mergeWithSavedValues($submission, $bodyValues); $this->validateMergedValues($schema, $mergedValues); @@ -133,12 +141,35 @@ final class PublicFormSubmissionController extends Controller $this->valueService->upsertMany($submission, $bodyValues, null); } + $this->applySubmitterDetails($submission, $validated); + if ($submission->isDirty()) { + $submission->save(); + } + $submission = $this->submissionService->submit($submission->refresh(), null); RateLimiter::hit('form-submit:'.$publicToken.':'.$request->ip(), 3600); return $this->created(new PublicFormSubmissionResource($submission)); } + /** + * Copy present submitter keys from the validated payload onto the model. + * Missing keys are left alone (standard Laravel update semantics) so a + * debounced save that omits the fields never nulls previously persisted + * name/email. + * + * @param array $validated + */ + private function applySubmitterDetails(FormSubmission $submission, array $validated): void + { + if (array_key_exists('public_submitter_name', $validated)) { + $submission->public_submitter_name = $validated['public_submitter_name']; + } + if (array_key_exists('public_submitter_email', $validated)) { + $submission->public_submitter_email = $validated['public_submitter_email']; + } + } + private function loadSubmission(FormSchema $schema, string $submissionId): FormSubmission { /** @var FormSubmission|null $submission */ diff --git a/api/app/Http/Requests/Api/V1/FormBuilder/SavePublicDraftRequest.php b/api/app/Http/Requests/Api/V1/FormBuilder/SavePublicDraftRequest.php index e929614c..d4700fb2 100644 --- a/api/app/Http/Requests/Api/V1/FormBuilder/SavePublicDraftRequest.php +++ b/api/app/Http/Requests/Api/V1/FormBuilder/SavePublicDraftRequest.php @@ -29,6 +29,8 @@ final class SavePublicDraftRequest extends FormRequest $base = [ 'values' => ['sometimes', 'array'], 'first_interacted_at' => ['nullable', 'date'], + 'public_submitter_name' => ['nullable', 'string', 'max:150'], + 'public_submitter_email' => ['nullable', 'email', 'max:255'], ]; $schema = $this->resolveSchema(); diff --git a/api/app/Http/Requests/Api/V1/FormBuilder/SubmitPublicSubmissionRequest.php b/api/app/Http/Requests/Api/V1/FormBuilder/SubmitPublicSubmissionRequest.php index d83b1173..1a79de93 100644 --- a/api/app/Http/Requests/Api/V1/FormBuilder/SubmitPublicSubmissionRequest.php +++ b/api/app/Http/Requests/Api/V1/FormBuilder/SubmitPublicSubmissionRequest.php @@ -35,6 +35,8 @@ final class SubmitPublicSubmissionRequest extends FormRequest $base = [ 'values' => ['sometimes', 'array'], 'captcha_token' => ['nullable', 'string', 'max:2000'], + 'public_submitter_name' => ['nullable', 'string', 'max:150'], + 'public_submitter_email' => ['nullable', 'email', 'max:255'], ]; $schema = $this->resolveSchema(); diff --git a/api/tests/Feature/Api/V1/Public/FormBuilder/PublicFormDraftLifecycleTest.php b/api/tests/Feature/Api/V1/Public/FormBuilder/PublicFormDraftLifecycleTest.php index 9bbf1584..2a79c831 100644 --- a/api/tests/Feature/Api/V1/Public/FormBuilder/PublicFormDraftLifecycleTest.php +++ b/api/tests/Feature/Api/V1/Public/FormBuilder/PublicFormDraftLifecycleTest.php @@ -191,6 +191,103 @@ final class PublicFormDraftLifecycleTest extends TestCase $response->assertStatus(404); } + public function test_put_persists_submitter_details(): void + { + $submission = $this->startDraft(); + + $this->putJson( + "/api/v1/public/forms/{$this->schema->public_token}/submissions/{$submission->id}", + [ + 'public_submitter_name' => 'Backend Fix Test', + 'public_submitter_email' => 'backendfix@test.nl', + ], + )->assertOk(); + + $fresh = $submission->fresh(); + $this->assertSame('Backend Fix Test', $fresh->public_submitter_name); + $this->assertSame('backendfix@test.nl', $fresh->public_submitter_email); + } + + public function test_put_without_submitter_details_leaves_existing_values_untouched(): void + { + $submission = $this->startDraft(); + + $this->putJson( + "/api/v1/public/forms/{$this->schema->public_token}/submissions/{$submission->id}", + [ + 'public_submitter_name' => 'Original Name', + 'public_submitter_email' => 'original@test.nl', + ], + )->assertOk(); + + // Second PUT omits the submitter keys — must not null them out. + $this->putJson( + "/api/v1/public/forms/{$this->schema->public_token}/submissions/{$submission->id}", + ['values' => ['naam' => 'Bart']], + )->assertOk(); + + $fresh = $submission->fresh(); + $this->assertSame('Original Name', $fresh->public_submitter_name); + $this->assertSame('original@test.nl', $fresh->public_submitter_email); + } + + public function test_submit_persists_submitter_details(): void + { + $submission = $this->startDraft(); + + $this->putJson( + "/api/v1/public/forms/{$this->schema->public_token}/submissions/{$submission->id}", + ['values' => ['naam' => 'Bart', 'email' => 'bart@example.nl']], + )->assertOk(); + + $this->postJson( + "/api/v1/public/forms/{$this->schema->public_token}/submissions/{$submission->id}/submit", + [ + 'public_submitter_name' => 'Submit Name', + 'public_submitter_email' => 'submit@test.nl', + ], + )->assertCreated(); + + $fresh = $submission->fresh(); + $this->assertSame('submitted', $fresh->status->value); + $this->assertSame('Submit Name', $fresh->public_submitter_name); + $this->assertSame('submit@test.nl', $fresh->public_submitter_email); + } + + public function test_submit_rejects_invalid_submitter_email(): void + { + $submission = $this->startDraft(); + + $response = $this->postJson( + "/api/v1/public/forms/{$this->schema->public_token}/submissions/{$submission->id}/submit", + [ + 'values' => ['naam' => 'Bart', 'email' => 'bart@example.nl'], + 'public_submitter_email' => 'not-an-email', + ], + ); + + $response->assertStatus(422); + $this->assertSame('VALIDATION_FAILED', $response->json('code')); + $this->assertArrayHasKey('public_submitter_email', $response->json('errors')); + } + + public function test_submit_rejects_overlong_submitter_name(): void + { + $submission = $this->startDraft(); + + $response = $this->postJson( + "/api/v1/public/forms/{$this->schema->public_token}/submissions/{$submission->id}/submit", + [ + 'values' => ['naam' => 'Bart', 'email' => 'bart@example.nl'], + 'public_submitter_name' => str_repeat('a', 151), + ], + ); + + $response->assertStatus(422); + $this->assertSame('VALIDATION_FAILED', $response->json('code')); + $this->assertArrayHasKey('public_submitter_name', $response->json('errors')); + } + private function startDraft(): FormSubmission { $response = $this->postJson(