PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.5.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.5.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-export / import / donors-import-mapper.php

donors-import-mapper.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.5.0, at inc/import-export/import/donors-import-mapper.php

237 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Donors import mapper.
4 *
5 * Imports donor profiles from a SureDonation-exported CSV (a "pure donor"
6 * import — profiles only, no donation history). Matches by unique email:
7 * existing donors are updated with the non-empty fields provided, new donors
8 * are created via Donors::add() (an existing WP user is linked by email; none
9 * is created).
10 *
11 * @package SureDonation
12 * @since 1.3.0
13 */
14
15 namespace SureDonation\Inc\Import_Export\Import;
16
17 use SureDonation\Inc\Database\Tables\Donors;
18 use SureDonation\Inc\Helper;
19 use SureDonation\Inc\Traits\Get_Instance;
20
21 // Exit if accessed directly.
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 /**
27 * Donors import mapper.
28 *
29 * @phpstan-type ImportResult array{imported:int, skipped:int, errors:int, donors_created:int, donors_matched:int, error_log:array<int, string>}
30 *
31 * @since 1.3.0
32 */
33 class Donors_Import_Mapper {
34
35 use Get_Instance;
36
37 /**
38 * Process one batch of donor rows.
39 *
40 * @param array<string, mixed> $progress Session progress (by reference).
41 * @param int $offset Data-row offset.
42 * @return int Number of source rows fetched this batch.
43 * @since 1.3.0
44 */
45 public function process_batch( array &$progress, $offset ) {
46 $token = Helper::get_string_value( $progress['token'] ?? '' );
47 $mapping = is_array( $progress['mapping'] ?? null ) ? $progress['mapping'] : [];
48 $options = is_array( $progress['options'] ?? null ) ? $progress['options'] : [];
49 $dry_run = ! empty( $options['dry_run'] );
50
51 // Offset 0 marks the start of the phase, so reset the byte cursor to the
52 // top of the file; otherwise resume from the stored byte position.
53 $byte_offset = ( 0 === (int) $offset ) ? 0 : Helper::get_integer_value( $progress['byte_offset'] ?? 0 );
54 $rows = Csv_File::read_batch( $token, $byte_offset, Import_Runner::BATCH_SIZE );
55 $progress['byte_offset'] = $byte_offset;
56 if ( empty( $rows ) ) {
57 return 0;
58 }
59
60 foreach ( $rows as $row ) {
61 $this->process_row( $row, $mapping, $progress, $dry_run );
62 }
63
64 return count( $rows );
65 }
66
67 /**
68 * Import a single donor row.
69 *
70 * @param array<int, string> $row Raw CSV cells.
71 * @param array<int|string, mixed> $mapping Header index => field.
72 * @param array<string, mixed> $progress Session progress (by reference).
73 * @param bool $dry_run Whether to write.
74 * @return void
75 * @since 1.3.0
76 */
77 private function process_row( $row, $mapping, &$progress, $dry_run ) {
78 /** @var array<string, ImportResult> $results */
79 $results = &$progress['results'];
80 $result = &$results['donors'];
81 $data = Column_Map::apply_row( $row, $mapping );
82
83 $email = sanitize_email( Helper::get_string_value( $data['email'] ?? '' ) );
84 if ( '' === $email || ! is_email( $email ) ) {
85 ++$result['errors'];
86 if ( count( $result['error_log'] ) < 50 ) {
87 $result['error_log'][] = __( 'Row skipped: missing or invalid email.', 'suredonation' );
88 }
89 return;
90 }
91
92 $fields = $this->build_fields( $data, $email );
93 $existing = Donors::get_by_email( $email );
94
95 if ( is_array( $existing ) && ! empty( $existing['id'] ) ) {
96 if ( ! $dry_run ) {
97 Donors::update( (int) $existing['id'], $this->fields_for_update( $fields, $data ) );
98 }
99 ++$result['donors_matched'];
100 ++$result['skipped'];
101 return;
102 }
103
104 ++$result['donors_created'];
105 ++$result['imported'];
106
107 if ( $dry_run ) {
108 return;
109 }
110
111 $user = get_user_by( 'email', $email );
112 $fields['user_id'] = $user instanceof \WP_User ? (int) $user->ID : null;
113 $donor_id = Donors::add( $fields );
114 if ( $donor_id ) {
115 Import_Runner::track_created( $progress, 'donors', (int) $donor_id );
116 // Action documented in the donations import mapper.
117 do_action( 'suredonation_import_donor_inserted', (int) $donor_id, $email );
118 }
119 }
120
121 /**
122 * Build the donor column data from a mapped row.
123 *
124 * Only non-empty values are included so an update doesn't blank existing
125 * fields. Aggregate columns are taken from the CSV as provided.
126 *
127 * @param array<string, string> $data Mapped row fields.
128 * @param string $email Sanitized email.
129 * @return array<string, mixed> Donor column data.
130 * @since 1.3.0
131 */
132 private function build_fields( $data, $email ) {
133 $fields = [
134 'email' => $email,
135 'import_source' => 'suredonation',
136 ];
137
138 $text_map = [
139 'name' => 'name',
140 'phone' => 'phone',
141 'company' => 'company',
142 ];
143 foreach ( $text_map as $field => $source ) {
144 $value = sanitize_text_field( Helper::get_string_value( $data[ $source ] ?? '' ) );
145 if ( '' !== $value ) {
146 $fields[ $field ] = $value;
147 }
148 }
149
150 $address = sanitize_textarea_field( Helper::get_string_value( $data['address'] ?? '' ) );
151 if ( '' !== $address ) {
152 $fields['address'] = $address;
153 }
154
155 $status = sanitize_text_field( Helper::get_string_value( $data['donor_status'] ?? '' ) );
156 $fields['donor_status'] = '' !== $status ? $status : 'active';
157
158 $source_id = isset( $data['import_source_id'] ) ? absint( $data['import_source_id'] ) : 0;
159 if ( $source_id > 0 ) {
160 $fields['import_source_id'] = $source_id;
161 }
162
163 $tags = trim( Helper::get_string_value( $data['donor_tags'] ?? '' ) );
164 if ( '' !== $tags ) {
165 $fields['donor_tags'] = array_values(
166 array_filter(
167 array_map(
168 static function ( $tag ) {
169 return sanitize_text_field( trim( $tag ) );
170 },
171 explode( ',', $tags )
172 )
173 )
174 );
175 }
176
177 foreach ( [ 'total_donated', 'largest_donation' ] as $decimal_field ) {
178 $raw = Helper::get_string_value( $data[ $decimal_field ] ?? '' );
179 if ( '' !== trim( $raw ) ) {
180 $fields[ $decimal_field ] = number_format( (float) preg_replace( '/[^0-9.\-]/', '', $raw ), 8, '.', '' );
181 }
182 }
183
184 $count = Helper::get_string_value( $data['donation_count'] ?? '' );
185 if ( '' !== trim( $count ) ) {
186 $fields['donation_count'] = absint( $count );
187 }
188
189 foreach ( [ 'first_donation_date', 'last_donation_date' ] as $date_field ) {
190 $raw = trim( Helper::get_string_value( $data[ $date_field ] ?? '' ) );
191 if ( '' !== $raw ) {
192 $ts = strtotime( $raw );
193 if ( false !== $ts ) {
194 $fields[ $date_field ] = gmdate( 'Y-m-d H:i:s', $ts );
195 }
196 }
197 }
198
199 return $fields;
200 }
201
202 /**
203 * Reduce the full field set to the columns that are safe to write on an
204 * EXISTING donor. Re-importing an already-known donor must not:
205 * - retag their provenance (`import_source`) — a native donor would be
206 * relabelled 'suredonation';
207 * - silently reactivate a blocked/inactive donor — `donor_status`
208 * defaults to 'active' only for newly created donors, so it is written
209 * on update only when the CSV explicitly supplied a status;
210 * - clobber the live-computed aggregate columns with the CSV snapshot.
211 * Profile fields (name/phone/company/address/tags) still update when the
212 * CSV provides a non-empty value.
213 *
214 * @param array<string, mixed> $fields Full field set from build_fields().
215 * @param array<string, string> $data Mapped row (to detect an explicit status).
216 * @return array<string, mixed> Fields safe to write on an existing donor.
217 * @since 1.3.0
218 */
219 private function fields_for_update( $fields, $data ) {
220 unset(
221 $fields['import_source'],
222 $fields['total_donated'],
223 $fields['largest_donation'],
224 $fields['donation_count'],
225 $fields['first_donation_date'],
226 $fields['last_donation_date']
227 );
228
229 $status = sanitize_text_field( Helper::get_string_value( $data['donor_status'] ?? '' ) );
230 if ( '' === $status ) {
231 unset( $fields['donor_status'] );
232 }
233
234 return $fields;
235 }
236 }
237