fix(api): accept submitter details on public draft PUT and submit POST
S3a PR 1 frontend sends public_submitter_name and public_submitter_email on draft saves (PUT) and final submit (POST /submit), but the matching SavePublicDraftRequest and SubmitPublicSubmissionRequest did not whitelist these fields — Laravel's validated() silently stripped them, preventing mid-form name/email updates from persisting. Align both form requests with StartPublicDraftRequest to accept the same submitter fields with identical rules (string, max:150 / email, max:255, nullable). Controller copies present keys onto the submission model and saves when dirty, matching standard Laravel update() semantics — missing keys leave prior values untouched. Closes the backend gap identified in PR 1 smoke test.
This commit is contained in:
@@ -84,16 +84,23 @@ final class PublicFormSubmissionController extends Controller
|
|||||||
$submission = $this->loadSubmission($schema, $submissionId);
|
$submission = $this->loadSubmission($schema, $submissionId);
|
||||||
$this->assertDraft($submission);
|
$this->assertDraft($submission);
|
||||||
|
|
||||||
$values = (array) ($request->validated('values') ?? []);
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$values = (array) ($validated['values'] ?? []);
|
||||||
if ($values !== []) {
|
if ($values !== []) {
|
||||||
// saveDraft → upsertMany + auto_save_count++ + FormSubmissionDraftUpdated
|
// saveDraft → upsertMany + auto_save_count++ + FormSubmissionDraftUpdated
|
||||||
// event. FieldValidationException bubbles to the D6 envelope.
|
// event. FieldValidationException bubbles to the D6 envelope.
|
||||||
$this->submissionService->saveDraft($submission, $values, null);
|
$this->submissionService->saveDraft($submission, $values, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$submission->refresh();
|
||||||
|
|
||||||
if ($request->filled('first_interacted_at')) {
|
if ($request->filled('first_interacted_at')) {
|
||||||
$submission->refresh();
|
$submission->first_interacted_at ??= $validated['first_interacted_at'];
|
||||||
$submission->first_interacted_at ??= $request->validated('first_interacted_at');
|
}
|
||||||
|
$this->applySubmitterDetails($submission, $validated);
|
||||||
|
|
||||||
|
if ($submission->isDirty()) {
|
||||||
$submission->save();
|
$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);
|
$mergedValues = $this->mergeWithSavedValues($submission, $bodyValues);
|
||||||
|
|
||||||
$this->validateMergedValues($schema, $mergedValues);
|
$this->validateMergedValues($schema, $mergedValues);
|
||||||
@@ -133,12 +141,35 @@ final class PublicFormSubmissionController extends Controller
|
|||||||
$this->valueService->upsertMany($submission, $bodyValues, null);
|
$this->valueService->upsertMany($submission, $bodyValues, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->applySubmitterDetails($submission, $validated);
|
||||||
|
if ($submission->isDirty()) {
|
||||||
|
$submission->save();
|
||||||
|
}
|
||||||
|
|
||||||
$submission = $this->submissionService->submit($submission->refresh(), null);
|
$submission = $this->submissionService->submit($submission->refresh(), null);
|
||||||
RateLimiter::hit('form-submit:'.$publicToken.':'.$request->ip(), 3600);
|
RateLimiter::hit('form-submit:'.$publicToken.':'.$request->ip(), 3600);
|
||||||
|
|
||||||
return $this->created(new PublicFormSubmissionResource($submission));
|
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<string, mixed> $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
|
private function loadSubmission(FormSchema $schema, string $submissionId): FormSubmission
|
||||||
{
|
{
|
||||||
/** @var FormSubmission|null $submission */
|
/** @var FormSubmission|null $submission */
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ final class SavePublicDraftRequest extends FormRequest
|
|||||||
$base = [
|
$base = [
|
||||||
'values' => ['sometimes', 'array'],
|
'values' => ['sometimes', 'array'],
|
||||||
'first_interacted_at' => ['nullable', 'date'],
|
'first_interacted_at' => ['nullable', 'date'],
|
||||||
|
'public_submitter_name' => ['nullable', 'string', 'max:150'],
|
||||||
|
'public_submitter_email' => ['nullable', 'email', 'max:255'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$schema = $this->resolveSchema();
|
$schema = $this->resolveSchema();
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ final class SubmitPublicSubmissionRequest extends FormRequest
|
|||||||
$base = [
|
$base = [
|
||||||
'values' => ['sometimes', 'array'],
|
'values' => ['sometimes', 'array'],
|
||||||
'captcha_token' => ['nullable', 'string', 'max:2000'],
|
'captcha_token' => ['nullable', 'string', 'max:2000'],
|
||||||
|
'public_submitter_name' => ['nullable', 'string', 'max:150'],
|
||||||
|
'public_submitter_email' => ['nullable', 'email', 'max:255'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$schema = $this->resolveSchema();
|
$schema = $this->resolveSchema();
|
||||||
|
|||||||
@@ -191,6 +191,103 @@ final class PublicFormDraftLifecycleTest extends TestCase
|
|||||||
$response->assertStatus(404);
|
$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
|
private function startDraft(): FormSubmission
|
||||||
{
|
{
|
||||||
$response = $this->postJson(
|
$response = $this->postJson(
|
||||||
|
|||||||
Reference in New Issue
Block a user