PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / trunk
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder vtrunk
6.2.15 6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 All 197 releases
← All changes | app/Modules/Payments/PaymentMethods/Stripe/StripeInlineProcessor.php +213 -74 6.2.2 → trunk View file →
@@ -2,8 +2,9 @@
2 2
3 3 namespace FluentForm\App\Modules\Payments\PaymentMethods\Stripe;
4 4
5 5 use FluentForm\App\Helpers\Helper;
6 +use FluentForm\App\Models\Submission;
6 7 use FluentForm\App\Modules\Payments\PaymentHelper;
7 8 use FluentForm\Framework\Helpers\ArrayHelper;
8 9 use FluentForm\App\Modules\Payments\PaymentMethods\Stripe\API\SCA;
9 10 use FluentForm\App\Modules\Payments\PaymentMethods\Stripe\API\Plan;
@@ -15,9 +16,9 @@
15 16 }
16 17
17 18 class StripeInlineProcessor extends StripeProcessor
18 19 {
19 -
20 +
20 21 public function init()
21 22 {
22 23 /*
23 24 * After form submission this hooks fire to start Making payment
@@ -42,9 +43,9 @@
42 43 $this->setSubmissionId($submissionId);
43 44 $this->form = $form;
44 45 $submission = $this->getSubmission();
45 46 $paymentTotal = $this->getAmountTotal();
46 -
47 +
47 48 if (!$paymentTotal && !$hasSubscriptions) {
48 49 return false;
49 50 }
50 51
@@ -75,9 +76,9 @@
75 76 'confirm' => 'true',
76 77 'description' => $this->getProductNames(),
77 78 'statement_descriptor_suffix' => StripeSettings::getPaymentDescriptor($form),
78 79 'metadata' => $this->getIntentMetaData($submission, $form, $transaction, $paymentSettings),
79 - 'customer' => $customer->id
80 + 'customer' => $customer->id,
80 81 ];
81 82
82 83 $intentArgs = apply_filters('fluentform/stripe_checkout_args_inline', $intentArgs, $submission, $transaction, $form);
83 84
@@ -82,9 +83,12 @@
82 83 $intentArgs = apply_filters('fluentform/stripe_checkout_args_inline', $intentArgs, $submission, $transaction, $form);
83 84
84 85 // If FluentForm Pro is not installed, apply the fee 1.9% of the total amount
85 86 if (!Helper::hasPro()) {
86 - $applicationFeeAmount = (int) ($totalPayable * 0.019);
87 + $applicationFeeAmount = $this->calculateApplicationFeeAmount(
88 + $totalPayable,
89 + $transaction->currency
90 + );
87 91 $intentArgs['application_fee_amount'] = $applicationFeeAmount;
88 92 }
89 93 $this->handlePaymentIntent($transaction, $submission, $intentArgs);
90 94 }
@@ -102,10 +106,10 @@
102 106 $subscription = $subscriptions[0];
103 107
104 108 $subscriptionTransactionArgs = Plan::getPriceIdsFromSubscriptionTransaction($subscription, $transaction);
105 109
106 - if(is_wp_error($subscriptionTransactionArgs)) {
107 - $this->handlePaymentChargeError($customer->get_error_message(), $submission, $transaction, false, 'customer');
110 + if (is_wp_error($subscriptionTransactionArgs)) {
111 + $this->handlePaymentChargeError($subscriptionTransactionArgs->get_error_message(), $submission, $transaction, false, 'customer');
108 112 }
109 113
110 114 $subscriptionArgs = [
111 115 'customer' => $customer->id,
@@ -152,9 +156,9 @@
152 156 $invoice = Invoice::retrieve(
153 157 $subscriptionPayment->latest_invoice,
154 158 $this->form->id,
155 159 [
156 - 'expand' => ['payment_intent.charges']
160 + 'expand' => ['payment_intent.charges'],
157 161 ]
158 162 );
159 163 if (is_wp_error($invoice)) {
160 164 $this->handlePaymentChargeError($invoice->get_error_message(), $submission, $transaction, false, 'invoice');
@@ -172,9 +176,9 @@
172 176 $this->processScaBeforeVerification($submission->form_id, $submission->id, $transactionId, $invoice->payment_intent->id);
173 177
174 178 $nonceAction = 'fluentform_sca_confirm_' . $submission->id;
175 179 $nonce = wp_create_nonce($nonceAction);
176 -
180 +
177 181 wp_send_json_success([
178 182 'nextAction' => 'payment',
179 183 'actionName' => 'stripeSetupIntent',
180 184 'stripe_subscription_id' => $subscriptionPayment->id,
@@ -186,10 +190,10 @@
186 190 'client_secret' => $invoice->payment_intent->client_secret,
187 191 '_ff_stripe_nonce' => $nonce,
188 192 'message' => __('Verifying your card details. Please wait...', 'fluentform'),
189 193 'result' => [
190 - 'insert_id' => $submission->id
191 - ]
194 + 'insert_id' => $submission->id,
195 + ],
192 196 ], 200);
193 197 }
194 198
195 199 // now this payment is successful. We don't need anything else
@@ -200,15 +204,15 @@
200 204 {
201 205 $customerArgs = [
202 206 'payment_method' => $paymentMethodId,
203 207 'invoice_settings' => [
204 - 'default_payment_method' => $paymentMethodId
208 + 'default_payment_method' => $paymentMethodId,
205 209 ],
206 210 'metadata' => [
207 211 'submission_id' => $submission->id,
208 212 'form_id' => $submission->form_id,
209 - 'form_name' => wp_strip_all_tags($this->form->title)
210 - ]
213 + 'form_name' => wp_strip_all_tags($this->form->title),
214 + ],
211 215 ];
212 216
213 217 $receiptEmail = PaymentHelper::getCustomerEmail($submission, $this->form);
214 218
@@ -222,19 +226,19 @@
222 226 $customerArgs['name'] = $receiptName;
223 227 $customerArgs['description'] = $receiptName;
224 228 }
225 229
226 - $address = PaymentHelper::getCustomerAddress($submission);
227 - if ($address) {
228 - $customerArgs['address'] = [
229 - 'city' => ArrayHelper::get($address, 'city'),
230 - 'country' => ArrayHelper::get($address, 'country'),
231 - 'line1' => ArrayHelper::get($address, 'address_line_1'),
232 - 'line2' => ArrayHelper::get($address, 'address_line_2'),
233 - 'postal_code' => ArrayHelper::get($address, 'zip'),
234 - 'state' => ArrayHelper::get($address, 'state'),
235 - ];
236 - }
230 + $address = PaymentHelper::getCustomerAddress($submission);
231 + if ($address) {
232 + $customerArgs['address'] = [
233 + 'city' => ArrayHelper::get($address, 'city'),
234 + 'country' => ArrayHelper::get($address, 'country'),
235 + 'line1' => ArrayHelper::get($address, 'address_line_1'),
236 + 'line2' => ArrayHelper::get($address, 'address_line_2'),
237 + 'postal_code' => ArrayHelper::get($address, 'zip'),
238 + 'state' => ArrayHelper::get($address, 'state'),
239 + ];
240 + }
237 241
238 242 return $customerArgs;
239 243 }
240 244
@@ -241,14 +245,16 @@
241 245 protected function handlePaidSubscriptionInvoice($invoice, $submission)
242 246 {
243 247 if ($invoice->status !== 'paid') {
244 248 wp_send_json([
245 - 'errors' => __('Stripe Error: Payment Failed! Please try again.', 'fluentform')
249 + 'errors' => __('Stripe Error: Payment Failed! Please try again.', 'fluentform'),
246 250 ], 423);
247 251 }
248 252
249 - // Submission status as paid
250 - $this->changeSubmissionPaymentStatus('paid');
253 + // was: an unconditional write; a refund recorded after the guard's read was overwritten with paid
254 + if (!$this->changeSubmissionPaymentStatusUnlessReversed('paid')) {
255 + $this->refuseIfReversedMeanwhile($submission, null, null);
256 + }
251 257
252 258 $subscriptions = $this->getSubscriptions();
253 259
254 260 $this->processSubscriptionSuccess($subscriptions, $invoice, $submission);
@@ -254,10 +260,13 @@
254 260 $this->processSubscriptionSuccess($subscriptions, $invoice, $submission);
255 261
256 262 $transaction = $this->getLastTransaction($submission->id);
257 263
258 - $paymentStatus = $this->getIntentSuccessName($invoice->payment_intent);
259 - $this->processOnetimeSuccess($invoice, $transaction, $paymentStatus);
264 + // was: intent only; a $0 invoice has none, so the invoice decides
265 + $paymentStatus = $this->getIntentSuccessName($invoice->payment_intent, $invoice);
266 + if (!$this->processOnetimeSuccess($invoice, $transaction, $paymentStatus)) {
267 + $this->refuseIfReversedMeanwhile($submission, $transaction, null);
268 + }
260 269
261 270 $this->recalculatePaidTotal();
262 271
263 272 $this->sendSuccess($submission);
@@ -279,9 +288,9 @@
279 288
280 289 $intent = SCA::createPaymentIntent($intentArgs, $this->form->id);
281 290
282 291 if (is_wp_error($intent)) {
283 - $this->handlePaymentChargeError($intent->get_error_message(), $submission, $transaction, false, 'payment_intent');
292 + $this->handlePaymentChargeError($intent->get_error_message(), $submission, $transaction, false, 'payment_intent', $intent);
284 293 }
285 294
286 295 if (
287 296 $intent->status == 'requires_action' &&
@@ -292,9 +301,9 @@
292 301
293 302 // Generate nonce for secure SCA confirmation
294 303 $nonceAction = 'fluentform_sca_confirm_' . $submission->id;
295 304 $nonce = wp_create_nonce($nonceAction);
296 -
305 +
297 306 # Tell the client to handle the action
298 307 wp_send_json_success([
299 308 'nextAction' => 'payment',
300 309 'actionName' => 'initStripeSCAModal',
@@ -302,13 +311,13 @@
302 311 'client_secret' => $intent->client_secret,
303 312 '_ff_stripe_nonce' => $nonce,
304 313 'message' => apply_filters('fluentform/stripe_strong_customer_verify_waiting_message', __('Verifying strong customer authentication. Please wait...', 'fluentform')),
305 314 'result' => [
306 - 'insert_id' => $submission->id
307 - ]
315 + 'insert_id' => $submission->id,
316 + ],
308 317 ], 200);
309 318
310 - } else if ($intent->status == 'succeeded') {
319 + } elseif ('succeeded' == $intent->status) {
311 320 // Payment is succeeded here
312 321 $charge = $intent->charges->data[0];
313 322
314 323 $this->handlePaymentSuccess($charge, $transaction, $submission);
@@ -328,9 +337,9 @@
328 337 $transactionData = [
329 338 'charge_id' => $charge->payment_intent,
330 339 'payment_method' => 'stripe',
331 340 'payment_mode' => $this->getPaymentMode(),
332 - 'payment_note' => maybe_serialize($charge)
341 + 'payment_note' => maybe_serialize($charge),
333 342 ];
334 343
335 344 $methodDetails = $charge->payment_method_details;
336 345 if ($methodDetails && !empty($methodDetails->card)) {
@@ -339,9 +348,12 @@
339 348 }
340 349
341 350 $this->updateTransaction($transaction->id, $transactionData);
342 351
343 - $this->changeTransactionStatus($transaction->id, 'paid');
352 + // was: an unconditional write; a refund recorded after the guard's read was overwritten with paid
353 + if (!$this->changeTransactionStatusUnlessReversed($transaction->id, 'paid')) {
354 + $this->refuseIfReversedMeanwhile($submission, $transaction, null);
355 + }
344 356
345 357 $logData = [
346 358 'parent_source_id' => $submission->form_id,
347 359 'source_type' => 'submission_item',
@@ -348,18 +360,23 @@
348 360 'source_id' => $submission->id,
349 361 'component' => 'Payment',
350 362 'status' => 'info',
351 363 'title' => __('Payment Status changed', 'fluentform'),
352 - 'description' => __('Payment status changed to paid', 'fluentform')
364 + 'description' => __('Payment status changed to paid', 'fluentform'),
353 365 ];
354 366
355 367 do_action('fluentform/log_data', $logData);
356 368
357 369 $this->updateSubmission($submission->id, [
358 - 'payment_status' => 'paid',
359 370 'payment_method' => 'stripe',
360 371 ]);
361 372
373 + // Trigger fluentform/after_payment_status_change (via BaseProcessor),
374 + // consistent with hosted Stripe checkout and offline payment flows.
375 + if (!$this->changeSubmissionPaymentStatusUnlessReversed('paid')) {
376 + $this->refuseIfReversedMeanwhile($submission, $transaction, null);
377 + }
378 +
362 379 $logData = [
363 380 'parent_source_id' => $submission->form_id,
364 381 'source_type' => 'submission_item',
365 382 'source_id' => $submission->id,
@@ -365,9 +382,9 @@
365 382 'source_id' => $submission->id,
366 383 'component' => 'Payment',
367 384 'status' => 'success',
368 385 'title' => __('Payment Complete', 'fluentform'),
369 - 'description' => __('One time Payment Successfully made via Stripe. Charge ID: ', 'fluentform') . $charge->id
386 + 'description' => __('One time Payment Successfully made via Stripe. Charge ID: ', 'fluentform') . $charge->id,
370 387 ];
371 388
372 389 do_action('fluentform/log_data', $logData);
373 390
@@ -415,33 +432,37 @@
415 432 if (!$submission || !$submission->id) {
416 433 return new \WP_Error('invalid_submission', __('Invalid submission.', 'fluentform'));
417 434 }
418 435
419 -
420 - if ($submission->payment_status === 'paid') {
436 + // was: rejected a submission already 'paid'; that is now the recovery case, a reversed one is what must never be confirmed
437 + if (PaymentHelper::isReversedPaymentStatus($submission->payment_status)) {
421 438 return new \WP_Error(
422 - 'already_paid',
423 - __('This payment has already been completed and cannot be modified.', 'fluentform')
439 + 'payment_reversed',
440 + __('This payment has been reversed and cannot be confirmed.', 'fluentform')
424 441 );
425 442 }
426 443
427 - // Transaction must exist and be in 'intended' status (set by processScaBeforeVerification
428 - // when the SCA flow starts). A 'pending' transaction means SCA was never initiated,
429 - // 'paid'/'failed' means it's already been processed.
430 444 if (!$transaction) {
431 445 return new \WP_Error('no_transaction', __('No transaction found for this submission.', 'fluentform'));
432 446 }
433 447
434 - if ($transaction->status !== 'intended') {
448 + // 'intended' is written when the 3DS challenge starts. If the charge.succeeded webhook
449 + // lands before the browser returns, this row is already 'paid' (or 'processing') while
450 + // the submission and subscription are still unfinished; the browser must complete them.
451 + $isAwaitingBrowserConfirmation = 'intended' === $transaction->status;
452 + $wasSettledByWebhookBeforeBrowserReturned = in_array($transaction->status, ['processing', 'paid'], true);
453 +
454 + if (!$isAwaitingBrowserConfirmation && !$wasSettledByWebhookBeforeBrowserReturned) {
435 455 return new \WP_Error(
436 456 'invalid_transaction_status',
437 - __('This transaction is not awaiting payment confirmation.', 'fluentform')
457 + __('This transaction is not an active payment attempt and cannot be confirmed.', 'fluentform')
438 458 );
439 459 }
440 460
441 461 // Verify the payment intent ID matches what was stored during SCA initiation.
442 462 // processScaBeforeVerification() stores the intent as charge_id.
443 - if ($transaction->charge_id && $transaction->charge_id !== $paymentIntentId) {
463 + // was: skipped when charge_id was empty; the intent binding is the identity check, so it is required
464 + if (!$transaction->charge_id || $transaction->charge_id !== $paymentIntentId) {
444 465 return new \WP_Error(
445 466 'payment_intent_mismatch',
446 467 __('Payment verification failed. Payment intent does not match.', 'fluentform')
447 468 );
@@ -455,9 +476,9 @@
455 476 'source_id' => $submission->id,
456 477 'component' => 'Payment',
457 478 'status' => 'warning',
458 479 'title' => __('Stripe SCA Security Warning', 'fluentform'),
459 - 'description' => implode('; ', $warnings)
480 + 'description' => implode('; ', $warnings),
460 481 ];
461 482 do_action('fluentform/log_data', $logData);
462 483 }
463 484
@@ -462,15 +483,15 @@
462 483 }
463 484
464 485 return [
465 486 'valid' => true,
466 - 'warnings' => $warnings
487 + 'warnings' => $warnings,
467 488 ];
468 489 }
469 490
470 491 public function confirmScaPayment()
471 492 {
472 - $submissionId = isset($_REQUEST['submission_id']) ? (int)$_REQUEST['submission_id'] : 0;
493 + $submissionId = isset($_REQUEST['submission_id']) ? (int) $_REQUEST['submission_id'] : 0;
473 494 $paymentMethod = isset($_REQUEST['payment_method']) ? sanitize_text_field(wp_unslash($_REQUEST['payment_method'])) : '';
474 495 $paymentIntentId = isset($_REQUEST['payment_intent_id']) ? sanitize_text_field(wp_unslash($_REQUEST['payment_intent_id'])) : '';
475 496
476 497 $this->setSubmissionId($submissionId);
@@ -482,27 +503,70 @@
482 503 $validation = $this->validateScaRequest($submissionId, $paymentIntentId, $submission, $transaction);
483 504
484 505 if (is_wp_error($validation)) {
485 506 wp_send_json([
486 - 'errors' => $validation->get_error_message()
507 + 'errors' => $validation->get_error_message(),
487 508 ], 423);
488 509 }
489 510
511 + // was: re-ran the status writers, so a repeated callback fired the payment-status hooks again
512 + if ($this->isPaymentAlreadyCompleted($submission, $transaction)) {
513 + $this->sendSuccess($submission);
514 + }
515 +
490 516 // Use submission's form_id rather than trusting $_REQUEST
491 517 $formId = $submission->form_id;
492 518
493 - $confirmation = SCA::confirmPayment($paymentIntentId, [
494 - 'payment_method' => $paymentMethod
495 - ], $formId);
519 + // was: confirmed blindly; the webhook may already have settled this intent, and a Stripe outage marked the payment failed
520 + $confirmation = SCA::retrievePaymentIntent($paymentIntentId, [], $formId);
496 521
497 522 if (is_wp_error($confirmation)) {
498 - $message = 'Payment has been failed. ' . $confirmation->get_error_message();
499 - $this->handlePaymentChargeError($message, $submission, $transaction, $confirmation, 'payment_error');
523 + $this->sendRetryableVerificationError($submission, $confirmation);
500 524 }
501 525
526 + // Confirming an intent Stripe has already settled counts against its confirm limit.
527 + if ('requires_confirmation' === $confirmation->status) {
528 + $confirmation = SCA::confirmPayment($paymentIntentId, [
529 + 'payment_method' => $paymentMethod,
530 + ], $formId);
531 +
532 + if (is_wp_error($confirmation)) {
533 + $message = 'Payment has been failed. ' . $confirmation->get_error_message();
534 + $this->handlePaymentChargeError($message, $submission, $transaction, $confirmation, 'payment_error');
535 + }
536 + }
537 +
502 538 if ($confirmation->status == 'succeeded') {
503 539 $charge = $confirmation->charges->data[0];
504 540
541 + // was: settled from the objects read before the Stripe round-trip; a refund recorded meanwhile,
542 + // or one Stripe already reports on the charge, was overwritten with paid and fulfilled
543 + $this->refuseIfReversedMeanwhile($submission, $transaction, $charge);
544 +
545 + $confirmedCurrency = strtolower((string) $confirmation->currency);
546 + $transactionCurrency = strtolower((string) $transaction->currency);
547 + if (!$confirmedCurrency || $confirmedCurrency !== $transactionCurrency) {
548 + $logData = [
549 + 'parent_source_id' => $submission->form_id,
550 + 'source_type' => 'submission_item',
551 + 'source_id' => $submission->id,
552 + 'component' => 'Payment',
553 + 'status' => 'error',
554 + 'title' => __('Stripe Currency Mismatch', 'fluentform'),
555 + 'description' => sprintf(
556 + // translators: %1$s is the expected currency, %2$s is the confirmed currency
557 + __('Expected %1$s but Stripe confirmed %2$s. Payment rejected.', 'fluentform'),
558 + strtoupper($transactionCurrency),
559 + strtoupper($confirmedCurrency)
560 + ),
561 + ];
562 + do_action('fluentform/log_data', $logData);
563 +
564 + wp_send_json([
565 + 'errors' => __('Payment currency verification failed.', 'fluentform'),
566 + ], 423);
567 + }
568 +
505 569 // Verify the confirmed amount matches the transaction amount.
506 570 // Normalize for zero-decimal currencies: FluentForm stores amounts x100 internally,
507 571 // but Stripe returns amounts in the currency's smallest unit (e.g. yen for JPY).
508 572 $confirmedAmount = (int) $confirmation->amount;
@@ -521,18 +585,23 @@
521 585 // translators: %1$d is the expected amount, %2$d is the confirmed amount
522 586 __('Expected %1$d but Stripe confirmed %2$d. Payment rejected.', 'fluentform'),
523 587 intval($transaction->payment_total),
524 588 intval($confirmation->amount)
525 - )
589 + ),
526 590 ];
527 591 do_action('fluentform/log_data', $logData);
528 592
529 593 wp_send_json([
530 - 'errors' => __('Payment amount verification failed.', 'fluentform')
594 + 'errors' => __('Payment amount verification failed.', 'fluentform'),
531 595 ], 423);
532 596 }
533 597
534 598 $this->handlePaymentSuccess($charge, $transaction, $submission);
599 + } elseif ('processing' === $confirmation->status) {
600 + // was: fell through to failed; Stripe settles a delayed method later by webhook, so nothing is decided yet
601 + wp_send_json([
602 + 'errors' => __('Your payment is still being processed. You will be notified once it completes.', 'fluentform'),
603 + ], 423);
535 604 } else {
536 605 $this->handlePaymentChargeError('We could not verify your payment. Please try again', $submission, $transaction, $confirmation, 'payment_error');
537 606 }
538 607 }
@@ -552,12 +621,17 @@
552 621 $validation = $this->validateScaRequest($submissionId, $intentId, $submission, $transaction);
553 622
554 623 if (is_wp_error($validation)) {
555 624 wp_send_json([
556 - 'errors' => $validation->get_error_message()
625 + 'errors' => $validation->get_error_message(),
557 626 ], 423);
558 627 }
559 628
629 + // was: re-ran the status writers, so a repeated callback fired the payment-status hooks again
630 + if ($this->isPaymentAlreadyCompleted($submission, $transaction)) {
631 + $this->sendSuccess($submission);
632 + }
633 +
560 634 // Use submission's form_id rather than trusting $_REQUEST
561 635 $formId = $submission->form_id;
562 636
563 637 // Let's retrieve the intent
@@ -562,33 +636,98 @@
562 636
563 637 // Let's retrieve the intent
564 638 $intent = SCA::retrievePaymentIntent($intentId, [
565 639 'expand' => [
566 - 'invoice.payment_intent'
567 - ]
640 + 'invoice.payment_intent',
641 + ],
568 642 ], $formId);
569 643
644 + // was: handlePaymentChargeError(), which marked a possibly paid submission failed on a Stripe outage
570 645 if (is_wp_error($intent)) {
571 - $this->handlePaymentChargeError($intent->get_error_message(), $submission, false, false, 'payment_intent');
646 + $this->sendRetryableVerificationError($submission, $intent);
572 647 }
573 648
574 649 $invoice = $intent->invoice;
575 650
651 + // was: settled from the objects read before the Stripe round-trip
652 + $this->refuseIfReversedMeanwhile($submission, $transaction, $intent->charges->data[0] ?? null);
653 +
576 654 $this->handlePaidSubscriptionInvoice($invoice, $submission);
577 655 }
578 656
657 + // Re-read after the Stripe round-trip: a refund webhook may have run meanwhile, and Stripe's own
658 + // charge carries the refund state before that webhook arrives.
659 + protected function refuseIfReversedMeanwhile($submission, $transaction, $charge)
660 + {
661 + $current = Submission::find($submission->id);
662 + $currentTransaction = $transaction ? $this->getTransaction($transaction->id) : null;
663 +
664 + // A row deleted mid-request leaves nothing to settle, so it is refused like a reversal
665 + $rowVanished = !$current || ($transaction && !$currentTransaction);
666 + $reversedLocally = $rowVanished
667 + || PaymentHelper::isReversedPaymentStatus($current->payment_status)
668 + || ($currentTransaction && PaymentHelper::isReversedPaymentStatus($currentTransaction->status));
669 + // was: full refunds only, while the local check already treats partially-refunded as reversed
670 + $refundedAtStripe = $charge && (!empty($charge->refunded) || !empty($charge->amount_refunded));
671 +
672 + if (!$reversedLocally && !$refundedAtStripe) {
673 + return;
674 + }
675 +
676 + do_action('fluentform/log_data', [
677 + 'parent_source_id' => $submission->form_id,
678 + 'source_type' => 'submission_item',
679 + 'source_id' => $submission->id,
680 + 'component' => 'Payment',
681 + 'status' => 'warning',
682 + 'title' => __('Stripe confirmation refused', 'fluentform'),
683 + 'description' => $reversedLocally
684 + ? __('The payment was reversed before the confirmation completed.', 'fluentform')
685 + : __('Stripe reports the charge as refunded.', 'fluentform'),
686 + ]);
687 +
688 + wp_send_json([
689 + 'errors' => __('This payment has been refunded and cannot be confirmed.', 'fluentform'),
690 + ], 423);
691 + }
692 +
693 + // Transaction paid, submission paid and actions fired: nothing is left for a callback to finish.
694 + protected function isPaymentAlreadyCompleted($submission, $transaction)
695 + {
696 + return 'paid' === $transaction->status
697 + && 'paid' === $submission->payment_status
698 + && 'yes' === $this->getMetaData('is_form_action_fired');
699 + }
700 +
701 + // The outcome is unknown, not failed: no failure hooks, no status downgrade.
702 + protected function sendRetryableVerificationError($submission, \WP_Error $error)
703 + {
704 + do_action('fluentform/log_data', [
705 + 'parent_source_id' => $submission->form_id,
706 + 'source_type' => 'submission_item',
707 + 'source_id' => $submission->id,
708 + 'component' => 'Payment',
709 + 'status' => 'warning',
710 + 'title' => __('Stripe verification deferred', 'fluentform'),
711 + 'description' => $error->get_error_message(),
712 + ]);
713 +
714 + wp_send_json([
715 + 'errors' => __('We could not reach Stripe to verify your payment. Please try again in a moment.', 'fluentform'),
716 + ], 423);
717 + }
718 +
579 719 protected function sendSuccess($submission)
580 720 {
581 721 try {
582 722 $returnData = $this->getReturnData();
583 723 wp_send_json_success($returnData, 200);
584 -
724 +
585 725 } catch (\Exception $e) {
586 726 wp_send_json([
587 - 'errors' => $e->getMessage()
727 + 'errors' => $e->getMessage(),
588 728 ], 423);
589 729 }
590 -
591 730 }
592 731
593 732 protected function processScaBeforeVerification($formId, $submissionId, $transactionId, $chargeId)
594 733 {
@@ -594,9 +733,9 @@
594 733 {
595 734 if ($transactionId) {
596 735 $this->updateTransaction($transactionId, [
597 736 'charge_id' => $chargeId,
598 - 'payment_mode' => $this->getPaymentMode()
737 + 'payment_mode' => $this->getPaymentMode(),
599 738 ]);
600 739
601 740 $this->changeTransactionStatus($transactionId, 'intended');
602 741 }
@@ -607,16 +746,17 @@
607 746 'source_id' => $submissionId,
608 747 'component' => 'Payment',
609 748 'status' => 'info',
610 749 'title' => __('Stripe SCA Required', 'fluentform'),
611 - 'description' => __('SCA is required for this payment. Requested SCA info from customer', 'fluentform')
750 + 'description' => __('SCA is required for this payment. Requested SCA info from customer', 'fluentform'),
612 751 ];
613 752
614 753 do_action('fluentform/log_data', $logData);
615 754 }
616 -
755 +
617 756 /**
618 757 * Products name comma separated
758 + *
619 759 * @return string
620 760 */
621 761 public function getProductNames()
622 762 {
@@ -622,12 +762,11 @@
622 762 {
623 763 $orderItems = $this->getOrderItems();
624 764 $itemsHtml = '';
625 765 foreach ($orderItems as $item) {
626 - $itemsHtml != "" && $itemsHtml .= ", ";
627 - $itemsHtml .= $item->item_name ;
766 + '' != $itemsHtml && $itemsHtml .= ', ';
767 + $itemsHtml .= $item->item_name;
628 768 }
629 -
769 +
630 770 return $itemsHtml;
631 771 }
632 -
633 772 }