PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.1.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.1.0
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 +18 -204 1.6.0 → 1.1.0 View file →
@@ -168,9 +168,9 @@
168 168 'donor_status VARCHAR(20) NOT NULL',
169 169 'donor_data LONGTEXT',
170 170 'stripe_customer_id VARCHAR(255) DEFAULT NULL',
171 171 'import_source_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0',
172 - 'import_source VARCHAR(20) NOT NULL DEFAULT \'\'',
172 + 'import_source VARCHAR(20) NOT NULL DEFAULT ""',
173 173 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP',
174 174 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
175 175 'INDEX idx_email (email)',
176 176 'INDEX idx_user (user_id)',
@@ -198,12 +198,12 @@
198 198 // produces silent divergence at the data layer — a future
199 199 // `WHERE address = ''` filter would miss legacy rows that landed as
200 200 // NULL from the migration.
201 201 return [
202 - 'company VARCHAR(255) NOT NULL DEFAULT \'\' AFTER phone',
203 - 'address TEXT NOT NULL DEFAULT \'\' AFTER company',
202 + 'company VARCHAR(255) NOT NULL DEFAULT "" AFTER phone',
203 + 'address TEXT NOT NULL DEFAULT "" AFTER company',
204 204 'import_source_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER stripe_customer_id',
205 - 'import_source VARCHAR(20) NOT NULL DEFAULT \'\' AFTER import_source_id',
205 + 'import_source VARCHAR(20) NOT NULL DEFAULT "" AFTER import_source_id',
206 206 'INDEX idx_import_source (import_source_id, import_source)',
207 207 ];
208 208 }
209 209
@@ -446,13 +446,12 @@
446 446 /**
447 447 * Get or create donor by email.
448 448 *
449 449 * @param string $email Donor email.
450 - * @param string $name Donor name. Stored when creating a new donor, or backfilled onto an existing donor only when its stored name is empty; never overwrites a populated value.
451 - * @param string $phone Donor phone. Stored when creating a new donor, or backfilled onto an existing donor only when its stored phone is empty; never overwrites a populated value.
450 + * @param string $name Donor name.
451 + * @param string $phone Donor phone.
452 452 * @return int|false Donor ID or false on error.
453 453 * @since 0.0.1
454 - * @since 1.4.0 A subsequent donation no longer overwrites an existing donor's name/phone; missing values are backfilled, populated ones are left intact.
455 454 */
456 455 public static function get_or_create( $email, $name = '', $phone = '' ) {
457 456 if ( empty( $email ) ) {
458 457 return false;
@@ -460,31 +459,27 @@
460 459
461 460 $existing = self::get_by_email( $email );
462 461
463 462 if ( $existing ) {
464 - $existing_id = isset( $existing['id'] ) && is_numeric( $existing['id'] ) ? (int) $existing['id'] : 0;
463 + // Update name/phone if provided and different.
464 + $updates = [];
465 465
466 - // Backfill name/phone only when the stored value is empty — a later
467 - // donation never overwrites a populated donor name/phone. This closes
468 - // the unauthenticated-tampering vector (an attacker who knows a
469 - // donor's email cannot change that donor's existing name/phone on a
470 - // bare, unverified match) while still letting genuinely missing
471 - // details fill in from a later donation — e.g. an optional-name
472 - // gateway, or a phone field mapped after the donor's first donation.
473 - // The name/phone entered for each donation are always captured on the
474 - // donation row regardless, and admins can edit a donor directly via
475 - // the donor management endpoints.
476 - $updates = [];
477 - if ( ! empty( $name ) && '' === (string) ( $existing['name'] ?? '' ) ) {
466 + if ( ! empty( $name ) && $name !== $existing['name'] ) {
478 467 $updates['name'] = $name;
479 468 }
480 - if ( ! empty( $phone ) && '' === (string) ( $existing['phone'] ?? '' ) ) {
469 +
470 + if ( ! empty( $phone ) && $phone !== $existing['phone'] ) {
481 471 $updates['phone'] = $phone;
482 472 }
483 - if ( ! empty( $updates ) && $existing_id > 0 ) {
484 - self::update( $existing_id, $updates );
473 +
474 + if ( ! empty( $updates ) && isset( $existing['id'] ) ) {
475 + $existing_id = is_numeric( $existing['id'] ) ? (int) $existing['id'] : 0;
476 + if ( $existing_id > 0 ) {
477 + self::update( $existing_id, $updates );
478 + }
485 479 }
486 480
481 + $existing_id = isset( $existing['id'] ) && is_numeric( $existing['id'] ) ? (int) $existing['id'] : 0;
487 482 return $existing_id > 0 ? $existing_id : false;
488 483 }
489 484
490 485 // Create new donor.
@@ -601,68 +596,10 @@
601 596 do_action( 'suredonation_donor_user_created', $user_id, $donor_id, $email );
602 597 }
603 598
604 599 /**
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 - /**
659 600 * Update donor statistics after a donation.
660 601 *
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().
664 - *
665 602 * @param int $donor_id Donor ID.
666 603 * @param float $amount Donation amount.
667 604 * @return int|false Number of rows updated or false on error.
668 605 * @since 0.0.1
@@ -861,131 +798,8 @@
861 798 return false;
862 799 }
863 800
864 801 $result = self::update( $donor_id, [ 'stripe_customer_id' => '' ] );
865 -
866 - return false !== $result;
867 - }
868 -
869 - /**
870 - * Get the Stripe customer ID for a donor on a specific connected account.
871 - *
872 - * Reads the per-account map stored in `donor_data['stripe_customers']`.
873 - * Falls back to the legacy single `stripe_customer_id` column when the
874 - * account is the site default, so pre-multi-account donors keep working.
875 - *
876 - * @param string $email Donor email.
877 - * @param string $account_id Stripe account id (`acct_…`).
878 - * @param bool $is_default Whether this is the site default account.
879 - * @return string Customer ID, or '' when none is stored.
880 - * @since 1.3.0
881 - */
882 - public static function get_stripe_customer_id_for_account( $email, $account_id, $is_default = false ) {
883 - if ( empty( $email ) || empty( $account_id ) ) {
884 - return '';
885 - }
886 -
887 - $donor = self::get_by_email( $email );
888 - if ( ! $donor ) {
889 - return '';
890 - }
891 -
892 - $donor_data = isset( $donor['donor_data'] ) && is_array( $donor['donor_data'] ) ? $donor['donor_data'] : [];
893 - $map = isset( $donor_data['stripe_customers'] ) && is_array( $donor_data['stripe_customers'] ) ? $donor_data['stripe_customers'] : [];
894 -
895 - if ( isset( $map[ $account_id ] ) && is_string( $map[ $account_id ] ) && '' !== $map[ $account_id ] ) {
896 - return $map[ $account_id ];
897 - }
898 -
899 - // Legacy fallback: the single column holds the default account's customer.
900 - if ( $is_default && ! empty( $donor['stripe_customer_id'] ) && is_string( $donor['stripe_customer_id'] ) ) {
901 - return $donor['stripe_customer_id'];
902 - }
903 -
904 - return '';
905 - }
906 -
907 - /**
908 - * Store the Stripe customer ID for a donor on a specific connected account.
909 - *
910 - * Writes the per-account map in `donor_data['stripe_customers']` and mirrors
911 - * the default account's customer into the legacy `stripe_customer_id` column
912 - * so back-compat readers keep working.
913 - *
914 - * @param string $email Donor email.
915 - * @param string $account_id Stripe account id (`acct_…`).
916 - * @param string $customer_id Stripe customer ID.
917 - * @param bool $is_default Whether this is the site default account.
918 - * @return bool True on success, false on failure.
919 - * @since 1.3.0
920 - */
921 - public static function set_stripe_customer_id_for_account( $email, $account_id, $customer_id, $is_default = false ) {
922 - if ( empty( $email ) || empty( $account_id ) || empty( $customer_id ) ) {
923 - return false;
924 - }
925 -
926 - $donor = self::get_by_email( $email );
927 - if ( ! $donor || empty( $donor['id'] ) ) {
928 - return false;
929 - }
930 - $donor_id = is_numeric( $donor['id'] ) ? (int) $donor['id'] : 0;
931 - if ( $donor_id <= 0 ) {
932 - return false;
933 - }
934 -
935 - $donor_data = isset( $donor['donor_data'] ) && is_array( $donor['donor_data'] ) ? $donor['donor_data'] : [];
936 - $map = isset( $donor_data['stripe_customers'] ) && is_array( $donor_data['stripe_customers'] ) ? $donor_data['stripe_customers'] : [];
937 -
938 - $map[ $account_id ] = sanitize_text_field( $customer_id );
939 - $donor_data['stripe_customers'] = $map;
940 -
941 - $update = [ 'donor_data' => $donor_data ];
942 - if ( $is_default ) {
943 - $update['stripe_customer_id'] = sanitize_text_field( $customer_id );
944 - }
945 -
946 - $result = self::update( $donor_id, $update );
947 -
948 - return false !== $result;
949 - }
950 -
951 - /**
952 - * Clear the stored Stripe customer ID for a donor on a specific account.
953 - *
954 - * Used when a cached customer id is no longer valid on that account
955 - * (deleted in Stripe, or a test/live mismatch).
956 - *
957 - * @param string $email Donor email.
958 - * @param string $account_id Stripe account id (`acct_…`).
959 - * @param bool $is_default Whether this is the site default account.
960 - * @return bool True on success, false on failure.
961 - * @since 1.3.0
962 - */
963 - public static function clear_stripe_customer_id_for_account( $email, $account_id, $is_default = false ) {
964 - if ( empty( $email ) || empty( $account_id ) ) {
965 - return false;
966 - }
967 -
968 - $donor = self::get_by_email( $email );
969 - if ( ! $donor || empty( $donor['id'] ) ) {
970 - return false;
971 - }
972 - $donor_id = is_numeric( $donor['id'] ) ? (int) $donor['id'] : 0;
973 - if ( $donor_id <= 0 ) {
974 - return false;
975 - }
976 -
977 - $donor_data = isset( $donor['donor_data'] ) && is_array( $donor['donor_data'] ) ? $donor['donor_data'] : [];
978 - $map = isset( $donor_data['stripe_customers'] ) && is_array( $donor_data['stripe_customers'] ) ? $donor_data['stripe_customers'] : [];
979 - unset( $map[ $account_id ] );
980 - $donor_data['stripe_customers'] = $map;
981 -
982 - $update = [ 'donor_data' => $donor_data ];
983 - if ( $is_default ) {
984 - $update['stripe_customer_id'] = '';
985 - }
986 -
987 - $result = self::update( $donor_id, $update );
988 802
989 803 return false !== $result;
990 804 }
991 805