PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.1.2
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.1.2
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
suredonation / inc / import / givewp / donor-mapper.php

donor-mapper.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.1.2, at inc/import/givewp/donor-mapper.php

305 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Donor resolver for the GiveWP migration tool.
4 *
5 * Looks up or creates the SureDonation donor row that a given GiveWP
6 * payment belongs to. Matches Charitable's verified pattern: link to an
7 * existing WP user if one is found by id or email, otherwise store
8 * user_id = 0. Never creates a WP user during migration — the donor
9 * dashboard's magic-link auth works against the donors table alone
10 * (see suredonation-pro/inc/donor-dashboard/magic-link.php:187).
11 *
12 * When the GiveWP donor id is known, the row is enriched from
13 * give_donormeta so phone, company, structured address, and any
14 * gateway-specific keys (Stripe customer id) survive the migration
15 * instead of being lost to payment-meta only.
16 *
17 * @package SureDonation
18 */
19
20 namespace SureDonation\Inc\Import\Givewp;
21
22 use SureDonation\Inc\Database\Tables\Donors;
23 use SureDonation\Inc\Traits\Get_Instance;
24
25 // Exit if accessed directly.
26 defined( 'ABSPATH' ) || exit;
27
28 /**
29 * Donor_Mapper class.
30 *
31 * @since 1.0.0
32 */
33 class Donor_Mapper {
34 use Get_Instance;
35
36 /**
37 * GiveWP donormeta keys that the enrichment logic maps to dedicated
38 * SureDonation donor columns (phone, company), top-level donor_data
39 * slots the donor dashboard reads (donor_data.avatar_url), or
40 * structured slots under donor_data.givewp.address /
41 * .stripe_customer_id. Anything outside this set is preserved raw
42 * under donor_data.givewp.donor_meta.
43 */
44 const STANDARD_DONOR_META_KEYS = [
45 '_give_donor_first_name',
46 '_give_donor_last_name',
47 '_give_donor_company',
48 '_give_donor_phone',
49 '_give_donor_avatar',
50 '_give_donor_anonymous',
51 '_give_donor_address_billing_line1_0',
52 '_give_donor_address_billing_line2_0',
53 '_give_donor_address_billing_city_0',
54 '_give_donor_address_billing_state_0',
55 '_give_donor_address_billing_zip_0',
56 '_give_donor_address_billing_country_0',
57 '_give_stripe_customer_id',
58 ];
59
60 /**
61 * Resolve (or create) the SureDonation donor row for a given GiveWP payment.
62 *
63 * Caches the result on $progress['donor_map'] keyed by email so subsequent
64 * payments from the same donor in the same session do not re-query.
65 *
66 * @param array $payment_meta Flat assoc of GiveWP payment post meta.
67 * @param array $progress Session progress (passed by reference; donor_map updated).
68 * @return int Donor ID (>0) or 0 on failure.
69 * @since 1.0.0
70 */
71 public function get_or_create_for_payment( $payment_meta, &$progress ) {
72 $payment_meta = is_array( $payment_meta ) ? $payment_meta : [];
73
74 $email = isset( $payment_meta['_give_payment_donor_email'] ) ? sanitize_email( $payment_meta['_give_payment_donor_email'] ) : '';
75 if ( '' === $email || ! is_email( $email ) ) {
76 return 0;
77 }
78
79 // Session-level cache.
80 if ( isset( $progress['donor_map'][ $email ] ) ) {
81 return (int) $progress['donor_map'][ $email ];
82 }
83
84 // Existing SureDonation donor by email?
85 $existing = Donors::get_by_email( $email );
86 if ( is_array( $existing ) && ! empty( $existing['id'] ) ) {
87 $donor_id = (int) $existing['id'];
88 $progress['donor_map'][ $email ] = $donor_id;
89 return $donor_id;
90 }
91
92 // Build the row from payment meta.
93 $first_name = isset( $payment_meta['_give_donor_billing_first_name'] ) ? sanitize_text_field( $payment_meta['_give_donor_billing_first_name'] ) : '';
94 $last_name = isset( $payment_meta['_give_donor_billing_last_name'] ) ? sanitize_text_field( $payment_meta['_give_donor_billing_last_name'] ) : '';
95 $name = trim( $first_name . ' ' . $last_name );
96
97 // Optionally enrich from give_donors row + give_donormeta.
98 $give_donor_id = isset( $payment_meta['_give_payment_donor_id'] ) ? absint( $payment_meta['_give_payment_donor_id'] ) : 0;
99 $give_donor = $give_donor_id > 0 ? Source::get_instance()->get_donor( $give_donor_id ) : null;
100 if ( is_array( $give_donor ) && '' === $name && isset( $give_donor['name'] ) && is_scalar( $give_donor['name'] ) ) {
101 $name = sanitize_text_field( (string) $give_donor['name'] );
102 }
103
104 $donor_meta = $give_donor_id > 0 ? Source::get_instance()->get_donor_meta( $give_donor_id ) : [];
105 $profile = self::extract_donor_profile( $donor_meta );
106
107 // Prefer the structured donor-level address; fall back to billing
108 // fields on the payment meta if donor meta is empty.
109 $address = '' !== $profile['address_str']
110 ? $profile['address_str']
111 : $this->address_from_payment_meta( $payment_meta );
112
113 $wp_user_id = $this->resolve_wp_user_id( $payment_meta, $email );
114
115 $givewp_block = array_filter(
116 [
117 'source_id' => $give_donor_id,
118 'wp_user_id' => $this->raw_give_user_id( $payment_meta ),
119 'import_id' => isset( $progress['import_id'] ) ? (string) $progress['import_id'] : '',
120 'address' => $profile['address_struct'],
121 'stripe_customer_id' => $profile['stripe_customer_id'],
122 'donor_meta' => $profile['extra_meta'],
123 'anonymous' => $profile['anonymous'],
124 ],
125 static function ( $v ) {
126 if ( is_array( $v ) ) {
127 return ! empty( $v );
128 }
129 if ( is_string( $v ) ) {
130 return '' !== $v;
131 }
132 return ! empty( $v );
133 }
134 );
135
136 // Avatar URL goes at the top level of donor_data so the donor
137 // dashboard's resolve_avatar_url() picks it up for non-WP-linked
138 // donors. For WP-linked donors the dashboard resolver checks
139 // user_meta(`suredonation_avatar_url`) first (so the override
140 // also feeds the site-wide get_avatar / get_avatar_url filter
141 // added in suredonation-pro PR #7), so mirror the URL there
142 // too when we have a user_id.
143 $donor_data = [ 'givewp' => $givewp_block ];
144 if ( '' !== $profile['avatar_url'] ) {
145 $donor_data['avatar_url'] = $profile['avatar_url'];
146 if ( $wp_user_id > 0 ) {
147 update_user_meta( $wp_user_id, 'suredonation_avatar_url', $profile['avatar_url'] );
148 }
149 }
150
151 // first_donation_date / last_donation_date are intentionally NOT
152 // set here. They're stamped lazily by Donation_Mapper after each
153 // donation insert (using the actual payment date), so for the
154 // freshly created donor they accurately reflect their oldest
155 // and newest contribution rather than the import time.
156 $data = [
157 'email' => $email,
158 'name' => $name,
159 'phone' => $profile['phone'],
160 'company' => $profile['company'],
161 'user_id' => $wp_user_id,
162 'address' => $address,
163 'import_source_id' => $give_donor_id,
164 'import_source' => 'givewp',
165 'donor_data' => $donor_data,
166 ];
167
168 // Donors::add() bypasses maybe_link_wp_user() (which is only called from
169 // get_or_create), so no WP user is silently created during import.
170 $donor_id = Donors::add( $data );
171 if ( ! $donor_id ) {
172 return 0;
173 }
174
175 $progress['donor_map'][ $email ] = (int) $donor_id;
176 return (int) $donor_id;
177 }
178
179 /**
180 * Reduce a flat give_donormeta map into the structured profile our
181 * mappers consume: dedicated columns (phone, company), structured
182 * address parts, and known gateway keys, plus any unknown keys
183 * preserved raw under `extra_meta`.
184 *
185 * Mirrors the meta keys Charitable's importer reads (see
186 * class-charitable-givewp-importer.php:1093+).
187 *
188 * @param array<string,string> $donor_meta Flat give_donormeta map.
189 * @return array{phone:string,company:string,avatar_url:string,stripe_customer_id:string,anonymous:bool,address_str:string,address_struct:array<string,string>,extra_meta:array<string,string>}
190 * @since 1.0.0
191 */
192 public static function extract_donor_profile( $donor_meta ) {
193 $donor_meta = is_array( $donor_meta ) ? $donor_meta : [];
194
195 $get = static function ( $key ) use ( $donor_meta ) {
196 return isset( $donor_meta[ $key ] ) ? (string) $donor_meta[ $key ] : '';
197 };
198
199 $address_struct = array_filter(
200 [
201 'line1' => sanitize_text_field( $get( '_give_donor_address_billing_line1_0' ) ),
202 'line2' => sanitize_text_field( $get( '_give_donor_address_billing_line2_0' ) ),
203 'city' => sanitize_text_field( $get( '_give_donor_address_billing_city_0' ) ),
204 'state' => sanitize_text_field( $get( '_give_donor_address_billing_state_0' ) ),
205 'zip' => sanitize_text_field( $get( '_give_donor_address_billing_zip_0' ) ),
206 'country' => sanitize_text_field( $get( '_give_donor_address_billing_country_0' ) ),
207 ],
208 static function ( $v ) {
209 return '' !== $v;
210 }
211 );
212
213 $address_str = sanitize_text_field( implode( ', ', $address_struct ) );
214
215 $extra_meta = [];
216 foreach ( $donor_meta as $k => $v ) {
217 $key = (string) $k;
218 if ( in_array( $key, self::STANDARD_DONOR_META_KEYS, true ) ) {
219 continue;
220 }
221 $extra_meta[ $key ] = is_scalar( $v ) ? (string) $v : '';
222 }
223
224 return [
225 'phone' => sanitize_text_field( $get( '_give_donor_phone' ) ),
226 'company' => sanitize_text_field( $get( '_give_donor_company' ) ),
227 // Donor dashboard reads donor_data.avatar_url; the mapper
228 // stores it at that top-level slot, this field just surfaces
229 // the value from give_donormeta for the mapper to use.
230 'avatar_url' => esc_url_raw( $get( '_give_donor_avatar' ) ),
231 'stripe_customer_id' => sanitize_text_field( $get( '_give_stripe_customer_id' ) ),
232 'anonymous' => '1' === $get( '_give_donor_anonymous' ),
233 'address_str' => $address_str,
234 'address_struct' => $address_struct,
235 'extra_meta' => $extra_meta,
236 ];
237 }
238
239 /**
240 * Build a flat display address from payment-side billing meta.
241 *
242 * Fallback when donor-level meta is empty (older GiveWP installs or
243 * forms that disabled address capture).
244 *
245 * @param array<string,mixed> $payment_meta Payment meta.
246 * @return string
247 * @since 1.0.0
248 */
249 private function address_from_payment_meta( $payment_meta ) {
250 $parts = array_filter(
251 [
252 isset( $payment_meta['_give_donor_billing_address1'] ) ? (string) $payment_meta['_give_donor_billing_address1'] : '',
253 isset( $payment_meta['_give_donor_billing_address2'] ) ? (string) $payment_meta['_give_donor_billing_address2'] : '',
254 isset( $payment_meta['_give_donor_billing_city'] ) ? (string) $payment_meta['_give_donor_billing_city'] : '',
255 isset( $payment_meta['_give_donor_billing_state'] ) ? (string) $payment_meta['_give_donor_billing_state'] : '',
256 isset( $payment_meta['_give_donor_billing_zip'] ) ? (string) $payment_meta['_give_donor_billing_zip'] : '',
257 isset( $payment_meta['_give_donor_billing_country'] ) ? (string) $payment_meta['_give_donor_billing_country'] : '',
258 ]
259 );
260 return sanitize_text_field( implode( ', ', $parts ) );
261 }
262
263 /**
264 * Resolve a WP user ID for an imported donor, link-only-if-exists pattern.
265 *
266 * @param array $payment_meta GiveWP payment meta.
267 * @param string $email Donor email (already sanitized).
268 * @return int 0 if no WP user found.
269 * @since 1.0.0
270 */
271 private function resolve_wp_user_id( $payment_meta, $email ) {
272 $give_user_id = $this->raw_give_user_id( $payment_meta );
273 if ( $give_user_id > 0 ) {
274 $user = get_userdata( $give_user_id );
275 if ( $user instanceof \WP_User ) {
276 return (int) $user->ID;
277 }
278 }
279
280 if ( '' !== $email ) {
281 $user = get_user_by( 'email', $email );
282 if ( $user instanceof \WP_User ) {
283 return (int) $user->ID;
284 }
285 }
286
287 return 0;
288 }
289
290 /**
291 * Extract the GiveWP-stored WP user ID from payment meta as a positive int.
292 *
293 * @param array $payment_meta Payment meta.
294 * @return int 0 if not set or non-numeric.
295 * @since 1.0.0
296 */
297 private function raw_give_user_id( $payment_meta ) {
298 if ( ! is_array( $payment_meta ) || ! isset( $payment_meta['_give_payment_user_id'] ) ) {
299 return 0;
300 }
301 $raw = $payment_meta['_give_payment_user_id'];
302 return is_numeric( $raw ) ? absint( $raw ) : 0;
303 }
304 }
305