diff --git a/api/app/Http/Controllers/Forms/PublicFormController.php b/api/app/Http/Controllers/Forms/PublicFormController.php index 5c8824a7..d1669c03 100644 --- a/api/app/Http/Controllers/Forms/PublicFormController.php +++ b/api/app/Http/Controllers/Forms/PublicFormController.php @@ -9,7 +9,7 @@ use App\Http\Resources\FormSubmissionResource; use App\Jobs\Form\StoreFormSubmissionJob; use App\Models\Forms\Form; use App\Models\Forms\FormSubmission; -use App\Open\MentionParser; +use App\Service\Forms\FormSubmissionProcessor; use App\Service\Forms\FormCleaner; use App\Service\WorkspaceHelper; use Illuminate\Http\Request; @@ -85,7 +85,7 @@ class PublicFormController extends Controller return redirect()->to($internal_url); } - public function answer(AnswerFormRequest $request) + public function answer(AnswerFormRequest $request, FormSubmissionProcessor $formSubmissionProcessor) { $form = $request->form; $isFirstSubmission = ($form->submissions_count === 0); @@ -95,10 +95,13 @@ class PublicFormController extends Controller $completionTime = $request->get('completion_time') ?? null; unset($submissionData['completion_time']); // Remove completion_time from the main data array - if ($form->editable_submissions) { - $job = new StoreFormSubmissionJob($form, $submissionData, $completionTime); + $job = new StoreFormSubmissionJob($form, $submissionData, $completionTime); + + if ($formSubmissionProcessor->shouldProcessSynchronously($form)) { $job->handle(); $submissionId = Hashids::encode($job->getSubmissionId()); + // Update submission data with generated values for redirect URL + $submissionData = $job->getProcessedData(); } else { StoreFormSubmissionJob::dispatch($form, $submissionData, $completionTime); } @@ -107,27 +110,7 @@ class PublicFormController extends Controller 'message' => 'Form submission saved.', 'submission_id' => $submissionId, 'is_first_submission' => $isFirstSubmission, - ], $this->getRedirectData($request->form, $submissionData))); - } - - private function getRedirectData($form, $submissionData) - { - $formattedData = collect($submissionData)->map(function ($value, $key) { - return ['id' => $key, 'value' => $value]; - })->values()->all(); - - $redirectUrl = ($form->redirect_url) ? (new MentionParser($form->redirect_url, $formattedData))->urlFriendlyOutput()->parseAsText() : null; - - if ($redirectUrl && !filter_var($redirectUrl, FILTER_VALIDATE_URL)) { - $redirectUrl = null; - } - - return $form->is_pro && $redirectUrl ? [ - 'redirect' => true, - 'redirect_url' => $redirectUrl, - ] : [ - 'redirect' => false, - ]; + ], $formSubmissionProcessor->getRedirectData($form, $submissionData))); } public function fetchSubmission(Request $request, string $slug, string $submissionId) diff --git a/api/app/Jobs/Form/StoreFormSubmissionJob.php b/api/app/Jobs/Form/StoreFormSubmissionJob.php index 6780244c..d5dd8214 100644 --- a/api/app/Jobs/Form/StoreFormSubmissionJob.php +++ b/api/app/Jobs/Form/StoreFormSubmissionJob.php @@ -27,6 +27,7 @@ class StoreFormSubmissionJob implements ShouldQueue use SerializesModels; public ?string $submissionId = null; + private ?array $formData = null; /** * Create a new job instance. @@ -44,13 +45,13 @@ class StoreFormSubmissionJob implements ShouldQueue */ public function handle() { - $formData = $this->getFormData(); - $this->addHiddenPrefills($formData); + $this->formData = $this->getFormData(); + $this->addHiddenPrefills($this->formData); - $this->storeSubmission($formData); + $this->storeSubmission($this->formData); - $formData['submission_id'] = $this->submissionId; - FormSubmitted::dispatch($this->form, $formData); + $this->formData['submission_id'] = $this->submissionId; + FormSubmitted::dispatch($this->form, $this->formData); } public function getSubmissionId() @@ -258,4 +259,15 @@ class StoreFormSubmissionJob implements ShouldQueue } }); } + + /** + * Get the processed form data after all transformations + */ + public function getProcessedData(): array + { + if ($this->formData === null) { + $this->formData = $this->getFormData(); + } + return $this->formData; + } } diff --git a/api/app/Service/Forms/FormSubmissionProcessor.php b/api/app/Service/Forms/FormSubmissionProcessor.php new file mode 100644 index 00000000..80ff7ea0 --- /dev/null +++ b/api/app/Service/Forms/FormSubmissionProcessor.php @@ -0,0 +1,75 @@ +editable_submissions) { + return true; + } + + // If no redirect URL, no need to process synchronously + if (!$form->redirect_url) { + return false; + } + + // Check if any UUID/auto-increment fields are used in redirect URL + foreach ($form->properties as $field) { + if ($this->isGeneratedField($field) && $this->isFieldUsedInRedirectUrl($form, $field['id'])) { + return true; + } + } + + return false; + } + + /** + * Checks if a field is a generated field (UUID or auto-increment) + */ + private function isGeneratedField(array $field): bool + { + return $field['type'] === 'text' && + ( + (isset($field['generates_uuid']) && $field['generates_uuid']) || + (isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) + ); + } + + /** + * Checks if a field ID is used in the form's redirect URL + */ + private function isFieldUsedInRedirectUrl(Form $form, string $fieldId): bool + { + return str_contains($form->redirect_url, '{' . $fieldId . '}'); + } + + /** + * Get the redirect data for a form submission + */ + public function getRedirectData(Form $form, array $submissionData): array + { + $redirectUrl = ($form->redirect_url) + ? (new MentionParser($form->redirect_url, array_values($submissionData)))->urlFriendlyOutput()->parseAsText() + : null; + + if ($redirectUrl && !filter_var($redirectUrl, FILTER_VALIDATE_URL)) { + $redirectUrl = null; + } + + return $form->is_pro && $redirectUrl ? [ + 'redirect' => true, + 'redirect_url' => $redirectUrl, + ] : [ + 'redirect' => false, + ]; + } +} diff --git a/api/tests/Feature/Forms/FormSubmissionProcessorTest.php b/api/tests/Feature/Forms/FormSubmissionProcessorTest.php new file mode 100644 index 00000000..4bb42157 --- /dev/null +++ b/api/tests/Feature/Forms/FormSubmissionProcessorTest.php @@ -0,0 +1,154 @@ +actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace, [ + 'editable_submissions' => true + ]); + + $processor = new FormSubmissionProcessor(); + expect($processor->shouldProcessSynchronously($form))->toBeTrue(); +}); + +it('processes synchronously with UUID field in redirect URL', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace, [ + 'redirect_url' => 'https://example.com/{field_1}', + 'properties' => [ + [ + 'id' => 'field_1', + 'type' => 'text', + 'generates_uuid' => true, + 'name' => 'UUID Field' + ] + ] + ]); + + $processor = new FormSubmissionProcessor(); + expect($processor->shouldProcessSynchronously($form))->toBeTrue(); +}); + +it('processes synchronously with auto increment field in redirect URL', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace, [ + 'redirect_url' => 'https://example.com/{field_1}', + 'properties' => [ + [ + 'id' => 'field_1', + 'type' => 'text', + 'generates_auto_increment_id' => true, + 'name' => 'Auto Increment Field' + ] + ] + ]); + + $processor = new FormSubmissionProcessor(); + expect($processor->shouldProcessSynchronously($form))->toBeTrue(); +}); + +it('processes asynchronously with no generated fields in redirect URL', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace, [ + 'redirect_url' => 'https://example.com/{field_1}', + 'properties' => [ + [ + 'id' => 'field_1', + 'type' => 'text', + 'name' => 'Regular Field' + ] + ] + ]); + + $processor = new FormSubmissionProcessor(); + expect($processor->shouldProcessSynchronously($form))->toBeFalse(); +}); + +it('processes asynchronously when generated field is not used in redirect URL', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace, [ + 'redirect_url' => 'https://example.com/{field_2}', + 'properties' => [ + [ + 'id' => 'field_1', + 'type' => 'text', + 'generates_uuid' => true, + 'name' => 'UUID Field' + ], + [ + 'id' => 'field_2', + 'type' => 'text', + 'name' => 'Regular Field' + ] + ] + ]); + + $processor = new FormSubmissionProcessor(); + expect($processor->shouldProcessSynchronously($form))->toBeFalse(); +}); + +it('processes asynchronously with no redirect URL', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace, [ + 'redirect_url' => null, + 'properties' => [ + [ + 'id' => 'field_1', + 'type' => 'text', + 'generates_uuid' => true, + 'name' => 'UUID Field' + ] + ] + ]); + + $processor = new FormSubmissionProcessor(); + expect($processor->shouldProcessSynchronously($form))->toBeFalse(); +}); + +it('formats redirect data correctly for pro users', function () { + $user = $this->actingAsProUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace, [ + 'redirect_url' => 'https://example.com/' + ]); + + $processor = new FormSubmissionProcessor(); + $redirectData = $processor->getRedirectData($form, [ + 'field_1' => [ + 'id' => 'field_1', + 'value' => 'test-value' + ] + ]); + + expect($redirectData)->toBe([ + 'redirect' => true, + 'redirect_url' => 'https://example.com/test-value' + ]); +}); + +it('returns no redirect for non-pro users', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace, [ + 'redirect_url' => 'https://example.com/' + ]); + + $processor = new FormSubmissionProcessor(); + $redirectData = $processor->getRedirectData($form, [ + 'field_1' => [ + 'id' => 'field_1', + 'value' => 'test-value' + ] + ]); + + expect($redirectData)->toBe([ + 'redirect' => false + ]); +});