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
← All changes | app/Modules/Payments/PaymentMethods/BaseProcessor.php +156 -16 6.2.56.2.14 View file →
@@ -52,9 +52,9 @@
52 52
53 53 $data = wp_parse_args($data, $this->getTransactionDefaults());
54 54
55 55 if (empty($data['transaction_hash'])) {
56 - $data['transaction_hash'] = md5($data['transaction_type'] . '_payment_' . $data['submission_id'] . '-' . $data['form_id'] . '_' . $data['created_at'] . '-' . time() . '-' . wp_rand(100, 999));
56 + $data['transaction_hash'] = wp_generate_password(32, false);
57 57 }
58 58
59 59 return Transaction::create($data)->id;
60 60 }
@@ -75,9 +75,9 @@
75 75 $data['user_id'] = $userId;
76 76 }
77 77
78 78 if (empty($data['transaction_hash'])) {
79 - $data['transaction_hash'] = md5($data['transaction_type'] . '_payment_' . $data['submission_id'] . '-' . $data['form_id'] . '_' . $data['created_at'] . '-' . time() . '-' . wp_rand(100, 999));
79 + $data['transaction_hash'] = wp_generate_password(32, false);
80 80 }
81 81
82 82 return Transaction::create($data)->id;
83 83 }
@@ -102,9 +102,11 @@
102 102 }
103 103
104 104 public function getLastTransaction($submissionId)
105 105 {
106 + // was: any row, so a refund ledger row inserted meanwhile became the row being settled
106 107 return Transaction::bySubmission($submissionId)
108 + ->where('transaction_type', '!=', 'refund')
107 109 ->orderBy('id', 'DESC')
108 110 ->first();
109 111 }
110 112
@@ -109,8 +111,18 @@
109 111 }
110 112
111 113 public function changeSubmissionPaymentStatus($newStatus)
112 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 + {
113 125 do_action_deprecated(
114 126 'fluentform_before_payment_status_change',
115 127 [
116 128 $newStatus,
@@ -123,8 +135,11 @@
123 135
124 136 do_action('fluentform/before_payment_status_change', $newStatus, $this->getSubmission());
125 137
126 138 Submission::where('id', $this->submissionId)
139 + ->when($unlessReversed, function ($query) {
140 + $query->whereNotIn('payment_status', PaymentHelper::reversedPaymentStatuses());
141 + })
127 142 ->update([
128 143 'payment_status' => $newStatus,
129 144 'updated_at' => current_time('mysql')
130 145 ]);
@@ -130,8 +145,16 @@
130 145 ]);
131 146
132 147 $this->submission = null;
133 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 +
134 157 $logData = [
135 158 'parent_source_id' => $this->getForm()->id,
136 159 'source_type' => 'submission_item',
137 160 'source_id' => $this->submissionId,
@@ -158,23 +181,31 @@
158 181
159 182 return true;
160 183 }
161 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 +
162 192 public function recalculatePaidTotal()
163 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
164 196 $transactions = Transaction::bySubmission($this->submissionId)
165 197 ->paid()
198 + ->where('transaction_type', '!=', 'refund')
166 199 ->get();
167 200
168 201 $total = 0;
169 202 $subscriptionId = false;
170 - $subBillCount = 0;
171 203
172 204 foreach ($transactions as $transaction) {
173 205 $total += $transaction->payment_total;
174 206 if($transaction->subscription_id) {
175 207 $subscriptionId = $transaction->subscription_id;
176 - $subBillCount += 1;
177 208 }
178 209 }
179 210
180 211 $refunds = $this->getRefundTotal();
@@ -187,11 +218,13 @@
187 218 'total_paid' => $total,
188 219 'updated_at' => current_time('mysql')
189 220 ]);
190 221
191 - if($subscriptionId && $subBillCount) {
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);
192 225 $this->updateSubscription($subscriptionId, [
193 - 'bill_count' => $subBillCount
226 + 'bill_count' => $installmentCount
194 227 ]);
195 228 }
196 229 }
197 230
@@ -210,8 +243,18 @@
210 243 }
211 244
212 245 public function changeTransactionStatus($transactionId, $newStatus)
213 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 + {
214 257 do_action_deprecated(
215 258 'fluentform_before_transaction_status_change',
216 259 [
217 260 $newStatus,
@@ -230,13 +273,23 @@
230 273 $transactionId
231 274 );
232 275
233 276 Transaction::where('id', $transactionId)
277 + ->when($unlessReversed, function ($query) {
278 + $query->whereNotIn('status', PaymentHelper::reversedPaymentStatuses());
279 + })
234 280 ->update([
235 281 'status' => $newStatus,
236 282 'updated_at' => current_time('mysql')
237 283 ]);
238 284
285 + if ($unlessReversed) {
286 + $transaction = $this->getTransaction($transactionId);
287 + if (!$transaction || PaymentHelper::isReversedPaymentStatus($transaction->status)) {
288 + return false;
289 + }
290 + }
291 +
239 292 do_action_deprecated(
240 293 'fluentform_after_transaction_status_change',
241 294 [
242 295 $newStatus,
@@ -294,8 +347,16 @@
294 347 if ($loginId = $this->getMetaData('_make_auto_login')) {
295 348 $this->maybeAutoLogin($loginId, $submission);
296 349 }
297 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 + ];
298 359 } else {
299 360 $returnData = $submissionService->processSubmissionData(
300 361 $this->submissionId, $submission->response, $this->getForm()
301 362 );
@@ -475,8 +536,23 @@
475 536 $this->setSubmissionId($submission->id);
476 537 $status = 'refunded';
477 538
478 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 +
479 555 $totalRefund = intval($refund_amount + $alreadyRefunded);
480 556
481 557 if ($totalRefund < $transaction->payment_total) {
482 558 $status = 'partially-refunded';
@@ -483,9 +559,9 @@
483 559 }
484 560
485 561 $this->changeTransactionStatus($transaction->id, $status);
486 562 $this->changeSubmissionPaymentStatus($status);
487 - $uniqueHash = md5('refund_' . $submission->id . '-' . $submission->form_id . '-' . time() . '-' . wp_rand(100, 999));
563 + $uniqueHash = wp_generate_password(32, false);
488 564
489 565 $refundData = [
490 566 'form_id' => $submission->form_id,
491 567 'submission_id' => $submission->id,
@@ -736,8 +812,13 @@
736 812
737 813 unset($item['transaction_hash']);
738 814 unset($item['created_at']);
739 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 +
740 821 Transaction::where('id', $exists->id)->update($item);
741 822
742 823 $id = $exists->id;
743 824 } else {
@@ -746,9 +827,9 @@
746 827 $item['updated_at'] = current_time('mysql');
747 828 }
748 829
749 830 if (empty($item['transaction_hash'])) {
750 - $uniqueHash = md5('subscription_payment_' . $item['submission_id'] . '-' . $item['charge_id'] . '-' . time() . '-' . wp_rand(100, 999));
831 + $uniqueHash = wp_generate_password(32, false);
751 832 $item['transaction_hash'] = $uniqueHash;
752 833 }
753 834
754 835 $id = Transaction::create($item)->id;
@@ -771,17 +852,22 @@
771 852 'payment_total' => $paymentTotal,
772 853 'updated_at' => current_time('mysql')
773 854 ]);
774 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
775 858 Submission::where('id', $parentSubscription->submission_id)
776 859 ->update([
777 860 'payment_total' => $paymentTotal,
778 - 'total_paid' => $paymentTotal,
779 861 ]);
862 + $this->recalculatePaidTotal();
780 863
781 864 $subscription = Subscription::where('id', $parentSubscription->id)->first();
782 865
783 - if($isNew) {
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) {
784 870 $submission = $this->getSubmission();
785 871 do_action_deprecated(
786 872 'fluentform_subscription_received_payment',
787 873 [
@@ -806,9 +892,22 @@
806 892 );
807 893 do_action('fluentform/subscription_received_payment_' . $submission->payment_method, $subscription, $submission);
808 894 }
809 895
810 - if($subscription->bill_times >= $subscription->bill_count) {
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) {
811 910 // We have to mark the subscription as completed
812 911 $this->updateSubscriptionStatus($subscription, 'completed');
813 912 }
814 913 }
@@ -817,25 +916,38 @@
817 916 }
818 917
819 918 public function getPaymentCountsAndTotal($subscriptionId, $paymentMethod = false)
820 919 {
821 - $payments = Transaction::select(['id', 'payment_total'])
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'])
822 922 ->subscriptionType()
823 923 ->where('subscription_id', $subscriptionId)
924 + ->whereIn('status', ['paid', 'partially-refunded', 'refunded'])
824 925 ->when($paymentMethod, function ($q) use ($paymentMethod) {
825 926 $q->where('payment_method', $paymentMethod);
826 927 })
827 928 ->get();
828 929
930 + $subscription = Subscription::find($subscriptionId);
931 +
932 + $installmentCount = 0;
829 933 $paymentTotal = 0;
830 934
831 935 foreach ($payments as $payment) {
832 936 $paymentTotal += $payment->payment_total;
937 + if ($this->isCountableSubscriptionPayment($payment, $subscription)) {
938 + $installmentCount++;
939 + }
833 940 }
834 941
835 - return [count($payments), $paymentTotal];
942 + return [$installmentCount, $paymentTotal];
836 943 }
837 944
945 + protected function isCountableSubscriptionPayment($payment, $subscription)
946 + {
947 + return $payment->payment_total > 0;
948 + }
949 +
838 950 protected function getCancelAtTimeStamp($subscription)
839 951 {
840 952 if (!$subscription->bill_times) {
841 953 return false;
@@ -955,13 +1067,14 @@
955 1067 }
956 1068
957 1069 $form = $this->getForm();
958 1070
959 - $uniqueHash = md5($submission->id . '-' . $form->id . '-' . time() . '-' . wp_rand(100, 999));
1071 + $uniqueHash = wp_generate_password(32, false);
960 1072
961 1073 $transactionData = [
962 1074 'transaction_type' => 'onetime',
963 1075 'transaction_hash' => $uniqueHash,
1076 + 'subscription_id' => null,
964 1077 'payment_total' => $this->getAmountTotal(),
965 1078 'status' => 'pending',
966 1079 'currency' => strtoupper($submission->currency),
967 1080 'payment_mode' => $this->getPaymentMode()
@@ -983,10 +1096,30 @@
983 1096 $transactionData['transaction_type'] = 'subscription';
984 1097 }
985 1098 }
986 1099
987 - $transactionId = $this->insertTransaction($transactionData);
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();
988 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 +
989 1122 $this->updateSubmission($submission->id, [
990 1123 'payment_total' => $transactionData['payment_total']
991 1124 ]);
992 1125
@@ -1012,9 +1145,16 @@
1012 1145 }
1013 1146
1014 1147 $oldStatus = $subscription->status;
1015 1148
1016 - if($oldStatus == $newStatus) {
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)) {
1017 1157 return $subscription;
1018 1158 }
1019 1159
1020 1160 Subscription::where('id', $subscription->id)