PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.4.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.4.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 / column-map.php

column-map.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.4.0, at inc/import-export/import/column-map.php

507 lines 16.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Column mapping for the own-data CSV importer.
4 *
5 * Defines the importable fields per entity (donations, donors), their header
6 * aliases, and the auto-mapping that matches a CSV's header row to those
7 * fields. The aliases include the exact labels SureDonation's own export
8 * produces, so an exported file round-trips with a 100% auto-map.
9 *
10 * @package SureDonation
11 * @since 1.3.0
12 */
13
14 namespace SureDonation\Inc\Import_Export\Import;
15
16 use SureDonation\Inc\Helper;
17
18 // Exit if accessed directly.
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 /**
24 * Column map / auto-mapping helper.
25 *
26 * @since 1.3.0
27 */
28 class Column_Map {
29
30 /**
31 * Importable donation fields. Each: label, aliases (lowercased header
32 * text this field auto-matches), and whether it is required.
33 *
34 * @return array<string, array<string, mixed>>
35 * @since 1.3.0
36 */
37 public static function donation_fields() {
38 return [
39 'import_source_id' => [
40 'label' => __( 'Donation ID', 'suredonation' ),
41 'aliases' => [ 'donation id', 'donation_id', 'id' ],
42 'required' => false,
43 ],
44 'donor_email' => [
45 'label' => __( 'Donor Email', 'suredonation' ),
46 'aliases' => [ 'donor email', 'email', 'email address', 'donor_email' ],
47 'required' => true,
48 ],
49 'amount' => [
50 'label' => __( 'Amount', 'suredonation' ),
51 'aliases' => [ 'amount', 'donation amount', 'donation total', 'total' ],
52 'required' => true,
53 ],
54 'donor_name' => [
55 'label' => __( 'Donor Name', 'suredonation' ),
56 'aliases' => [ 'donor name', 'name', 'full name', 'donor_name' ],
57 'required' => false,
58 ],
59 'first_name' => [
60 'label' => __( 'First Name', 'suredonation' ),
61 'aliases' => [ 'first name', 'first_name' ],
62 'required' => false,
63 ],
64 'last_name' => [
65 'label' => __( 'Last Name', 'suredonation' ),
66 'aliases' => [ 'last name', 'last_name' ],
67 'required' => false,
68 ],
69 'donor_phone' => [
70 'label' => __( 'Donor Phone', 'suredonation' ),
71 'aliases' => [ 'donor phone', 'phone', 'phone number', 'donor_phone' ],
72 'required' => false,
73 ],
74 'company' => [
75 'label' => __( 'Company', 'suredonation' ),
76 'aliases' => [ 'company', 'company name' ],
77 'required' => false,
78 ],
79 'address' => [
80 'label' => __( 'Address', 'suredonation' ),
81 'aliases' => [ 'address' ],
82 'required' => false,
83 ],
84 'campaign' => [
85 'label' => __( 'Campaign', 'suredonation' ),
86 'aliases' => [ 'campaign', 'campaign name', 'campaign title', 'campaign id', 'campaign_id' ],
87 'required' => false,
88 ],
89 'form' => [
90 'label' => __( 'Form', 'suredonation' ),
91 'aliases' => [ 'form', 'form title', 'form id', 'form_id' ],
92 'required' => false,
93 ],
94 'currency' => [
95 'label' => __( 'Currency', 'suredonation' ),
96 'aliases' => [ 'currency', 'currency code' ],
97 'required' => false,
98 ],
99 'gateway' => [
100 'label' => __( 'Gateway', 'suredonation' ),
101 'aliases' => [ 'gateway', 'payment gateway', 'payment method', 'method' ],
102 'required' => false,
103 ],
104 'payment_status' => [
105 'label' => __( 'Payment Status', 'suredonation' ),
106 'aliases' => [ 'payment status', 'status', 'donation status' ],
107 'required' => false,
108 ],
109 'payment_mode' => [
110 'label' => __( 'Payment Mode', 'suredonation' ),
111 'aliases' => [ 'payment mode', 'mode', 'test mode' ],
112 'required' => false,
113 ],
114 'transaction_id' => [
115 'label' => __( 'Transaction ID', 'suredonation' ),
116 'aliases' => [ 'transaction id', 'transaction_id' ],
117 'required' => false,
118 ],
119 'fees_covered' => [
120 'label' => __( 'Fees Covered', 'suredonation' ),
121 'aliases' => [ 'fees covered', 'fees_covered', 'fee covered' ],
122 'required' => false,
123 ],
124 'refunded_amount' => [
125 'label' => __( 'Refunded Amount', 'suredonation' ),
126 'aliases' => [ 'refunded amount', 'refunded_amount' ],
127 'required' => false,
128 ],
129 'is_anonymous' => [
130 'label' => __( 'Anonymous', 'suredonation' ),
131 'aliases' => [ 'anonymous', 'is anonymous', 'is_anonymous' ],
132 'required' => false,
133 ],
134 'donation_type' => [
135 'label' => __( 'Donation Type', 'suredonation' ),
136 'aliases' => [ 'donation type', 'type', 'donation_type' ],
137 'required' => false,
138 ],
139 'subscription_id' => [
140 'label' => __( 'Subscription ID', 'suredonation' ),
141 'aliases' => [ 'subscription id', 'subscription_id' ],
142 'required' => false,
143 ],
144 'subscription_status' => [
145 'label' => __( 'Subscription Status', 'suredonation' ),
146 'aliases' => [ 'subscription status', 'subscription_status' ],
147 'required' => false,
148 ],
149 'parent_subscription_id' => [
150 'label' => __( 'Parent Subscription ID', 'suredonation' ),
151 'aliases' => [ 'parent subscription id', 'parent_subscription_id' ],
152 'required' => false,
153 ],
154 'donor_comment' => [
155 'label' => __( 'Donor Comment', 'suredonation' ),
156 'aliases' => [ 'donor comment', 'comment', 'donor_comment' ],
157 'required' => false,
158 ],
159 'ip_address' => [
160 'label' => __( 'IP Address', 'suredonation' ),
161 'aliases' => [ 'ip address', 'ip', 'ip_address' ],
162 'required' => false,
163 ],
164 'donation_date' => [
165 'label' => __( 'Date', 'suredonation' ),
166 'aliases' => [ 'date', 'donation date', 'created at', 'created_at', 'donation_date' ],
167 'required' => false,
168 ],
169 ];
170 }
171
172 /**
173 * Importable donor fields.
174 *
175 * @return array<string, array<string, mixed>>
176 * @since 1.3.0
177 */
178 public static function donor_fields() {
179 return [
180 'import_source_id' => [
181 'label' => __( 'Donor ID', 'suredonation' ),
182 'aliases' => [ 'donor id', 'donor_id', 'id' ],
183 'required' => false,
184 ],
185 'email' => [
186 'label' => __( 'Email', 'suredonation' ),
187 'aliases' => [ 'email', 'donor email', 'email address' ],
188 'required' => true,
189 ],
190 'name' => [
191 'label' => __( 'Name', 'suredonation' ),
192 'aliases' => [ 'name', 'donor name', 'full name' ],
193 'required' => false,
194 ],
195 'phone' => [
196 'label' => __( 'Phone', 'suredonation' ),
197 'aliases' => [ 'phone', 'phone number', 'donor phone' ],
198 'required' => false,
199 ],
200 'company' => [
201 'label' => __( 'Company', 'suredonation' ),
202 'aliases' => [ 'company', 'company name' ],
203 'required' => false,
204 ],
205 'address' => [
206 'label' => __( 'Address', 'suredonation' ),
207 'aliases' => [ 'address' ],
208 'required' => false,
209 ],
210 'donor_status' => [
211 'label' => __( 'Status', 'suredonation' ),
212 'aliases' => [ 'status', 'donor status' ],
213 'required' => false,
214 ],
215 'donor_tags' => [
216 'label' => __( 'Tags', 'suredonation' ),
217 'aliases' => [ 'tags', 'donor tags' ],
218 'required' => false,
219 ],
220 'total_donated' => [
221 'label' => __( 'Total Donated', 'suredonation' ),
222 'aliases' => [ 'total donated', 'total_donated' ],
223 'required' => false,
224 ],
225 'donation_count' => [
226 'label' => __( 'Donation Count', 'suredonation' ),
227 'aliases' => [ 'donation count', 'donation_count', 'number of donations' ],
228 'required' => false,
229 ],
230 'largest_donation' => [
231 'label' => __( 'Largest Donation', 'suredonation' ),
232 'aliases' => [ 'largest donation', 'largest_donation' ],
233 'required' => false,
234 ],
235 'first_donation_date' => [
236 'label' => __( 'First Donation', 'suredonation' ),
237 'aliases' => [ 'first donation', 'first donation date', 'first_donation_date' ],
238 'required' => false,
239 ],
240 'last_donation_date' => [
241 'label' => __( 'Last Donation', 'suredonation' ),
242 'aliases' => [ 'last donation', 'last donation date', 'last_donation_date' ],
243 'required' => false,
244 ],
245 'created_at' => [
246 'label' => __( 'Created At', 'suredonation' ),
247 'aliases' => [ 'created at', 'created_at' ],
248 'required' => false,
249 ],
250 ];
251 }
252
253 /**
254 * Get the field definitions for an entity.
255 *
256 * @param string $entity 'donations' or 'donors'.
257 * @return array<string, array<string, mixed>> Field definitions.
258 * @since 1.3.0
259 */
260 public static function fields_for( $entity ) {
261 return 'donors' === $entity ? self::donor_fields() : self::donation_fields();
262 }
263
264 /**
265 * Auto-map a CSV header row to entity fields.
266 *
267 * Case-insensitive exact match of each (trimmed) header against the field
268 * aliases; each field is auto-assigned to at most one header (first match
269 * wins), so duplicate-labelled columns don't collide.
270 *
271 * @param array<int, string> $headers CSV header cells.
272 * @param string $entity 'donations' or 'donors'.
273 * @return array<int, string> Map of header index => field key ('' = unmapped).
274 * @since 1.3.0
275 */
276 public static function auto_map( $headers, $entity ) {
277 $fields = self::fields_for( $entity );
278 $used = [];
279 $map = [];
280
281 foreach ( $headers as $index => $header ) {
282 $needle = strtolower( trim( (string) $header ) );
283 $map[ $index ] = '';
284
285 if ( '' === $needle ) {
286 continue;
287 }
288
289 foreach ( $fields as $field => $def ) {
290 if ( isset( $used[ $field ] ) ) {
291 continue;
292 }
293 if ( in_array( $needle, self::aliases_for( $def ), true ) ) {
294 $map[ $index ] = $field;
295 $used[ $field ] = true;
296 break;
297 }
298 }
299 }
300
301 return $map;
302 }
303
304 /**
305 * The header text a field auto-matches: its declared (English) aliases plus
306 * its own localized label.
307 *
308 * The exporters write translated labels as the header row, so on a non-English
309 * site the site's own export would not match the English aliases at all. The
310 * label is therefore an implicit alias, which keeps the round-trip working in
311 * every locale while still accepting the English headers of a hand-made file.
312 *
313 * @param array<string, mixed> $def Field definition.
314 * @return array<int, string> Lowercased header text this field matches.
315 * @since 1.3.0
316 */
317 private static function aliases_for( $def ) {
318 $aliases = isset( $def['aliases'] ) && is_array( $def['aliases'] ) ? $def['aliases'] : [];
319 $label = strtolower( trim( Helper::get_string_value( $def['label'] ?? '' ) ) );
320
321 if ( '' !== $label && ! in_array( $label, $aliases, true ) ) {
322 $aliases[] = $label;
323 }
324
325 return $aliases;
326 }
327
328 /**
329 * Header text that signals an entity, collected from the given fields'
330 * aliases and localized labels.
331 *
332 * @param string $entity 'donations' or 'donors'.
333 * @param array<int,string> $keys Field keys that only that entity exports.
334 * @return array<int, string> Lowercased signal header text.
335 * @since 1.3.0
336 */
337 private static function signals_for( $entity, $keys ) {
338 $fields = self::fields_for( $entity );
339 $signals = [];
340
341 foreach ( $keys as $key ) {
342 if ( isset( $fields[ $key ] ) ) {
343 $signals = array_merge( $signals, self::aliases_for( $fields[ $key ] ) );
344 }
345 }
346
347 return array_values( array_unique( $signals ) );
348 }
349
350 /**
351 * Guess whether a header row describes donations or donors.
352 *
353 * Scores the headers against each entity's donor-only / donation-only
354 * signal columns; the higher score wins, defaulting to donations on a tie.
355 * Signals are derived from the field definitions so they carry the localized
356 * labels too — otherwise a translated export scores zero on both sides.
357 *
358 * @param array<int, string> $headers CSV header cells.
359 * @return string 'donations' or 'donors'.
360 * @since 1.3.0
361 */
362 public static function detect_entity( $headers ) {
363 $normalized = array_map(
364 static function ( $header ) {
365 return strtolower( trim( (string) $header ) );
366 },
367 $headers
368 );
369
370 $donation_signals = self::signals_for( 'donations', [ 'amount', 'payment_status', 'transaction_id', 'gateway' ] );
371 $donor_signals = self::signals_for( 'donors', [ 'total_donated', 'donation_count', 'largest_donation' ] );
372
373 $donation_score = count( array_intersect( $normalized, $donation_signals ) );
374 $donor_score = count( array_intersect( $normalized, $donor_signals ) );
375
376 return $donor_score > $donation_score ? 'donors' : 'donations';
377 }
378
379 /**
380 * Apply a column mapping to a raw CSV row, producing a field => value map.
381 *
382 * @param array<int, string> $row Raw cell values (column order).
383 * @param array<int|string, mixed> $mapping Header index => field key.
384 * @return array<string, string> Field => value (unmapped columns dropped).
385 * @since 1.3.0
386 */
387 public static function apply_row( $row, $mapping ) {
388 $data = [];
389 if ( ! is_array( $mapping ) ) {
390 return $data;
391 }
392 foreach ( $mapping as $index => $field ) {
393 $field_key = Helper::get_string_value( $field );
394 if ( '' === $field_key ) {
395 continue;
396 }
397 $data[ $field_key ] = isset( $row[ $index ] ) ? (string) $row[ $index ] : '';
398 }
399 return $data;
400 }
401
402 /**
403 * The fixed set of standard column labels a SureDonation donations export
404 * emits, in order, before any custom form-field columns.
405 *
406 * Single source of truth: the exporter uses this for its header row, and the
407 * importer treats any column NOT in this set as a submitted custom field. The
408 * order here must match the value order the exporter writes per row.
409 *
410 * @return array<int, string> Column labels.
411 * @since 1.3.0
412 */
413 public static function standard_donation_export_labels() {
414 return [
415 __( 'Donation ID', 'suredonation' ),
416 __( 'Campaign ID', 'suredonation' ),
417 __( 'Campaign Name', 'suredonation' ),
418 __( 'Form ID', 'suredonation' ),
419 __( 'Form Title', 'suredonation' ),
420 __( 'Donor ID', 'suredonation' ),
421 __( 'Donor Name', 'suredonation' ),
422 __( 'Donor Email', 'suredonation' ),
423 __( 'Donor Phone', 'suredonation' ),
424 __( 'Amount', 'suredonation' ),
425 __( 'Fees Covered', 'suredonation' ),
426 __( 'Refunded Amount', 'suredonation' ),
427 __( 'Currency', 'suredonation' ),
428 __( 'Gateway', 'suredonation' ),
429 __( 'Payment Status', 'suredonation' ),
430 __( 'Payment Mode', 'suredonation' ),
431 __( 'Transaction ID', 'suredonation' ),
432 __( 'Donation Type', 'suredonation' ),
433 __( 'Subscription ID', 'suredonation' ),
434 __( 'Subscription Status', 'suredonation' ),
435 __( 'Parent Subscription ID', 'suredonation' ),
436 __( 'Anonymous', 'suredonation' ),
437 __( 'Donor Comment', 'suredonation' ),
438 __( 'IP Address', 'suredonation' ),
439 __( 'Date', 'suredonation' ),
440 __( 'Import Source', 'suredonation' ),
441 __( 'Import Source ID', 'suredonation' ),
442 ];
443 }
444
445 /**
446 * Pull submitted custom form-field values out of a donation row: any column
447 * whose header is not a standard export column is treated as a custom field
448 * (these are the per-form fields flattened into trailing export columns).
449 *
450 * @param array<int, string> $headers CSV header cells.
451 * @param array<int, string> $row Raw row cells (column order).
452 * @return array<string, array{label: string, value: string}> Custom fields keyed by slug.
453 * @since 1.3.0
454 */
455 public static function extract_custom_fields( $headers, $row ) {
456 $standard = array_map(
457 static function ( $label ) {
458 return strtolower( trim( (string) $label ) );
459 },
460 self::standard_donation_export_labels()
461 );
462
463 $fields = [];
464 foreach ( $headers as $index => $header ) {
465 $label = trim( (string) $header );
466 if ( '' === $label || in_array( strtolower( $label ), $standard, true ) ) {
467 continue;
468 }
469 $value = isset( $row[ $index ] ) ? (string) $row[ $index ] : '';
470 if ( '' === $value ) {
471 continue;
472 }
473 $key = sanitize_key( $label );
474 if ( '' === $key ) {
475 $key = 'field_' . (int) $index;
476 }
477 $fields[ $key ] = [
478 'label' => sanitize_text_field( $label ),
479 'value' => sanitize_text_field( $value ),
480 ];
481 }
482 return $fields;
483 }
484
485 /**
486 * Field keys that are required but missing from a mapping.
487 *
488 * @param array<int, string> $mapping Header index => field key.
489 * @param string $entity 'donations' or 'donors'.
490 * @return array<int, string> Missing required field labels.
491 * @since 1.3.0
492 */
493 public static function missing_required( $mapping, $entity ) {
494 $fields = self::fields_for( $entity );
495 $mapped = array_values( array_filter( $mapping ) );
496 $missing = [];
497
498 foreach ( $fields as $field => $def ) {
499 if ( ! empty( $def['required'] ) && ! in_array( $field, $mapped, true ) ) {
500 $missing[] = Helper::get_string_value( $def['label'] );
501 }
502 }
503
504 return $missing;
505 }
506 }
507