PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.0
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 / charitable / donor-mapper.php

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

276 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Maps Charitable donors onto SureDonation donor rows.
4 *
5 * Shared resolver used by the donations phase (Free), the CSV path, and the
6 * Pro subscription / standalone-donor phases. Resolves by email against the
7 * existing SureDonation donors table, creating a new row only when needed —
8 * and never creating a WordPress user.
9 *
10 * @package SureDonation
11 * @since 1.5.1
12 */
13
14 namespace SureDonation\Inc\Import\Charitable;
15
16 use SureDonation\Inc\Database\Tables\Donors;
17 use SureDonation\Inc\Traits\Get_Instance;
18
19 // Exit if accessed directly.
20 defined( 'ABSPATH' ) || exit;
21
22 /**
23 * Donor_Mapper class.
24 *
25 * @since 1.5.1
26 */
27 class Donor_Mapper {
28 use Get_Instance;
29
30 /**
31 * Resolve (or create) the SureDonation donor row for a Charitable donation.
32 *
33 * Caches the result on $progress['donor_map'] keyed by email so subsequent
34 * donations from the same donor in the same session do not re-query.
35 *
36 * @param array<string, string> $snapshot The donation's serialized donor snapshot (Source::extract_donor_snapshot()).
37 * @param int $charitable_donor_id Charitable donors-table ID (0 when unknown).
38 * @param array<string, string> $donation_meta Flat donation post meta (for contact_consent fallback).
39 * @param array<string, mixed> $progress Session progress (by reference; donor_map updated).
40 * @return int Donor ID (>0) or 0 on failure.
41 * @since 1.5.1
42 */
43 public function get_or_create_for_donation( $snapshot, $charitable_donor_id, $donation_meta, &$progress ) {
44 $snapshot = is_array( $snapshot ) ? $snapshot : [];
45 $donation_meta = is_array( $donation_meta ) ? $donation_meta : [];
46 $charitable_donor_id = absint( $charitable_donor_id );
47
48 $donor_row = $charitable_donor_id > 0 ? Source::get_instance()->get_donor( $charitable_donor_id ) : null;
49 $donor_row = is_array( $donor_row ) ? $donor_row : [];
50
51 $email = isset( $snapshot['email'] ) ? sanitize_email( (string) $snapshot['email'] ) : '';
52 if ( ( '' === $email || ! is_email( $email ) ) && isset( $donor_row['email'] ) && is_scalar( $donor_row['email'] ) ) {
53 $email = sanitize_email( (string) $donor_row['email'] );
54 }
55 if ( '' === $email || ! is_email( $email ) ) {
56 return 0;
57 }
58
59 // Session-level cache.
60 $donor_map = isset( $progress['donor_map'] ) && is_array( $progress['donor_map'] ) ? $progress['donor_map'] : [];
61 if ( isset( $donor_map[ $email ] ) && is_numeric( $donor_map[ $email ] ) ) {
62 return (int) $donor_map[ $email ];
63 }
64
65 // Existing SureDonation donor by email?
66 $existing = Donors::get_by_email( $email );
67 if ( is_array( $existing ) && ! empty( $existing['id'] ) && is_numeric( $existing['id'] ) ) {
68 $donor_id = (int) $existing['id'];
69 $donor_map[ $email ] = $donor_id;
70 $progress['donor_map'] = $donor_map;
71 return $donor_id;
72 }
73
74 // Name: snapshot first, donors-table fallback.
75 $snap_first = isset( $snapshot['first_name'] ) && is_scalar( $snapshot['first_name'] ) ? (string) $snapshot['first_name'] : '';
76 $snap_last = isset( $snapshot['last_name'] ) && is_scalar( $snapshot['last_name'] ) ? (string) $snapshot['last_name'] : '';
77 $name = trim( trim( $snap_first ) . ' ' . trim( $snap_last ) );
78 if ( '' === $name ) {
79 $row_first = isset( $donor_row['first_name'] ) && is_scalar( $donor_row['first_name'] ) ? (string) $donor_row['first_name'] : '';
80 $row_last = isset( $donor_row['last_name'] ) && is_scalar( $donor_row['last_name'] ) ? (string) $donor_row['last_name'] : '';
81 $name = trim( sanitize_text_field( $row_first ) . ' ' . sanitize_text_field( $row_last ) );
82 }
83
84 $wp_user_id = $this->resolve_wp_user_id( $donor_row, $email );
85
86 // Address/phone: snapshot first; usermeta donor_* fallback for
87 // registered users (Charitable's two storage locations).
88 $address_struct = $this->address_struct_from_snapshot( $snapshot );
89 $address = $this->flatten_address( $address_struct );
90 $phone = isset( $snapshot['phone'] ) && is_scalar( $snapshot['phone'] ) ? sanitize_text_field( (string) $snapshot['phone'] ) : '';
91
92 if ( '' === $address && $wp_user_id > 0 ) {
93 $address_struct = $this->address_struct_from_usermeta( $wp_user_id );
94 $address = $this->flatten_address( $address_struct );
95 }
96 if ( '' === $phone && $wp_user_id > 0 ) {
97 $user_phone = get_user_meta( $wp_user_id, 'donor_phone', true );
98 $phone = is_scalar( $user_phone ) ? sanitize_text_field( (string) $user_phone ) : '';
99 }
100
101 $contact_consent = ! empty( $donor_row['contact_consent'] ) || ! empty( $donation_meta['contact_consent'] );
102
103 $donor_meta = $charitable_donor_id > 0 ? Source::get_instance()->get_donor_meta( $charitable_donor_id ) : [];
104 $profile = self::extract_donor_profile( $donor_meta );
105
106 $charitable_block = array_filter(
107 [
108 'source_id' => $charitable_donor_id,
109 'wp_user_id' => isset( $donor_row['user_id'] ) && is_numeric( $donor_row['user_id'] ) ? absint( $donor_row['user_id'] ) : 0,
110 'import_id' => isset( $progress['import_id'] ) && is_scalar( $progress['import_id'] ) ? (string) $progress['import_id'] : '',
111 'address' => $address_struct,
112 'contact_consent' => $contact_consent,
113 'date_joined' => isset( $donor_row['date_joined'] ) && is_scalar( $donor_row['date_joined'] ) ? sanitize_text_field( (string) $donor_row['date_joined'] ) : '',
114 'donor_meta' => $profile['extra_meta'],
115 ],
116 static function ( $v ) {
117 if ( is_array( $v ) ) {
118 return ! empty( $v );
119 }
120 if ( is_string( $v ) ) {
121 return '' !== $v;
122 }
123 return ! empty( $v );
124 }
125 );
126
127 // first_donation_date / last_donation_date are intentionally NOT set
128 // here — Donation_Mapper stamps them after each insert using the
129 // actual donation date, so they reflect the donor's real history
130 // rather than the import time.
131 $data = [
132 'email' => $email,
133 'name' => $name,
134 'phone' => $phone,
135 'company' => '',
136 'user_id' => $wp_user_id,
137 'address' => $address,
138 'import_source_id' => $charitable_donor_id,
139 'import_source' => 'charitable',
140 'donor_data' => [ 'charitable' => $charitable_block ],
141 ];
142
143 // Donors::add() bypasses maybe_link_wp_user() (which is only called from
144 // get_or_create), so no WP user is silently created during import.
145 $donor_id = Donors::add( $data );
146 if ( ! $donor_id ) {
147 return 0;
148 }
149
150 $donor_map[ $email ] = (int) $donor_id;
151 $progress['donor_map'] = $donor_map;
152 return (int) $donor_id;
153 }
154
155 /**
156 * Reduce a flat charitable_donormeta map into the structured profile shape
157 * the mappers consume (mirrors the GiveWP profile extractor so the Pro
158 * standalone-donor mapper can reuse it unchanged).
159 *
160 * Charitable's free plugin defines no fixed donormeta vocabulary, so
161 * everything lands in extra_meta and is preserved raw on donor_data.
162 *
163 * @param array<string, string> $donor_meta Flat donormeta map.
164 * @return array{phone: string, company: string, address_str: string, address_struct: array<string, string>, extra_meta: array<string, string>}
165 * @since 1.5.1
166 */
167 public static function extract_donor_profile( $donor_meta ) {
168 $donor_meta = is_array( $donor_meta ) ? $donor_meta : [];
169
170 $extra = [];
171 foreach ( $donor_meta as $key => $value ) {
172 $clean_key = sanitize_key( (string) $key );
173 if ( '' === $clean_key || ! is_scalar( $value ) ) {
174 continue;
175 }
176 $extra[ $clean_key ] = sanitize_text_field( (string) $value );
177 }
178
179 return [
180 'phone' => '',
181 'company' => '',
182 'address_str' => '',
183 'address_struct' => [],
184 'extra_meta' => $extra,
185 ];
186 }
187
188 /**
189 * Build the structured address from a donation's donor snapshot.
190 *
191 * @param array<string, string> $snapshot Donor snapshot.
192 * @return array<string, string> line1/line2/city/state/zip/country (empty values omitted).
193 * @since 1.5.1
194 */
195 private function address_struct_from_snapshot( $snapshot ) {
196 $pick = static function ( $key ) use ( $snapshot ) {
197 return isset( $snapshot[ $key ] ) && is_scalar( $snapshot[ $key ] ) ? sanitize_text_field( (string) $snapshot[ $key ] ) : '';
198 };
199
200 return array_filter(
201 [
202 'line1' => $pick( 'address' ),
203 'line2' => $pick( 'address_2' ),
204 'city' => $pick( 'city' ),
205 'state' => $pick( 'state' ),
206 'zip' => $pick( 'postcode' ),
207 'country' => $pick( 'country' ),
208 ]
209 );
210 }
211
212 /**
213 * Build the structured address from Charitable's donor_* usermeta keys.
214 *
215 * @param int $user_id WordPress user ID.
216 * @return array<string, string>
217 * @since 1.5.1
218 */
219 private function address_struct_from_usermeta( $user_id ) {
220 $map = [
221 'line1' => 'donor_address',
222 'line2' => 'donor_address_2',
223 'city' => 'donor_city',
224 'state' => 'donor_state',
225 'zip' => 'donor_postcode',
226 'country' => 'donor_country',
227 ];
228
229 $struct = [];
230 foreach ( $map as $slot => $meta_key ) {
231 $raw = get_user_meta( $user_id, $meta_key, true );
232 $value = is_scalar( $raw ) ? sanitize_text_field( (string) $raw ) : '';
233 if ( '' !== $value ) {
234 $struct[ $slot ] = $value;
235 }
236 }
237
238 return $struct;
239 }
240
241 /**
242 * Flatten a structured address to the single-line string stored on the
243 * donors table.
244 *
245 * @param array<string, string> $struct Structured address.
246 * @return string
247 * @since 1.5.1
248 */
249 private function flatten_address( $struct ) {
250 return implode( ', ', array_filter( array_map( 'trim', array_values( (array) $struct ) ) ) );
251 }
252
253 /**
254 * Link to an existing WordPress user only — never create one.
255 *
256 * Tries the charitable_donors.user_id column first, then an email match.
257 *
258 * @param array<string, mixed> $donor_row Charitable donors-table row (may be empty).
259 * @param string $email Donor email.
260 * @return int WP user ID or 0.
261 * @since 1.5.1
262 */
263 private function resolve_wp_user_id( $donor_row, $email ) {
264 $row_user_id = isset( $donor_row['user_id'] ) && is_numeric( $donor_row['user_id'] ) ? absint( $donor_row['user_id'] ) : 0;
265 if ( $row_user_id > 0 ) {
266 $user = get_userdata( $row_user_id );
267 if ( $user instanceof \WP_User ) {
268 return (int) $user->ID;
269 }
270 }
271
272 $user = get_user_by( 'email', $email );
273 return $user instanceof \WP_User ? (int) $user->ID : 0;
274 }
275 }
276