| @@ -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. |
| @@ -803,131 +798,8 @@ | ||
| 803 | 798 | return false; |
| 804 | 799 | } |
| 805 | 800 | |
| 806 | 801 | $result = self::update( $donor_id, [ 'stripe_customer_id' => '' ] ); |
| 807 | - | |
| 808 | - return false !== $result; | |
| 809 | - } | |
| 810 | - | |
| 811 | - /** | |
| 812 | - * Get the Stripe customer ID for a donor on a specific connected account. | |
| 813 | - * | |
| 814 | - * Reads the per-account map stored in `donor_data['stripe_customers']`. | |
| 815 | - * Falls back to the legacy single `stripe_customer_id` column when the | |
| 816 | - * account is the site default, so pre-multi-account donors keep working. | |
| 817 | - * | |
| 818 | - * @param string $email Donor email. | |
| 819 | - * @param string $account_id Stripe account id (`acct_…`). | |
| 820 | - * @param bool $is_default Whether this is the site default account. | |
| 821 | - * @return string Customer ID, or '' when none is stored. | |
| 822 | - * @since 1.3.0 | |
| 823 | - */ | |
| 824 | - public static function get_stripe_customer_id_for_account( $email, $account_id, $is_default = false ) { | |
| 825 | - if ( empty( $email ) || empty( $account_id ) ) { | |
| 826 | - return ''; | |
| 827 | - } | |
| 828 | - | |
| 829 | - $donor = self::get_by_email( $email ); | |
| 830 | - if ( ! $donor ) { | |
| 831 | - return ''; | |
| 832 | - } | |
| 833 | - | |
| 834 | - $donor_data = isset( $donor['donor_data'] ) && is_array( $donor['donor_data'] ) ? $donor['donor_data'] : []; | |
| 835 | - $map = isset( $donor_data['stripe_customers'] ) && is_array( $donor_data['stripe_customers'] ) ? $donor_data['stripe_customers'] : []; | |
| 836 | - | |
| 837 | - if ( isset( $map[ $account_id ] ) && is_string( $map[ $account_id ] ) && '' !== $map[ $account_id ] ) { | |
| 838 | - return $map[ $account_id ]; | |
| 839 | - } | |
| 840 | - | |
| 841 | - // Legacy fallback: the single column holds the default account's customer. | |
| 842 | - if ( $is_default && ! empty( $donor['stripe_customer_id'] ) && is_string( $donor['stripe_customer_id'] ) ) { | |
| 843 | - return $donor['stripe_customer_id']; | |
| 844 | - } | |
| 845 | - | |
| 846 | - return ''; | |
| 847 | - } | |
| 848 | - | |
| 849 | - /** | |
| 850 | - * Store the Stripe customer ID for a donor on a specific connected account. | |
| 851 | - * | |
| 852 | - * Writes the per-account map in `donor_data['stripe_customers']` and mirrors | |
| 853 | - * the default account's customer into the legacy `stripe_customer_id` column | |
| 854 | - * so back-compat readers keep working. | |
| 855 | - * | |
| 856 | - * @param string $email Donor email. | |
| 857 | - * @param string $account_id Stripe account id (`acct_…`). | |
| 858 | - * @param string $customer_id Stripe customer ID. | |
| 859 | - * @param bool $is_default Whether this is the site default account. | |
| 860 | - * @return bool True on success, false on failure. | |
| 861 | - * @since 1.3.0 | |
| 862 | - */ | |
| 863 | - public static function set_stripe_customer_id_for_account( $email, $account_id, $customer_id, $is_default = false ) { | |
| 864 | - if ( empty( $email ) || empty( $account_id ) || empty( $customer_id ) ) { | |
| 865 | - return false; | |
| 866 | - } | |
| 867 | - | |
| 868 | - $donor = self::get_by_email( $email ); | |
| 869 | - if ( ! $donor || empty( $donor['id'] ) ) { | |
| 870 | - return false; | |
| 871 | - } | |
| 872 | - $donor_id = is_numeric( $donor['id'] ) ? (int) $donor['id'] : 0; | |
| 873 | - if ( $donor_id <= 0 ) { | |
| 874 | - return false; | |
| 875 | - } | |
| 876 | - | |
| 877 | - $donor_data = isset( $donor['donor_data'] ) && is_array( $donor['donor_data'] ) ? $donor['donor_data'] : []; | |
| 878 | - $map = isset( $donor_data['stripe_customers'] ) && is_array( $donor_data['stripe_customers'] ) ? $donor_data['stripe_customers'] : []; | |
| 879 | - | |
| 880 | - $map[ $account_id ] = sanitize_text_field( $customer_id ); | |
| 881 | - $donor_data['stripe_customers'] = $map; | |
| 882 | - | |
| 883 | - $update = [ 'donor_data' => $donor_data ]; | |
| 884 | - if ( $is_default ) { | |
| 885 | - $update['stripe_customer_id'] = sanitize_text_field( $customer_id ); | |
| 886 | - } | |
| 887 | - | |
| 888 | - $result = self::update( $donor_id, $update ); | |
| 889 | - | |
| 890 | - return false !== $result; | |
| 891 | - } | |
| 892 | - | |
| 893 | - /** | |
| 894 | - * Clear the stored Stripe customer ID for a donor on a specific account. | |
| 895 | - * | |
| 896 | - * Used when a cached customer id is no longer valid on that account | |
| 897 | - * (deleted in Stripe, or a test/live mismatch). | |
| 898 | - * | |
| 899 | - * @param string $email Donor email. | |
| 900 | - * @param string $account_id Stripe account id (`acct_…`). | |
| 901 | - * @param bool $is_default Whether this is the site default account. | |
| 902 | - * @return bool True on success, false on failure. | |
| 903 | - * @since 1.3.0 | |
| 904 | - */ | |
| 905 | - public static function clear_stripe_customer_id_for_account( $email, $account_id, $is_default = false ) { | |
| 906 | - if ( empty( $email ) || empty( $account_id ) ) { | |
| 907 | - return false; | |
| 908 | - } | |
| 909 | - | |
| 910 | - $donor = self::get_by_email( $email ); | |
| 911 | - if ( ! $donor || empty( $donor['id'] ) ) { | |
| 912 | - return false; | |
| 913 | - } | |
| 914 | - $donor_id = is_numeric( $donor['id'] ) ? (int) $donor['id'] : 0; | |
| 915 | - if ( $donor_id <= 0 ) { | |
| 916 | - return false; | |
| 917 | - } | |
| 918 | - | |
| 919 | - $donor_data = isset( $donor['donor_data'] ) && is_array( $donor['donor_data'] ) ? $donor['donor_data'] : []; | |
| 920 | - $map = isset( $donor_data['stripe_customers'] ) && is_array( $donor_data['stripe_customers'] ) ? $donor_data['stripe_customers'] : []; | |
| 921 | - unset( $map[ $account_id ] ); | |
| 922 | - $donor_data['stripe_customers'] = $map; | |
| 923 | - | |
| 924 | - $update = [ 'donor_data' => $donor_data ]; | |
| 925 | - if ( $is_default ) { | |
| 926 | - $update['stripe_customer_id'] = ''; | |
| 927 | - } | |
| 928 | - | |
| 929 | - $result = self::update( $donor_id, $update ); | |
| 930 | 802 | |
| 931 | 803 | return false !== $result; |
| 932 | 804 | } |
| 933 | 805 | |