PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
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 3.6.65 All 196 releases
fluentform / app / Modules / Payments / PaymentMethods / BaseProcessor.php

BaseProcessor.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at app/Modules/Payments/PaymentMethods/BaseProcessor.php

1,212 lines 42.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Modules\Payments\PaymentMethods;
4
5 if (!defined('ABSPATH')) {
6 exit; // Exit if accessed directly.
7 }
8
9 use FluentForm\App\Helpers\Helper;
10 use FluentForm\App\Models\Form;
11 use FluentForm\App\Models\OrderItem;
12 use FluentForm\App\Models\Submission;
13 use FluentForm\App\Models\Subscription;
14 use FluentForm\App\Models\SubmissionMeta;
15 use FluentForm\App\Models\Transaction;
16 use FluentForm\App\Modules\Payments\PaymentHelper;
17 use FluentForm\Framework\Helpers\ArrayHelper;
18 use FluentForm\App\Services\Form\SubmissionHandlerService;
19
20 abstract class BaseProcessor
21 {
22 protected $method;
23
24 protected $form = null;
25
26 protected $submission = null;
27
28 protected $submissionId = null;
29
30 public function init()
31 {
32 add_action('fluentform/process_payment_' . $this->method, array($this, 'handlePaymentAction'), 10, 6);
33 }
34
35 public abstract function handlePaymentAction($submissionId, $submissionData, $form, $methodSettings, $hasSubscriptions, $totalPayable);
36
37 public function setSubmissionId($submissionId)
38 {
39 $this->submissionId = $submissionId;
40 }
41
42 public function getSubmissionId()
43 {
44 return $this->submissionId;
45 }
46
47 public function insertTransaction($data)
48 {
49 if (empty($data['transaction_type'])) {
50 $data['transaction_type'] = 'onetime';
51 }
52
53 $data = wp_parse_args($data, $this->getTransactionDefaults());
54
55 if (empty($data['transaction_hash'])) {
56 $data['transaction_hash'] = wp_generate_password(32, false);
57 }
58
59 return Transaction::create($data)->id;
60 }
61
62 public function insertRefund($data)
63 {
64 $submission = $this->getSubmission();
65 $data['created_at'] = current_time('mysql');
66 $data['updated_at'] = current_time('mysql');
67 $data['form_id'] = $submission->form_id;
68 $data['submission_id'] = $submission->id;
69 $data['payment_method'] = $this->method;
70 if (empty($data['transaction_type'])) {
71 $data['transaction_type'] = 'refund';
72 }
73
74 if ($userId = get_current_user_id()) {
75 $data['user_id'] = $userId;
76 }
77
78 if (empty($data['transaction_hash'])) {
79 $data['transaction_hash'] = wp_generate_password(32, false);
80 }
81
82 return Transaction::create($data)->id;
83 }
84
85 public function getTransaction($transactionId, $column = 'id')
86 {
87 return Transaction::where($column, $transactionId)->first();
88 }
89
90 public function getRefund($refundId, $column = 'id')
91 {
92 return Transaction::where($column, $refundId)
93 ->refunds()
94 ->first();
95 }
96
97 public function getTransactionByChargeId($chargeId)
98 {
99 return Transaction::bySubmission($this->submissionId)
100 ->where('charge_id', $chargeId)
101 ->first();
102 }
103
104 public function getLastTransaction($submissionId)
105 {
106 // was: any row, so a refund ledger row inserted meanwhile became the row being settled
107 return Transaction::bySubmission($submissionId)
108 ->where('transaction_type', '!=', 'refund')
109 ->orderBy('id', 'DESC')
110 ->first();
111 }
112
113 public function changeSubmissionPaymentStatus($newStatus)
114 {
115 return $this->writeSubmissionPaymentStatus($newStatus, false);
116 }
117
118 public function changeSubmissionPaymentStatusUnlessReversed($newStatus)
119 {
120 return $this->writeSubmissionPaymentStatus($newStatus, true);
121 }
122
123 protected function writeSubmissionPaymentStatus($newStatus, $unlessReversed)
124 {
125 do_action_deprecated(
126 'fluentform_before_payment_status_change',
127 [
128 $newStatus,
129 $this->getSubmission()
130 ],
131 FLUENTFORM_FRAMEWORK_UPGRADE,
132 'fluentform/before_payment_status_change',
133 'Use fluentform/before_payment_status_change instead of fluentform_before_payment_status_change.'
134 );
135
136 do_action('fluentform/before_payment_status_change', $newStatus, $this->getSubmission());
137
138 Submission::where('id', $this->submissionId)
139 ->when($unlessReversed, function ($query) {
140 $query->whereNotIn('payment_status', PaymentHelper::reversedPaymentStatuses());
141 })
142 ->update([
143 'payment_status' => $newStatus,
144 'updated_at' => current_time('mysql')
145 ]);
146
147 $this->submission = null;
148
149 // A reversal recorded between the caller's read and this write keeps the row; the caller decides
150 if ($unlessReversed) {
151 $submission = $this->getSubmission();
152 if (!$submission || PaymentHelper::isReversedPaymentStatus($submission->payment_status)) {
153 return false;
154 }
155 }
156
157 $logData = [
158 'parent_source_id' => $this->getForm()->id,
159 'source_type' => 'submission_item',
160 'source_id' => $this->submissionId,
161 'component' => 'Payment',
162 'status' => 'paid' === $newStatus ? 'success' : $newStatus,
163 'title' => __('Payment Status changed', 'fluentform'),
164 'description' => __('Payment status changed to ', 'fluentform') . $newStatus
165 ];
166
167 do_action('fluentform/log_data', $logData);
168
169 do_action_deprecated(
170 'fluentform_after_payment_status_change',
171 [
172 $newStatus,
173 $this->getSubmission()
174 ],
175 FLUENTFORM_FRAMEWORK_UPGRADE,
176 'fluentform/after_payment_status_change',
177 'Use fluentform/after_payment_status_change instead of fluentform_after_payment_status_change.'
178 );
179
180 do_action('fluentform/after_payment_status_change', $newStatus, $this->getSubmission());
181
182 return true;
183 }
184
185 protected function shouldRunSubmissionActions()
186 {
187 $current = Submission::find($this->submissionId);
188
189 return $current && apply_filters('fluentform/should_process_submission_actions', true, $current, $this->getForm());
190 }
191
192 public function recalculatePaidTotal()
193 {
194 // was: the paid scope also matched the refund rows themselves (status refunded), so a full refund
195 // summed gross + refund - refund and total_paid stayed at the gross amount
196 $transactions = Transaction::bySubmission($this->submissionId)
197 ->paid()
198 ->where('transaction_type', '!=', 'refund')
199 ->get();
200
201 $total = 0;
202 $subscriptionId = false;
203
204 foreach ($transactions as $transaction) {
205 $total += $transaction->payment_total;
206 if($transaction->subscription_id) {
207 $subscriptionId = $transaction->subscription_id;
208 }
209 }
210
211 $refunds = $this->getRefundTotal();
212 if ($refunds) {
213 $total = $total - $refunds;
214 }
215
216 Submission::where('id', $this->submissionId)
217 ->update([
218 'total_paid' => $total,
219 'updated_at' => current_time('mysql')
220 ]);
221
222 // was: its own count of every paid-scope row here, so a $0 trial start became installment one
223 if ($subscriptionId) {
224 list($installmentCount) = $this->getPaymentCountsAndTotal($subscriptionId);
225 $this->updateSubscription($subscriptionId, [
226 'bill_count' => $installmentCount
227 ]);
228 }
229 }
230
231 public function getRefundTotal()
232 {
233 $refunds = Transaction::bySubmission($this->submissionId)
234 ->refunds()
235 ->get();
236
237 $total = 0;
238 foreach ($refunds as $refund) {
239 $total += $refund->payment_total;
240 }
241
242 return $total;
243 }
244
245 public function changeTransactionStatus($transactionId, $newStatus)
246 {
247 return $this->writeTransactionStatus($transactionId, $newStatus, false);
248 }
249
250 public function changeTransactionStatusUnlessReversed($transactionId, $newStatus)
251 {
252 return $this->writeTransactionStatus($transactionId, $newStatus, true);
253 }
254
255 protected function writeTransactionStatus($transactionId, $newStatus, $unlessReversed)
256 {
257 do_action_deprecated(
258 'fluentform_before_transaction_status_change',
259 [
260 $newStatus,
261 $this->getSubmission(),
262 $transactionId
263 ],
264 FLUENTFORM_FRAMEWORK_UPGRADE,
265 'fluentform/before_transaction_status_change',
266 'Use fluentform/before_transaction_status_change instead of fluentform_before_transaction_status_change.'
267 );
268
269 do_action(
270 'fluentform/before_transaction_status_change',
271 $newStatus,
272 $this->getSubmission(),
273 $transactionId
274 );
275
276 Transaction::where('id', $transactionId)
277 ->when($unlessReversed, function ($query) {
278 $query->whereNotIn('status', PaymentHelper::reversedPaymentStatuses());
279 })
280 ->update([
281 'status' => $newStatus,
282 'updated_at' => current_time('mysql')
283 ]);
284
285 if ($unlessReversed) {
286 $transaction = $this->getTransaction($transactionId);
287 if (!$transaction || PaymentHelper::isReversedPaymentStatus($transaction->status)) {
288 return false;
289 }
290 }
291
292 do_action_deprecated(
293 'fluentform_after_transaction_status_change',
294 [
295 $newStatus,
296 $this->getSubmission(),
297 $transactionId
298 ],
299 FLUENTFORM_FRAMEWORK_UPGRADE,
300 'fluentform/after_transaction_status_change',
301 'Use fluentform/after_transaction_status_change instead of fluentform_after_transaction_status_change.'
302 );
303
304 do_action(
305 'fluentform/after_transaction_status_change',
306 $newStatus,
307 $this->getSubmission(),
308 $transactionId
309 );
310
311 return true;
312 }
313
314 public function updateTransaction($transactionId, $data)
315 {
316 $data['updated_at'] = current_time('mysql');
317
318 return Transaction::where('id', $transactionId)->update($data);
319 }
320
321 public function completePaymentSubmission($isAjax = true)
322 {
323 $returnData = $this->getReturnData();
324 if ($isAjax) {
325 wp_send_json_success($returnData, 200);
326 }
327 return $returnData;
328 }
329
330 public function getReturnData()
331 {
332 $submission = $this->getSubmission();
333 try {
334 $submissionService = new SubmissionHandlerService();
335 if ($this->getMetaData('is_form_action_fired') == 'yes') {
336 $data = $submissionService->getReturnData($submission->id, $this->getForm(),
337 $submission->response);
338
339 $returnData = [
340 'insert_id' => $submission->id,
341 'result' => $data,
342 'error' => ''
343 ];
344 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is checking for payment gateway webhook notification
345 if (!isset($_REQUEST['fluentform_payment_api_notify'])) {
346 // now we have to check if we need this user as auto login
347 if ($loginId = $this->getMetaData('_make_auto_login')) {
348 $this->maybeAutoLogin($loginId, $submission);
349 }
350 }
351 } elseif (!$this->shouldRunSubmissionActions()) {
352 // was: fired the actions unconditionally; a reversal recorded since the paid write is
353 // skipped here as it is on the deferred pipeline (PaymentHandler::skipActionsForReversedPayment)
354 $returnData = [
355 'insert_id' => $submission->id,
356 'result' => $submissionService->getReturnData($submission->id, $this->getForm(), $submission->response),
357 'error' => '',
358 ];
359 } else {
360 $returnData = $submissionService->processSubmissionData(
361 $this->submissionId, $submission->response, $this->getForm()
362 );
363 $this->setMetaData('is_form_action_fired', 'yes');
364 }
365 return $returnData;
366
367 } catch (\Exception $e) {
368 return [
369 'insert_id' => $submission->id,
370 'result' => '',
371 'error' => $e->getMessage(),
372 ];
373 }
374
375 }
376
377 public function getSubmission()
378 {
379 if (!is_null($this->submission)) {
380 return $this->submission;
381 }
382
383 $submission = Submission::where('id', $this->submissionId)->first();
384
385 if (!$submission) {
386 return false;
387 }
388
389 $submission->response = json_decode($submission->response, true);
390
391 $this->submission = $submission;
392
393 return $this->submission;
394
395 }
396
397 public function getForm()
398 {
399 if (!is_null($this->form)) {
400 return $this->form;
401 }
402
403 $submission = $this->getSubmission();
404
405 $this->form = Form::where('id', $submission->form_id)->first();
406
407 return $this->form;
408 }
409
410 public function getOrderItems()
411 {
412 return OrderItem::bySubmission($this->submissionId)
413 ->products()
414 ->get();
415 }
416
417 public function getDiscountItems()
418 {
419 return OrderItem::bySubmission($this->submissionId)
420 ->discounts()
421 ->get();
422 }
423
424 public function setMetaData($name, $value)
425 {
426 $value = maybe_serialize($value);
427
428 return SubmissionMeta::create([
429 'response_id' => $this->getSubmissionId(),
430 'form_id' => $this->getForm()->id,
431 'meta_key' => $name,
432 'value' => $value,
433 'created_at' => current_time('mysql'),
434 'updated_at' => current_time('mysql')
435 ])->id;
436 }
437
438 public function deleteMetaData($name)
439 {
440 return SubmissionMeta::where('meta_key', $name)
441 ->where('response_id', $this->getSubmissionId())
442 ->delete();
443 }
444
445 public function getMetaData($metaKey)
446 {
447 $meta = SubmissionMeta::where('response_id', $this->getSubmissionId())
448 ->where('meta_key', $metaKey)
449 ->first();
450
451 if ($meta && $meta->value) {
452 return Helper::safeUnserialize($meta->value);
453 }
454
455 return false;
456 }
457
458 public function showPaymentView($returnData)
459 {
460 $redirectUrl = ArrayHelper::get($returnData, 'result.redirectUrl');
461 if ($redirectUrl) {
462 wp_redirect($redirectUrl);
463 exit();
464 }
465
466 $form = $this->getForm();
467
468 if (!empty($returnData['title'])) {
469 $title = $returnData['title'];
470 } else if ($returnData['type'] == 'success') {
471 $title = __('Payment Success', 'fluentform');
472 $title = apply_filters('fluentform/payment_success_title', $title, $this->getSubmission(), $form);
473 } else {
474 $title = __('Payment Failed', 'fluentform');
475 $title = apply_filters('fluentform/payment_failed_title', $title, $this->getSubmission(), $form);
476 }
477
478 $message = $returnData['error'];
479 if (!$message) {
480 $message = $returnData['result']['message'];
481 }
482
483 // Apply payment message filter
484 if ($message) {
485 $message = apply_filters('fluentform/payment_confirmation_message', $message, $this->getSubmission(), $form);
486 }
487
488 $data = [
489 'status' => $returnData['type'],
490 'form' => $form,
491 'title' => $title,
492 'submission' => $this->getSubmission(),
493 'message' => $message,
494 'is_new' => $returnData['is_new'],
495 'data' => $returnData
496 ];
497
498 $data = apply_filters_deprecated(
499 'fluentform_frameless_page_data',
500 [
501 $data
502 ],
503 FLUENTFORM_FRAMEWORK_UPGRADE,
504 'fluentform/frameless_page_data',
505 'Use fluentform/frameless_page_data instead of fluentform_frameless_page_data.'
506 );
507
508 $data = apply_filters('fluentform/frameless_page_data', $data);
509
510 add_filter('pre_get_document_title', function ($title) use ($data) {
511 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- document_title_separator is a WordPress core hook
512 return $data['title'] . ' ' . apply_filters('document_title_separator', '-') . ' ' . $data['form']->title;
513 });
514
515 add_action('wp_enqueue_scripts', function () {
516 wp_enqueue_style('fluent-form-landing', fluentFormMix('css/frameless.css'), [], FLUENTFORM_VERSION);
517 });
518
519 status_header(200);
520 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- loadView() returns complete HTML template with pre-sanitized content
521 echo $this->loadView('frameless/frameless_page_view', $data);
522 exit(200);
523 }
524
525 public function loadView($view, $data = [])
526 {
527 $file = FLUENTFORM_DIR_PATH . 'app/Views/' . $view . '.php';
528 extract($data);
529 ob_start();
530 include($file);
531 return ob_get_clean();
532 }
533
534 public function refund($refund_amount, $transaction, $submission, $method = '', $refundId = '', $refundNote = 'Refunded')
535 {
536 $this->setSubmissionId($submission->id);
537 $status = 'refunded';
538
539 $alreadyRefunded = $this->getRefundTotal();
540
541 // Submission-wide, matching getRefundTotal(); a subscription's invoices share one submission.
542 $charged = (int) Transaction::bySubmission($submission->id)
543 ->where('transaction_type', '!=', 'refund')
544 ->whereIn('status', ['paid', 'processing', 'refunded', 'partially-refunded'])
545 ->sum('payment_total');
546
547 // round(), not intval(): gateways pass floats (PayPal does mc_gross * -100) and
548 // 19.99 * 100 is 1998.9999... - truncating would shave a cent off the refund.
549 $refund_amount = min((int) round($refund_amount), max(0, $charged - (int) round($alreadyRefunded)));
550
551 if ($refund_amount <= 0) {
552 return;
553 }
554
555 $totalRefund = intval($refund_amount + $alreadyRefunded);
556
557 if ($totalRefund < $transaction->payment_total) {
558 $status = 'partially-refunded';
559 }
560
561 $this->changeTransactionStatus($transaction->id, $status);
562 $this->changeSubmissionPaymentStatus($status);
563 $uniqueHash = wp_generate_password(32, false);
564
565 $refundData = [
566 'form_id' => $submission->form_id,
567 'submission_id' => $submission->id,
568 'transaction_hash' => $uniqueHash,
569 'payment_method' => $transaction->payment_method,
570 'charge_id' => $refundId,
571 'payment_note' => $refundNote,
572 'payment_total' => $refund_amount,
573 'currency' => $transaction->currency,
574 'payment_mode' => $transaction->payment_mode,
575 'created_at' => current_time('mysql'),
576 'updated_at' => current_time('mysql'),
577 'status' => 'refunded',
578 'transaction_type' => 'refund'
579 ];
580
581 $refundId = $this->insertRefund($refundData);
582
583 $logData = [
584 'parent_source_id' => $submission->form_id,
585 'source_type' => 'submission_item',
586 'source_id' => $submission->id,
587 'component' => 'Payment',
588 'status' => 'info',
589 'title' => __('Refund issued', 'fluentform'),
590 'description' => __('Refund issued and refund amount: ', 'fluentform') . number_format($refund_amount / 100, 2)
591 ];
592
593 do_action('fluentform/log_data', $logData);
594
595 $this->recalculatePaidTotal();
596 $refund = $this->getRefund($refundId);
597
598 do_action_deprecated(
599 'fluentform_payment_' . $status . '_' . $method,
600 [
601 $refund,
602 $transaction,
603 $submission
604 ],
605 FLUENTFORM_FRAMEWORK_UPGRADE,
606 'fluentform/payment_' . $status . '_' . $method,
607 'Use fluentform/payment_' . $status . '_' . $method . ' instead of fluentform_payment_' . $status . '_' . $method
608 );
609
610 do_action('fluentform/payment_' . $status . '_' . $method, $refund, $transaction, $submission);
611
612 do_action_deprecated(
613 'fluentform_payment_' . $status,
614 [
615 $refund,
616 $transaction,
617 $submission
618 ],
619 FLUENTFORM_FRAMEWORK_UPGRADE,
620 'fluentform/payment_' . $status,
621 'Use fluentform/payment_' . $status . ' instead of fluentform_payment_' . $status
622 );
623 do_action('fluentform/payment_' . $status, $refund, $transaction, $submission);
624 }
625
626 public function updateRefund($totalRefund, $transaction, $submission, $method = '', $refundId = '', $refundNote = 'Refunded')
627 {
628 if(!$totalRefund) {
629 return;
630 }
631
632 $this->setSubmissionId($submission->id);
633 $existingRefund = Transaction::bySubmission($submission->id)
634 ->refunds()
635 ->first();
636
637 if ($existingRefund) {
638
639 if ($existingRefund->payment_total == $totalRefund) {
640 return;
641 }
642
643 $status = 'refunded';
644 if ($totalRefund < $transaction->payment_total) {
645 $status = 'partially-refunded';
646 }
647 $updateData = [
648 'form_id' => $submission->form_id,
649 'submission_id' => $submission->id,
650 'payment_method' => $transaction->payment_method,
651 'charge_id' => $refundId,
652 'payment_note' => $refundNote,
653 'payment_total' => $totalRefund,
654 'payment_mode' => $transaction->payment_mode,
655 'created_at' => current_time('mysql'),
656 'updated_at' => current_time('mysql'),
657 'status' => 'refunded',
658 'transaction_type' => 'refund'
659 ];
660
661 Transaction::where('id', $existingRefund->id)->update($updateData);
662
663 $existingRefund = Transaction::bySubmission($submission->id)
664 ->refunds()
665 ->first();
666
667 if ($transaction->status != $status) {
668 $this->changeTransactionStatus($transaction->id, $status);
669 }
670
671 do_action_deprecated(
672 'fluentform_payment_refund_updated_' . $method,
673 [
674 $existingRefund,
675 $existingRefund->form_id
676 ],
677 FLUENTFORM_FRAMEWORK_UPGRADE,
678 'fluentform/payment_refund_updated_' . $method,
679 'Use fluentform/payment_refund_updated_' . $method . ' instead of fluentform_payment_refund_updated_' . $method
680 );
681
682 do_action('fluentform/payment_refund_updated_' . $method, $existingRefund, $existingRefund->form_id);
683
684 do_action_deprecated(
685 'fluentform_payment_refund_updated',
686 [
687 $existingRefund,
688 $existingRefund->form_id
689 ],
690 FLUENTFORM_FRAMEWORK_UPGRADE,
691 'fluentform/payment_refund_updated',
692 'Use fluentform/payment_refund_updated instead of fluentform_payment_refund_updated.'
693 );
694
695 do_action('fluentform/payment_refund_updated', $existingRefund, $existingRefund->form_id);
696
697 } else {
698 $this->refund($totalRefund, $transaction, $submission, $method, $refundId, $refundNote);
699 }
700 }
701
702 private function maybeAutoLogin($loginId, $submission)
703 {
704 if (is_user_logged_in() || !$loginId) {
705 return;
706 }
707 if ($loginId != $submission->user_id) {
708 return;
709 }
710
711 wp_clear_auth_cookie();
712 wp_set_current_user($loginId);
713 wp_set_auth_cookie($loginId);
714 $this->deleteMetaData('_make_auto_login');
715 }
716
717 public function getAmountTotal()
718 {
719 $orderItems = $this->getOrderItems();
720
721 $amountTotal = 0;
722 foreach ($orderItems as $item) {
723 $amountTotal += $item->line_total;
724 }
725
726 $discountItems = $this->getDiscountItems();
727 foreach ($discountItems as $discountItem) {
728 $amountTotal -= $discountItem->line_total;
729 }
730
731 return $amountTotal;
732 }
733
734 public function handleSessionRedirectBack($data)
735 {
736 $submissionId = intval($data['fluentform_payment']);
737 $this->setSubmissionId($submissionId);
738
739 $submission = $this->getSubmission();
740
741 $transactionHash = sanitize_text_field($data['transaction_hash']);
742 $transaction = $this->getTransaction($transactionHash, 'transaction_hash');
743
744 if (!$transaction || !$submission) {
745 return;
746 }
747
748 $type = $transaction->status;
749 $form = $this->getForm();
750
751 if ($type == 'paid') {
752 $returnData = $this->getReturnData();
753 } else {
754 $pendingTitle = __('Payment was not marked as paid', 'fluentform');
755 $pendingMessage = __('Looks like you have is still on pending status', 'fluentform');
756 $returnData = [
757 'insert_id' => $submission->id,
758 'title' => apply_filters('fluentform/payment_pending_title', $pendingTitle, $submission, $form),
759 'result' => false,
760 'error' => apply_filters('fluentform/payment_pending_message', $pendingMessage, $submission, $form)
761 ];
762 }
763
764 $returnData['type'] = 'success';
765 $returnData['is_new'] = false;
766
767 $this->showPaymentView($returnData);
768 }
769
770 public function getSubscriptions($status = false)
771 {
772 $subscriptions = Subscription::bySubmission($this->submissionId)
773 ->when($status, function ($q) use ($status) {
774 $q->where('status', $status);
775 })
776 ->get();
777
778 foreach ($subscriptions as $subscription) {
779 $subscription->original_plan = Helper::safeUnserialize($subscription->original_plan);
780 $subscription->vendor_response = Helper::safeUnserialize($subscription->vendor_response);
781 }
782
783 return $subscriptions;
784 }
785
786 public function updateSubscription($id, $data)
787 {
788 $data['updated_at'] = current_time('mysql');
789
790 return Subscription::where('id', $id)->update($data);
791 }
792
793 public function maybeInsertSubscriptionCharge($item)
794 {
795 $exists = Transaction::subscriptionType()
796 ->where('submission_id', $item['submission_id'])
797 ->where('subscription_id', $item['subscription_id'])
798 ->where('charge_id', $item['charge_id'])
799 ->where('payment_method', $item['payment_method'])
800 ->first();
801
802 $isNew = false;
803
804 if ($exists) {
805 // We don't want to update the address and payer email that we already have here
806 if ($exists->billing_address) {
807 unset($item['billing_address']);
808 }
809 if ($exists->payer_email) {
810 unset($item['payer_email']);
811 }
812
813 unset($item['transaction_hash']);
814 unset($item['created_at']);
815
816 // was: overwrote the status unconditionally, so a replayed success event un-refunded the row
817 if (PaymentHelper::isReversedPaymentStatus($exists->status)) {
818 unset($item['status']);
819 }
820
821 Transaction::where('id', $exists->id)->update($item);
822
823 $id = $exists->id;
824 } else {
825 if (empty($item['created_at'])) {
826 $item['created_at'] = current_time('mysql');
827 $item['updated_at'] = current_time('mysql');
828 }
829
830 if (empty($item['transaction_hash'])) {
831 $uniqueHash = wp_generate_password(32, false);
832 $item['transaction_hash'] = $uniqueHash;
833 }
834
835 $id = Transaction::create($item)->id;
836 $isNew = true;
837 }
838
839
840 $transaction = fluentFormApi('submissions')->transaction($id);
841
842 // We want to update the total amount here
843 $parentSubscription = Subscription::where('id', $transaction->subscription_id)->first();
844
845 // Let's count the total subscription payment
846 if ($parentSubscription) {
847 list($billCount, $paymentTotal) = $this->getPaymentCountsAndTotal($parentSubscription->id);
848
849 Subscription::where('id', $parentSubscription->id)
850 ->update([
851 'bill_count' => $billCount,
852 'payment_total' => $paymentTotal,
853 'updated_at' => current_time('mysql')
854 ]);
855
856 // was: total_paid took the gross subscription sum here, so a renewal or a replayed invoice
857 // after a refund put the refunded amount back; recalculatePaidTotal() is the net authority
858 Submission::where('id', $parentSubscription->submission_id)
859 ->update([
860 'payment_total' => $paymentTotal,
861 ]);
862 $this->recalculatePaidTotal();
863
864 $subscription = Subscription::where('id', $parentSubscription->id)->first();
865
866 // Keep a missing 6.2.13 renewal without reviving a closed subscription.
867 $isTerminalSubscription = in_array($subscription->status, ['cancelled', 'completed'], true);
868
869 if ($isNew && !$isTerminalSubscription) {
870 $submission = $this->getSubmission();
871 do_action_deprecated(
872 'fluentform_subscription_received_payment',
873 [
874 $subscription,
875 $submission
876 ],
877 FLUENTFORM_FRAMEWORK_UPGRADE,
878 'fluentform/subscription_received_payment',
879 'Use fluentform/subscription_received_payment instead of fluentform_subscription_received_payment.'
880 );
881 do_action('fluentform/subscription_received_payment', $subscription, $submission);
882
883 do_action_deprecated(
884 'fluentform_subscription_received_payment_' . $submission->payment_method,
885 [
886 $subscription,
887 $submission
888 ],
889 FLUENTFORM_FRAMEWORK_UPGRADE,
890 'fluentform/subscription_received_payment_' . $submission->payment_method,
891 'Use fluentform/subscription_received_payment_' . $submission->payment_method . ' instead of fluentform_subscription_received_payment_' . $submission->payment_method
892 );
893 do_action('fluentform/subscription_received_payment_' . $submission->payment_method, $subscription, $submission);
894 }
895
896 if ($isNew && $isTerminalSubscription) {
897 $submission = $this->getSubmission();
898 do_action('fluentform/log_data', [
899 'parent_source_id' => $submission->form_id,
900 'source_type' => 'submission_item',
901 'source_id' => $submission->id,
902 'component' => 'Payment',
903 'status' => 'info',
904 'title' => __('Late subscription payment recorded', 'fluentform'),
905 'description' => __('The subscription is already closed, so no renewal actions were fired.', 'fluentform'),
906 ]);
907 }
908
909 if (!$isTerminalSubscription && $subscription->bill_times > 0 && $subscription->bill_count >= $subscription->bill_times) {
910 // We have to mark the subscription as completed
911 $this->updateSubscriptionStatus($subscription, 'completed');
912 }
913 }
914
915 return $id;
916 }
917
918 public function getPaymentCountsAndTotal($subscriptionId, $paymentMethod = false)
919 {
920 // was: every row of any status, so a pending echeck or a failed attempt counted as an installment
921 $payments = Transaction::select(['id', 'payment_method', 'payment_total', 'payment_note'])
922 ->subscriptionType()
923 ->where('subscription_id', $subscriptionId)
924 ->whereIn('status', ['paid', 'partially-refunded', 'refunded'])
925 ->when($paymentMethod, function ($q) use ($paymentMethod) {
926 $q->where('payment_method', $paymentMethod);
927 })
928 ->get();
929
930 $subscription = Subscription::find($subscriptionId);
931
932 $installmentCount = 0;
933 $paymentTotal = 0;
934
935 foreach ($payments as $payment) {
936 $paymentTotal += $payment->payment_total;
937 if ($this->isCountableSubscriptionPayment($payment, $subscription)) {
938 $installmentCount++;
939 }
940 }
941
942 return [$installmentCount, $paymentTotal];
943 }
944
945 protected function isCountableSubscriptionPayment($payment, $subscription)
946 {
947 return $payment->payment_total > 0;
948 }
949
950 protected function getCancelAtTimeStamp($subscription)
951 {
952 if (!$subscription->bill_times) {
953 return false;
954 }
955
956 $dateTime = current_datetime();
957 $localtime = $dateTime->getTimestamp() + $dateTime->getOffset();
958
959 $billingStartDate = $localtime;
960
961 if ($subscription->expiration_at) {
962 $billingStartDate = strtotime($subscription->expiration_at);
963 }
964
965 $billTimes = $subscription->bill_times;
966
967 $interval = $subscription->billing_interval;
968
969 $interValMaps = [
970 'day' => 'days',
971 'week' => 'weeks',
972 'month' => 'months',
973 'year' => 'years'
974 ];
975
976 if (isset($interValMaps[$interval]) && $billTimes > 1) {
977 $interval = $interValMaps[$interval];
978 }
979
980 return strtotime('+ ' . $billTimes . ' ' . $interval, $billingStartDate);
981 }
982
983 public function updateSubmission($id, $data)
984 {
985 $data['updated_at'] = current_time('mysql');
986
987 return Submission::where('id', $id)->update($data);
988 }
989
990 public function limitLength($string, $limit = 127)
991 {
992 $str_limit = $limit - 3;
993 if (function_exists('mb_strimwidth')) {
994 if (mb_strlen($string) > $limit) {
995 $string = mb_strimwidth($string, 0, $str_limit) . '...';
996 }
997 } else {
998 if (strlen($string) > $limit) {
999 $string = substr($string, 0, $str_limit) . '...';
1000 }
1001 }
1002 return $string;
1003 }
1004
1005 public function getTransactionDefaults()
1006 {
1007 $submission = $this->getSubmission();
1008 if (!$submission) {
1009 return [];
1010 }
1011
1012 $data = [];
1013
1014 if ($customerEmail = PaymentHelper::getCustomerEmail($submission, $this->getForm())) {
1015 $data['payer_email'] = $customerEmail;
1016 }
1017
1018 if ($customerName = PaymentHelper::getCustomerName($submission, $this->getForm())) {
1019 $data['payer_name'] = $customerName;
1020 }
1021
1022 if ($submission->user_id) {
1023 $data['user_id'] = $submission->user_id;
1024 } else if ($user = get_user_by('ID', get_current_user_id())) {
1025 $data['user_id'] = $user->ID;
1026 }
1027
1028 if (!$submission->user_id && !empty($data['payer_email'])) {
1029 $email = $data['payer_email'];
1030 $maybeUser = get_user_by('email', $email);
1031 if ($maybeUser) {
1032
1033 $this->updateSubmission($submission->id, [
1034 'user_id' => $maybeUser->ID
1035 ]);
1036
1037 if (empty($data['user_id'])) {
1038 $data['user_id'] = $maybeUser->ID;
1039 }
1040 }
1041 }
1042
1043 $address = PaymentHelper::getCustomerAddress($submission);
1044 if (!$address) {
1045 $address = ArrayHelper::get($submission->response, 'address_1');
1046 }
1047 if ($address) {
1048 $address = array_filter($address);
1049 if ($address) {
1050 $data['billing_address'] = implode(', ', $address);
1051 }
1052 }
1053
1054 $data['created_at'] = current_time('mysql');
1055 $data['updated_at'] = current_time('mysql');
1056 $data['form_id'] = $submission->form_id;
1057 $data['submission_id'] = $submission->id;
1058 $data['payment_method'] = $this->method;
1059
1060 return $data;
1061 }
1062
1063 public function createInitialPendingTransaction($submission = false, $hasSubscriptions = false)
1064 {
1065 if (!$submission) {
1066 $submission = $this->getSubmission();
1067 }
1068
1069 $form = $this->getForm();
1070
1071 $uniqueHash = wp_generate_password(32, false);
1072
1073 $transactionData = [
1074 'transaction_type' => 'onetime',
1075 'transaction_hash' => $uniqueHash,
1076 'subscription_id' => null,
1077 'payment_total' => $this->getAmountTotal(),
1078 'status' => 'pending',
1079 'currency' => strtoupper($submission->currency),
1080 'payment_mode' => $this->getPaymentMode()
1081 ];
1082 if ($hasSubscriptions) {
1083 $subscriptions = $this->getSubscriptions();
1084 if ($subscriptions) {
1085 $subscriptionInitialTotal = 0;
1086 foreach ($subscriptions as $subscription) {
1087 if (!$subscription->trial_days) {
1088 $subscriptionInitialTotal += $subscription->recurring_amount;
1089 }
1090 if ($subscription->initial_amount) {
1091 $subscriptionInitialTotal += $subscription->initial_amount;
1092 }
1093 $transactionData['subscription_id'] = $subscription->id;
1094 }
1095 $transactionData['payment_total'] += $subscriptionInitialTotal;
1096 $transactionData['transaction_type'] = 'subscription';
1097 }
1098 }
1099
1100 $existingTransaction = Transaction::bySubmission($submission->id)
1101 ->where('payment_method', $this->method)
1102 ->where('transaction_type', '!=', 'refund')
1103 ->where('status', 'pending')
1104 ->orderBy('id', 'DESC')
1105 ->first();
1106
1107 if ($existingTransaction) {
1108 Transaction::where('id', $existingTransaction->id)->update([
1109 'transaction_type' => $transactionData['transaction_type'],
1110 'subscription_id' => $transactionData['subscription_id'],
1111 'payment_mode' => $transactionData['payment_mode'],
1112 'status' => 'pending',
1113 'payment_total' => $transactionData['payment_total'],
1114 'currency' => $transactionData['currency'],
1115 'updated_at' => current_time('mysql')
1116 ]);
1117 $transactionId = $existingTransaction->id;
1118 } else {
1119 $transactionId = $this->insertTransaction($transactionData);
1120 }
1121
1122 $this->updateSubmission($submission->id, [
1123 'payment_total' => $transactionData['payment_total']
1124 ]);
1125
1126 return $this->getTransaction($transactionId);
1127
1128 }
1129
1130 /**
1131 * @param object $subscription
1132 * @param string $newStatus
1133 * @param string $note
1134 * @return object
1135 */
1136 public function updateSubscriptionStatus($subscription, $newStatus, $note = '')
1137 {
1138 if (!$note) {
1139 $note = sprintf(
1140 /* translators: 1: new status, 2: old status */
1141 __('Subscription status has been changed to %1$s from %2$s', 'fluentform'),
1142 $newStatus,
1143 $subscription->status
1144 );
1145 }
1146
1147 $oldStatus = $subscription->status;
1148
1149 if ($oldStatus == $newStatus) {
1150 return $subscription;
1151 }
1152
1153 // A terminal subscription never moves again: a late/replayed event must not revive
1154 // cancelled -> completed (or completed -> cancelled) and fire its status side effects.
1155 $terminalStatuses = ['cancelled', 'completed'];
1156 if (in_array($oldStatus, $terminalStatuses)) {
1157 return $subscription;
1158 }
1159
1160 Subscription::where('id', $subscription->id)
1161 ->update([
1162 'status' => $newStatus,
1163 'updated_at' => current_time('mysql')
1164 ]);
1165
1166 $logData = [
1167 'parent_source_id' => $this->getForm()->id,
1168 'source_type' => 'submission_item',
1169 'source_id' => $this->submissionId,
1170 'component' => 'Payment',
1171 'status' => 'info',
1172 'title' => __('Subscription Status changed to ', 'fluentform') . $newStatus,
1173 'description' => $note
1174 ];
1175
1176 do_action('fluentform/log_data', $logData);
1177
1178 $subscription->status = $newStatus;
1179
1180 $submission = $this->getSubmission();
1181
1182 do_action_deprecated(
1183 'fluentform_subscription_payment_' . $newStatus,
1184 [
1185 $subscription,
1186 $submission,
1187 false
1188 ],
1189 FLUENTFORM_FRAMEWORK_UPGRADE,
1190 'fluentform/subscription_payment_' . $newStatus,
1191 'Use fluentform/subscription_payment_' . $newStatus . ' instead of fluentform_subscription_payment_' . $newStatus
1192 );
1193
1194 do_action('fluentform/subscription_payment_' . $newStatus, $subscription, $submission, false);
1195
1196 do_action_deprecated(
1197 'fluentform_subscription_payment_' . $newStatus . '_' . $submission->payment_method,
1198 [
1199 $subscription,
1200 $submission,
1201 false
1202 ],
1203 FLUENTFORM_FRAMEWORK_UPGRADE,
1204 'fluentform/subscription_payment_' . $newStatus . '_' . $submission->payment_method,
1205 'Use fluentform/subscription_payment_' . $newStatus . '_' . $submission->payment_method . ' instead of fluentform_subscription_payment_' . $newStatus . '_' . $submission->payment_method
1206 );
1207 do_action('fluentform/subscription_payment_' . $newStatus . '_' . $submission->payment_method, $subscription, $submission, false);
1208
1209 return $subscription;
1210 }
1211 }
1212