PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
1.6.1 1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
← All changes | inc/database/tables/donors.php +58 -0 1.5.0 → 1.6.1 View file →
@@ -601,9 +601,67 @@
601 601 do_action( 'suredonation_donor_user_created', $user_id, $donor_id, $email );
602 602 }
603 603
604 604 /**
605 + * Record a donation against a donor's aggregates exactly once.
606 + *
607 + * Two paths complete the same Stripe donation — the client-side confirm
608 + * (`complete_donation()`) and the `payment_intent.succeeded` webhook — and
609 + * neither knows whether the other got there first. The webhook applies no
610 + * "still pending" guard, so calling record_donation() from both would double
611 + * a donor's total; calling it from neither (until now) left the totals stale
612 + * on every donation whose webhook never arrived.
613 + *
614 + * Keyed on the donation, not the donor, so a second genuine gift still
615 + * counts. Marked before the write: a duplicated total is harder to notice
616 + * and impossible to unpick, whereas a missed one is visible against the
617 + * donation list and recomputable.
618 + *
619 + * @param int $donor_id Donor row ID.
620 + * @param float $amount Donation amount.
621 + * @param int $donation_id Donation row ID this call is for.
622 + * @return bool True when this call recorded it, false when already recorded or invalid.
623 + * @since 1.6.0
624 + */
625 + public static function record_donation_once( $donor_id, $amount, $donation_id ) {
626 + $donation_id = absint( $donation_id );
627 +
628 + if ( $donation_id <= 0 ) {
629 + return false;
630 + }
631 +
632 + $key = 'suredonation_donor_recorded_' . $donation_id;
633 +
634 + if ( get_transient( $key ) ) {
635 + return false;
636 + }
637 +
638 + // Marked before the write, so two racers cannot both get through on a
639 + // read that saw nothing.
640 + set_transient( $key, true, WEEK_IN_SECONDS );
641 +
642 + $recorded = (bool) self::record_donation( $donor_id, $amount );
643 +
644 + if ( ! $recorded ) {
645 + // record_donation() refuses a non-positive amount or a donor row
646 + // that no longer exists (the privacy eraser can remove one), and
647 + // returns false without writing anything. Leaving the marker up
648 + // after that would be worse than not having it: the gateway
649 + // webhook retry is this row's safety net, and it would find the
650 + // marker and skip, so the donation would never reach the donor's
651 + // totals at all.
652 + delete_transient( $key );
653 + }
654 +
655 + return $recorded;
656 + }
657 +
658 + /**
605 659 * Update donor statistics after a donation.
660 + *
661 + * Unconditional: it takes no status and no donation id, so it cannot tell a
662 + * repeat call for the same donation from a second gift. Callers that can be
663 + * reached twice for one donation should use record_donation_once().
606 664 *
607 665 * @param int $donor_id Donor ID.
608 666 * @param float $amount Donation amount.
609 667 * @return int|false Number of rows updated or false on error.