submissionUploadService = $submissionUploadService; $this->formService = $formService; $this->fieldService = $fieldService; $this->fieldDuplicateValidationService = $fieldDuplicateValidationService; $this->securityService = $securityService; $this->entryService = $entryService; $this->notificationService = $notificationService; $this->mailerService = $mailerService; $this->confirmationService = $confirmationService; $this->formSubmissionPaymentService = $formSubmissionPaymentService; } /** * @param array $params Normalized submission payload * @param array $uploadedFiles Multipart files from the transport layer * * @return array * * @throws InvalidArgumentException|NotFoundException|ForbiddenException|ValidationException */ public function processSubmission( array $params, array $uploadedFiles = [], ?int $formIdOverride = null ): array { $context = $this->buildSubmissionContext($params, $uploadedFiles, $formIdOverride); $duplicateResult = $this->buildDuplicateResultIfNeeded($context); if ($duplicateResult !== null) { return $duplicateResult; } do_action( 'ivyforms/form/before_submission', $context['formId'], $context['submissionData'], $context['formFields'] ); $paymentOutcome = $this->formSubmissionPaymentService->processWhenRequired( $context['params'], $context['form'], $context['formFields'], $context['submissionData'], $this->formSubmissionPaymentService->findPaymentMethodField($context['formFields']) ); $earlyPaymentResult = $this->buildEarlyPaymentResult($paymentOutcome); if ($earlyPaymentResult !== null) { return $earlyPaymentResult; } $entry = $this->persistEntryWithOrphanProtection( $context['form'], $context['formFields'], $context['submissionData'], $context['formId'], $paymentOutcome ); if (isset($entry['payment_error'])) { return $entry; } $paymentPersistenceResult = $this->persistPaymentWithOrphanProtection( $entry, $context['formId'], $paymentOutcome ); if ($paymentPersistenceResult !== null) { return $paymentPersistenceResult; } $entryId = (int) ($entry['id'] ?? 0); if ($entryId > 0 && !empty($paymentOutcome['squareOrderId'])) { $this->formSubmissionPaymentService->recordOrderForEntry( $entryId, (string) $paymentOutcome['squareOrderId'], isset($paymentOutcome['squareCustomerId']) ? (string) $paymentOutcome['squareCustomerId'] : null, MoneyMinorUnits::toMinor( (float) $paymentOutcome['paymentTotal'], (string) $paymentOutcome['paymentCurrency'] ), (string) $paymentOutcome['paymentCurrency'] ); } $entryId = $entry['id'] ?? null; $placeholderData = $this->buildPlaceholderData($context['formFields'], $context['submissionData'], $entryId); $notificationsResult = $this->dispatchNotifications($context, $placeholderData); $confirmationPayload = $this->confirmationService->sanitizeProcessedPayload( $this->confirmationService->processConfirmations( $context['formId'], $context['submissionData'], $context['formFields'], $placeholderData['fieldData'], $placeholderData['generalData'], $context['fieldLabels'] ) ); do_action( 'ivyforms/form/after_submission', $context['formId'], $context['submissionData'], $context['formFields'], $entryId ); return $this->buildSuccessResult($entry, $notificationsResult, $confirmationPayload); } /** * @param array $params * @param array $uploadedFiles * @param array<\IvyForms\Entity\Field\Field> $formFields * * @return array */ public function mergeUploadedFilesIntoValues( array $params, array $uploadedFiles, array $formFields ): array { return $this->submissionUploadService->mergeUploadedFilesIntoValues( $params, $uploadedFiles, $formFields ); } /** * @param array $params * @param array $uploadedFiles * * @return array{ * formId: int, * form: Form, * formFields: array, * submissionData: array, * params: array, * fieldLabels: array * } * * @throws InvalidArgumentException|NotFoundException|ForbiddenException|ValidationException */ private function buildSubmissionContext( array $params, array $uploadedFiles, ?int $formIdOverride = null ): array { if ($formIdOverride !== null) { $params['formId'] = $formIdOverride; } if (empty($params) || !isset($params['values']) || !is_array($params['values'])) { throw new InvalidArgumentException( BackendStrings::getExceptionStrings()['invalid_request_data'] ); } $formId = Sanitizer::sanitizeId((int) ($params['formId'] ?? 0)); if ($formId <= 0) { throw new InvalidArgumentException( BackendStrings::getExceptionStrings()['invalid_form_id'] ); } $form = $this->formService->getFormById($formId); $formFields = $this->fieldService->getAllFields($form->getId()); $this->fieldService->validateFieldsType($formFields); $fieldLabels = PlaceholderService::buildFieldLabels($formFields); $this->securityService->validateFormSubmission($params, $formFields); $params = $this->mergeUploadedFilesIntoValues($params, $uploadedFiles, $formFields); foreach (['sourceId', 'idempotencyKey', 'paymentMethodId', 'paymentIntentId', 'paypalOrderId'] as $key) { if (isset($params[$key])) { $params[$key] = sanitize_text_field((string) $params[$key]); } } $submissionData = Sanitizer::sanitizeFormSubmissionData($params, $formFields); do_action('ivyforms/form/validate_submission', $submissionData, $formFields); return [ 'formId' => $formId, 'form' => $form, 'formFields' => $formFields, 'submissionData' => $submissionData, 'params' => $params, 'fieldLabels' => $fieldLabels, ]; } /** * @param array{ * formId: int, * form: Form, * formFields: array, * submissionData: array, * params: array, * fieldLabels: array * } $context * * @return array|null */ private function buildDuplicateResultIfNeeded(array $context): ?array { [$isDuplicate, $duplicateErrors] = $this->fieldDuplicateValidationService->checkDuplicateFieldValues( $context['formFields'], $context['submissionData'], $context['formId'] ); if (!$isDuplicate) { return null; } return [ 'success' => null, 'entry' => ['stored' => false], 'confirmation' => null, 'is_duplicate' => true, 'duplicate_errors' => $duplicateErrors, ]; } /** * @param array $formFields * @param array $submissionData * * @return array{stored: bool, id?: int} */ private function storeEntryIfEnabled( Form $form, int $formId, array $formFields, array $submissionData ): array { if (!$form->isStoreEntries()) { return ['stored' => false]; } $entryData = EntryHelper::buildEntryData($formId); $entryObj = EntryFactory::create($entryData); $entryId = $this->entryService->getEntryManager()->createEntry($entryObj); $this->entryService->getEntryFieldManager()->addEntryFields($formFields, $entryId, $submissionData); return [ 'stored' => true, 'id' => $entryId, ]; } /** * @param array $formFields * @param array $submissionData * * @return array{fieldData: array, generalData: array} */ private function buildPlaceholderData(array $formFields, array $submissionData, ?int $entryId): array { return [ 'fieldData' => PlaceholderService::buildFieldData($formFields, $submissionData), 'generalData' => PlaceholderService::buildGeneralData($entryId ?? 0, $submissionData), ]; } /** * @param array{ * formId: int, * form: Form, * formFields: array, * submissionData: array, * params: array, * fieldLabels: array * } $context * @param array{fieldData: array, generalData: array} $placeholderData * * @return mixed */ private function dispatchNotifications(array $context, array $placeholderData) { return $this->notificationService->processNotifications( $context['formId'], $context['submissionData'], $context['formFields'], $placeholderData['fieldData'], $placeholderData['generalData'], $this->mailerService, $context['fieldLabels'] ); } /** * @param array{stored: bool, id?: int} $entry * @param mixed $notificationsResult * @param array{message: string, result: array|null} $confirmationPayload * * @return array */ private function buildSuccessResult( array $entry, $notificationsResult, array $confirmationPayload ): array { return [ 'success' => $notificationsResult, 'entry' => $entry, 'confirmation' => $confirmationPayload['message'], 'confirmationResult' => $confirmationPayload['result'], ]; } /** * @param array $paymentOutcome * @return array|null */ private function buildEarlyPaymentResult(array $paymentOutcome): ?array { $errorResponse = $paymentOutcome['errorResponse'] ?? null; if ($errorResponse instanceof WP_REST_Response) { return $this->normalizePaymentErrorResult($errorResponse); } if (empty($paymentOutcome['requiresAction'])) { return null; } return [ 'requires_action' => true, 'client_secret' => $paymentOutcome['clientSecret'] ?? null, 'payment_intent_id' => $paymentOutcome['paymentIntentId'] ?? null, 'payment_method' => $paymentOutcome['paymentMethod'] ?? 'stripe', 'success' => false, ]; } /** * @param array $formFields * @param array $submissionData * @param array $paymentOutcome * @return array */ private function persistEntryWithOrphanProtection( Form $form, array $formFields, array $submissionData, int $formId, array $paymentOutcome ): array { $transactionId = (string) ($paymentOutcome['paymentResult']['transactionId'] ?? ''); if ( $transactionId !== '' && $this->formSubmissionPaymentService->paymentAlreadyLinkedToEntry($transactionId) ) { return $this->normalizePaymentErrorResult( $this->formSubmissionPaymentService->duplicatePaymentErrorResponse() ); } try { $entry = $this->storeEntryIfEnabled($form, $formId, $formFields, $submissionData); } catch (\Throwable $exception) { $this->logPersistFailure('entry', $exception); $this->recordOrphanedPaymentSafely($formId, $paymentOutcome); return $this->orphanedPaymentResult(); } if (!empty($entry['stored']) && empty($entry['id']) && $paymentOutcome['paymentResult'] !== null) { $this->recordOrphanedPaymentSafely($formId, $paymentOutcome); return $this->orphanedPaymentResult(); } return $entry; } /** * @param array $entry * @param array $paymentOutcome * @return array|null */ private function persistPaymentWithOrphanProtection( array $entry, int $formId, array $paymentOutcome ): ?array { try { $recorded = $this->formSubmissionPaymentService->recordSubmissionPayment( (int) ($entry['id'] ?? 0), $formId, $paymentOutcome ); } catch (\Throwable $exception) { $this->logPersistFailure('payment', $exception); $this->rollbackEntryIfNeeded($entry); $this->recordOrphanedPaymentSafely($formId, $paymentOutcome); return $this->orphanedPaymentResult(); } if ($recorded) { return null; } $this->rollbackEntryIfNeeded($entry); return $this->normalizePaymentErrorResult( $this->formSubmissionPaymentService->duplicatePaymentErrorResponse() ); } /** * @param array $paymentOutcome */ private function recordOrphanedPaymentSafely(int $formId, array $paymentOutcome): void { try { $this->formSubmissionPaymentService->recordOrphanedPayment($formId, $paymentOutcome); } catch (\Throwable $exception) { $this->logPersistFailure('orphaned payment', $exception); } } private function logPersistFailure(string $stage, \Throwable $exception): void { error_log(sprintf( '[IvyForms] Failed to persist %s after payment: %s in %s:%d', $stage, $exception->getMessage(), $exception->getFile(), $exception->getLine() )); } /** * @return array */ private function orphanedPaymentResult(): array { return $this->normalizePaymentErrorResult(new WP_REST_Response([ 'message' => BackendStrings::getPaymentStrings()['payment_failed'], 'data' => [ 'success' => false, 'payment_error' => true, ], ], 200)); } /** * @param array{stored: bool, id?: int} $entry */ private function rollbackEntryIfNeeded(array $entry): void { if (empty($entry['stored']) || empty($entry['id'])) { return; } $entryId = (int) $entry['id']; $this->entryService->getDeletionManager()->deleteEntryFieldsByEntryIds([$entryId]); $this->entryService->getDeletionManager()->deleteEntry($entryId); } /** * @return array */ private function normalizePaymentErrorResult(WP_REST_Response $errorResponse): array { $payload = $errorResponse->get_data(); $payload = is_array($payload) ? $payload : []; return [ 'payment_error' => true, 'message' => (string) ( $payload['message'] ?? BackendStrings::getPaymentStrings()['payment_failed'] ), 'success' => false, 'entry' => ['stored' => false], 'confirmation' => null, ]; } }