PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.15
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.15
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/AjaxEndpoints.php +77 -8 6.2.12 → 6.2.15 View file →
@@ -254,10 +254,20 @@
254 254
255 255 $transactionId = $transactionData['id'];
256 256 $oldTransaction = Transaction::find($transactionId);
257 257
258 + if (!$oldTransaction) {
259 + wp_send_json_error(['message' => __('Transaction not found.', 'fluentform')], 404);
260 + }
261 +
258 262 $changingStatus = $oldTransaction->status != $transactionData['status'];
259 263
264 + // Only a *changed* status is validated; a row may already hold one this build does not
265 + // register, e.g. Pro's 'requires_review', and editing other fields must not be blocked.
266 + if ($changingStatus && !isset(PaymentHelper::getPaymentStatuses()[$transactionData['status']])) {
267 + wp_send_json_error(['message' => __('Invalid payment status.', 'fluentform')], 422);
268 + }
269 +
260 270 $updateData = ArrayHelper::only($transactionData, [
261 271 'payer_name',
262 272 'payer_email',
263 273 'billing_address',
@@ -271,15 +281,21 @@
271 281 Transaction::where('id', $transactionId)->update($updateData);
272 282
273 283 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in route registration
274 284 if ($subscriptionId) {
275 - $existingSubscription = Subscription::find($subscriptionId);
285 + // Bind to the transaction's submission; submission_id comes from the row, not the request.
286 + $existingSubscription = Subscription::where('id', $subscriptionId)
287 + ->where('submission_id', $oldTransaction->submission_id)
288 + ->first();
276 289
277 290 $changedStatus = ArrayHelper::get($transactionData, 'status');
278 291
279 - $isStatusChanged = $existingSubscription->status != $changedStatus;
292 + // Only mirror real subscription statuses; 'paid' here would block cancellation forever.
293 + $isMappable = $existingSubscription
294 + && isset(PaymentHelper::getSubscriptionStatuses()[$changedStatus])
295 + && $existingSubscription->status != $changedStatus;
280 296
281 - if ($isStatusChanged) {
297 + if ($isMappable) {
282 298 Subscription::where('id', $subscriptionId)
283 299 ->update([
284 300 'status' => $changedStatus,
285 301 'updated_at' => current_time('mysql')
@@ -295,10 +311,10 @@
295 311 }
296 312 };
297 313
298 314 if (
299 - ($changingStatus && ($newStatus == 'refunded' || $newStatus == 'partial-refunded')) ||
300 - ($newStatus == 'partial-refunded' && ArrayHelper::get($transactionData, 'refund_amount'))
315 + ($changingStatus && ($newStatus == 'refunded' || $newStatus == 'partially-refunded')) ||
316 + ($newStatus == 'partially-refunded' && ArrayHelper::get($transactionData, 'refund_amount'))
301 317 ) {
302 318 $refundAmount = 0;
303 319 $refundNote = 'Refunded by Admin';
304 320
@@ -306,9 +322,9 @@
306 322 // Handle refund here
307 323 $refundAmount = $oldTransaction->payment_total;
308 324 } else if ($newStatus == 'partially-refunded') {
309 325 $refundAmount = ArrayHelper::get($transactionData, 'refund_amount') * 100;
310 - $refundNote = ArrayHelper::get($transactionData, 'refund_note');
326 + $refundNote = ArrayHelper::get($transactionData, 'refund_note') ?: $refundNote;
311 327 }
312 328
313 329 if ($refundAmount) {
314 330 $baseProcessor->setSubmissionId($oldTransaction->submission_id);
@@ -314,8 +330,12 @@
314 330 $baseProcessor->setSubmissionId($oldTransaction->submission_id);
315 331
316 332 $submission = $baseProcessor->getSubmission();
317 333 $baseProcessor->refund($refundAmount, $oldTransaction, $submission, $oldTransaction->payment_method, 'refund_' . time(), $refundNote);
334 +
335 + // refund() derives the real status from the refunded total: an amount covering
336 + // the whole charge is a full refund, whatever status was requested.
337 + $newStatus = Transaction::find($transactionId)->status;
318 338 }
319 339
320 340 }
321 341
@@ -321,10 +341,10 @@
321 341
322 342 if ($changingStatus) {
323 343
324 344 if ($newStatus == 'paid' || $newStatus == 'pending' || $newStatus == 'processing') {
325 - // Delete All Refunds
326 - Transaction::bySubmission($oldTransaction->submission_id)->refunds()->delete();
345 + // Delete All Refunds, recording them first
346 + $this->recordAndRemoveRefundLedger($oldTransaction, $newStatus);
327 347 }
328 348
329 349 $baseProcessor->setSubmissionId($oldTransaction->submission_id);
330 350 $baseProcessor->changeSubmissionPaymentStatus($newStatus);
@@ -348,8 +368,57 @@
348 368
349 369 wp_send_json_success([
350 370 'message' => __('Successfully updated data', 'fluentform')
351 371 ], 200);
372 + }
373 +
374 + /**
375 + * The record is the compensating control for an irreversible delete, so if it cannot be written the rows must survive.
376 + */
377 + private function recordAndRemoveRefundLedger($oldTransaction, $newStatus)
378 + {
379 + $refunds = Transaction::bySubmission($oldTransaction->submission_id)->refunds()->get();
380 +
381 + if (!count($refunds)) {
382 + return;
383 + }
384 +
385 + $ids = [];
386 + $total = 0;
387 + $records = [];
388 +
389 + foreach ($refunds as $refund) {
390 + $ids[] = $refund->id;
391 + $total += $refund->payment_total;
392 + $records[] = '#' . $refund->id . ' (' . PaymentHelper::formatMoney($refund->payment_total, $refund->currency) . ')';
393 + }
394 +
395 + $description = sprintf(
396 + /* translators: 1: previous status, 2: new status, 3: number of refund records, 4: formatted total, 5: the deleted records */
397 + __(
398 + 'Payment status changed from %1$s to %2$s, which removed %3$d refund record(s) totalling %4$s: %5$s',
399 + 'fluentform'
400 + ),
401 + $oldTransaction->status,
402 + $newStatus,
403 + count($refunds),
404 + PaymentHelper::formatMoney($total, $oldTransaction->currency),
405 + implode(', ', $records)
406 + );
407 +
408 + // Record first: if this write fails the rows are still here to try again.
409 + do_action('fluentform/log_data', [
410 + 'parent_source_id' => $oldTransaction->form_id,
411 + 'source_type' => 'submission_item',
412 + 'source_id' => $oldTransaction->submission_id,
413 + 'component' => 'Payment',
414 + 'status' => 'info',
415 + 'title' => __('Refund records deleted', 'fluentform'),
416 + 'description' => $description,
417 + ]);
418 +
419 + // Only the rows just recorded, so a refund added meanwhile is not destroyed unrecorded.
420 + Transaction::whereIn('id', $ids)->delete();
352 421 }
353 422
354 423 public function getStripeConnectConfig()
355 424 {