PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.12.0
GiveWP – Donation Plugin and Fundraising Platform v4.12.0
4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 2.31.0 2.31.1 2.32.0 2.33.0 2.33.1 2.33.2 2.33.3 2.33.4 2.33.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.5.0 2.5.1 2.5.10 2.5.11 2.5.12 2.5.13 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.9.0 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.10.0 3.11.0 3.12.0 3.12.1 3.12.2 3.12.3 3.13.0 3.14.0 3.14.1 3.14.2 3.15.0 3.15.1 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.17.0 3.17.1 3.17.2 3.18.0 3.19.0 3.19.1 3.19.2 3.19.3 3.19.4 3.2.0 3.2.1 3.2.2 3.20.0 3.21.0 3.21.1 3.22.0 3.22.1 3.22.2 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.7.0 3.8.0 3.9.0 4.0.0 4.1.0 4.1.1 4.10.0 4.10.1 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.14.5 4.14.6 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.1 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 trunk 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.2 2.11.3 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.13.1 2.13.2 2.13.3 2.13.4 2.14.0 2.15.0 2.16.0 2.16.1 2.17.0 2.17.1 2.17.3 2.18.0 2.18.1 2.19.1 2.19.2 2.19.3 2.19.4 2.19.5 2.19.6 2.19.7 2.19.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.20.0 2.20.1 2.20.2 2.21.0 2.21.1 2.21.2 2.21.3 2.21.4 2.22.0 2.22.1 2.22.2 2.22.3 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.24.2 2.25.0 2.25.1 2.25.2 2.25.3 2.26.0 2.27.0 2.27.1 2.27.2 2.27.3 2.28.0 2.29.0 2.29.1 2.29.2
give / includes / admin / tools / import / class-give-import-subscriptions.php
give / includes / admin / tools / import Last commit date
class-give-import-core-settings.php 1 year ago class-give-import-donations.php 2 years ago class-give-import-subscriptions.php 8 months ago
class-give-import-subscriptions.php
1281 lines
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit; // Exit if accessed directly
5 }
6
7 use Give\Donations\Models\Donation;
8 use Give\Donations\ValueObjects\DonationMetaKeys;
9 use Give\Framework\Database\DB;
10
11 if (!class_exists('Give_Import_Subscriptions')) {
12
13 /**
14 * Give_Import_Subscriptions.
15 *
16 * @since 4.11.0
17 */
18 final class Give_Import_Subscriptions
19 {
20 /**
21 * Importer type
22 *
23 * @var string
24 */
25 private $importer_type = 'import_subscriptions';
26
27 /**
28 * Instance.
29 *
30 * @var static
31 */
32 private static $instance;
33
34 /**
35 * Importing rows per page.
36 *
37 * @var int
38 */
39 public static $per_page = 25;
40
41 /**
42 * CSV valid redirect URL
43 *
44 * @var string|bool
45 */
46 public $is_csv_valid = false;
47
48 /**
49 * Singleton
50 * @since 4.11.0
51 */
52 private function __construct()
53 {
54 self::$per_page = !empty($_GET['per_page']) ? absint($_GET['per_page']) : self::$per_page;
55 }
56
57 /**
58 * Get instance
59 *
60 * @since 4.11.0
61 * @return static
62 */
63 public static function get_instance()
64 {
65 if (null === static::$instance) {
66 self::$instance = new static();
67 }
68
69 return self::$instance;
70 }
71
72 /**
73 * Setup
74 *
75 * @since 4.11.0
76 */
77 public function setup()
78 {
79 $this->setup_hooks();
80 }
81
82 /**
83 * Setup Hooks.
84 *
85 * @since 4.11.0
86 */
87 private function setup_hooks()
88 {
89 if (!$this->is_subscriptions_import_page()) {
90 return;
91 }
92
93 // Do not render main import tools page.
94 remove_action('give_admin_field_tools_import', ['Give_Settings_Import', 'render_import_field']);
95
96 // Render subscriptions import page
97 add_action('give_admin_field_tools_import', [$this, 'render_page']);
98
99 // Print the HTML.
100 add_action('give_tools_import_subscriptions_form_start', [$this, 'html'], 10);
101
102 // Handle submit
103 add_action('give-tools_save_import', [$this, 'save']);
104
105 add_action('give-tools_update_notices', [$this, 'update_notices'], 11, 1);
106
107 // Used to add submit button.
108 add_action('give_tools_import_subscriptions_form_end', [$this, 'submit'], 10);
109 }
110
111 /**
112 * Update notice
113 *
114 * @since 4.11.0
115 * @param $messages
116 *
117 * @return mixed
118 */
119 public function update_notices($messages)
120 {
121 if (!empty($_GET['tab']) && 'import' === give_clean($_GET['tab'])) {
122 unset($messages['give-setting-updated']);
123 }
124
125 return $messages;
126 }
127
128 /**
129 * Print submit and nonce button.
130 *
131 * @since 4.11.0
132 */
133 public function submit()
134 {
135 wp_nonce_field('give-save-settings', '_give-save-settings');
136 ?>
137 <input type="hidden" class="import-step" id="import-step" name="step"
138 value="<?php echo esc_attr($this->get_step()); ?>" />
139 <input type="hidden" class="importer-type" value="<?php echo esc_attr($this->importer_type); ?>" />
140 <?php
141 }
142
143 /**
144 * Print the HTML for importer.
145 *
146 * @since 4.11.0
147 */
148 public function html()
149 {
150 $step = $this->get_step();
151
152 // Show progress.
153 $this->render_progress();
154 ?>
155 <section>
156 <table
157 class="widefat export-options-table give-table <?php echo esc_attr("step-{$step}"); ?> <?php echo esc_attr((1 === $step && !empty($this->is_csv_valid) ? 'give-hidden' : '')); ?> "
158 id="<?php echo esc_attr("step-{$step}"); ?>">
159 <tbody>
160 <?php
161 switch ($step) {
162 case 1:
163 $this->render_media_csv();
164 break;
165
166 case 2:
167 $this->render_dropdown();
168 break;
169
170 case 3:
171 $this->start_import();
172 break;
173
174 case 4:
175 $this->import_success();
176 }
177 if (false === $this->check_for_dropdown_or_import()) {
178 ?>
179 <tr valign="top">
180 <th>
181 <input type="submit"
182 class="button button-primary button-large button-secondary <?php echo esc_attr("step-{$step}"); ?>"
183 id="recount-stats-submit"
184 value="
185 <?php
186 echo esc_attr(apply_filters('give_import_subscription_submit_button_text', __('Submit', 'give')));
187 ?>
188 " />
189 </th>
190 <th>
191 <?php
192 do_action('give_import_subscription_submit_button');
193 ?>
194 </th>
195 </tr>
196 <?php
197 }
198 ?>
199 </tbody>
200 </table>
201 </section>
202 <?php
203 }
204
205 /**
206 * Show success notice
207 *
208 * @since 4.11.0
209 */
210 public function import_success()
211 {
212 check_admin_referer('give_subscription_import_success');
213
214 $delete_csv = (!empty($_GET['delete_csv']) ? absint($_GET['delete_csv']) : false);
215 $csv = (!empty($_GET['csv']) ? absint($_GET['csv']) : false);
216 if (!empty($delete_csv) && !empty($csv)) {
217 wp_delete_attachment($csv, true);
218 }
219
220 $report = $this->get_report();
221
222 $total = (int)$_GET['total'];
223 --$total;
224 $success = (bool)$_GET['success'];
225 $dry_run = empty($_GET['dry_run']) ? 0 : absint($_GET['dry_run']);
226 ?>
227 <tr valign="top" class="give-import-dropdown">
228 <th colspan="2">
229 <h2>
230 <?php
231 if ($success) {
232 if ($dry_run) {
233 printf(
234 _n('Dry run import complete! %s row processed', 'Dry run import complete! %s rows processed', $total, 'give'),
235 "<strong>{$total}</strong>"
236 );
237 } else {
238 printf(
239 _n('Import complete! %s row processed', 'Import complete! %s rows processed', $total, 'give'),
240 "<strong>{$total}</strong>"
241 );
242 }
243 } else {
244 printf(
245 _n('Failed to import %s row', 'Failed to import %s rows', $total, 'give'),
246 "<strong>{$total}</strong>"
247 );
248 }
249 ?>
250 </h2>
251
252 <?php
253 $text = __('Import Subscriptions', 'give');
254 $query_arg = [
255 'post_type' => 'give_forms',
256 'page' => 'give-tools',
257 'tab' => 'import',
258 ];
259 if ($success) {
260 if ($dry_run) {
261 $query_arg = [
262 'post_type' => 'give_forms',
263 'page' => 'give-tools',
264 'tab' => 'import',
265 'importer-type' => 'import_subscriptions',
266 ];
267 $text = __('Start Import', 'give');
268 } else {
269 $query_arg = [
270 'post_type' => 'give_forms',
271 'page' => 'give-subscriptions',
272 ];
273 $text = __('View Subscriptions', 'give');
274 }
275 }
276
277 if (!empty($report)) {
278 if (isset($report['create_subscription'])) {
279 echo '<p>' . sprintf(_n('%s subscription created', '%s subscriptions created', (int)$report['create_subscription'], 'give'), (int)$report['create_subscription']) . '</p>';
280 }
281 if (isset($report['failed_subscription'])) {
282 echo '<p>' . sprintf(_n('%s subscription failed', '%s subscriptions failed', (int)$report['failed_subscription'], 'give'), (int)$report['failed_subscription']) . '</p>';
283 }
284 if (!empty($report['failed_subscription_initial_donation'])) {
285 echo '<p>' . sprintf(_n('%s initial donation failed', '%s initial donations failed', (int)$report['failed_subscription_initial_donation'], 'give'), (int)$report['failed_subscription_initial_donation']) . '</p>';
286 }
287 if (!empty($report['errors']) && is_array($report['errors'])) {
288 echo '<div class="notice notice-error" style="margin-top:10px;">';
289 echo '<p><strong>' . esc_html__('Errors', 'give') . ':</strong></p>';
290 echo '<ul style="margin-left:20px;list-style:disc;">';
291 foreach ($report['errors'] as $err) {
292 echo '<li>' . esc_html($err) . '</li>';
293 }
294 echo '</ul>';
295 echo '</div>';
296 }
297 }
298 ?>
299
300 <p>
301 <a class="button button-large button-secondary"
302 href="<?php echo esc_url(add_query_arg($query_arg, admin_url('edit.php'))); ?>"><?php echo esc_html($text); ?></a>
303 </p>
304 </th>
305 </tr>
306 <?php
307 }
308
309 /**
310 * Start Import
311 * @since 4.11.0
312 */
313 public function start_import()
314 {
315 $this->reset_report();
316
317 $csv = absint($_REQUEST['csv']);
318 $delimiter = (!empty($_REQUEST['delimiter']) ? give_clean($_REQUEST['delimiter']) : 'csv');
319 $index_start = 1;
320 $next = true;
321 $total = self::get_csv_total($csv);
322 if (self::$per_page < $total) {
323 $total_ajax = ceil($total / self::$per_page);
324 $index_end = self::$per_page;
325 } else {
326 $total_ajax = 1;
327 $index_end = $total;
328 $next = false;
329 }
330 $current_percentage = 100 / ($total_ajax + 1);
331
332 ?>
333 <tr valign="top" class="give-import-dropdown">
334 <th colspan="2">
335 <h2 id="give-import-title"><?php _e('Importing', 'give'); ?></h2>
336 <p class="give-field-description"><?php _e('Your subscriptions are now being imported...', 'give'); ?></p>
337 </th>
338 </tr>
339
340 <tr valign="top" class="give-import-dropdown">
341 <th colspan="2">
342 <span class="spinner is-active"></span>
343 <div class="give-progress"
344 data-current="1"
345 data-total_ajax="<?php echo esc_attr((int)$total_ajax); ?>"
346 data-start="<?php echo esc_attr((int)$index_start); ?>"
347 data-end="<?php echo esc_attr((int)$index_end); ?>"
348 data-next="<?php echo esc_attr((int)$next); ?>"
349 data-total="<?php echo esc_attr((int)$total); ?>"
350 data-per_page="<?php echo esc_attr((int)self::$per_page); ?>">
351
352 <div style="width: <?php echo esc_attr((float)$current_percentage); ?>%"></div>
353 </div>
354 <input type="hidden" value="3" name="step">
355 <input type="hidden" value='<?php echo esc_attr(maybe_serialize($_REQUEST['mapto'])); ?>' name="mapto" class="mapto">
356 <input type="hidden" value="<?php echo esc_attr((int)$csv); ?>" name="csv" class="csv">
357 <input type="hidden" value="<?php echo esc_attr(isset($_REQUEST['mode']) ? sanitize_text_field((string)$_REQUEST['mode']) : ''); ?>" name="mode" class="mode">
358 <input type="hidden" value="<?php echo esc_attr(isset($_REQUEST['create_user']) ? (int)$_REQUEST['create_user'] : 0); ?>" name="create_user" class="create_user">
359 <input type="hidden" value="<?php echo esc_attr(isset($_REQUEST['delete_csv']) ? (int)$_REQUEST['delete_csv'] : 0); ?>" name="delete_csv" class="delete_csv">
360 <input type="hidden" value="<?php echo esc_attr($delimiter); ?>" name="delimiter">
361 <input type="hidden" value="<?php echo esc_attr(isset($_REQUEST['dry_run']) ? (int)$_REQUEST['dry_run'] : 0); ?>" name="dry_run">
362 <input type="hidden" value='<?php echo esc_attr(maybe_serialize(self::get_importer($csv, 0, $delimiter))); ?>' name="main_key" class="main_key">
363 </th>
364 </tr>
365 <?php
366 }
367
368 /**
369 * Validate required mapped fields
370 * @since 4.11.0
371 */
372 public function check_for_dropdown_or_import()
373 {
374 $return = true;
375 if (isset($_REQUEST['mapto'])) {
376 $mapto = (array)$_REQUEST['mapto'];
377 $required = ['form_id', 'donor_id', 'period', 'frequency', 'amount', 'status'];
378 foreach ($required as $key) {
379 if (false === in_array($key, $mapto)) {
380 Give_Admin_Settings::add_error('give-import-csv-subscriptions', sprintf(__('A column must be mapped to "%s".', 'give'), $key));
381 $return = false;
382 }
383 }
384 } else {
385 $return = false;
386 }
387
388 return $return;
389 }
390
391 /**
392 * Print the Dropdown option for CSV.
393 * @since 4.11.0
394 */
395 public function render_dropdown()
396 {
397 if (!$this->is_nonce_valid()) {
398 Give_Admin_Settings::add_error('give-import-csv', __('Something went wrong.', 'give'));
399 ?>
400 <input type="hidden" name="csv_not_valid" class="csv_not_valid" value="<?php echo esc_attr(give_import_page_url()); ?>" />
401 <?php
402 wp_die();
403 }
404
405 $csv = (int)$_GET['csv'];
406 $delimiter = (!empty($_GET['delimiter']) ? give_clean($_GET['delimiter']) : 'csv');
407
408 if (!$this->is_valid_csv($csv)) {
409 $url = give_import_page_url();
410 ?>
411 <input type="hidden" name="csv_not_valid" class="csv_not_valid" value="<?php echo esc_attr($url); ?>" />
412 <?php
413 } else {
414 ?>
415 <tr valign="top" class="give-import-dropdown">
416 <th colspan="2">
417 <h2 id="give-import-title"><?php _e('Map CSV fields to subscriptions', 'give'); ?></h2>
418
419 <p class="give-import-donation-required-fields-title"><?php _e('Required Fields', 'give'); ?></p>
420
421 <p class="give-field-description"><?php _e('These fields are required for the import to be submitted', 'give'); ?></p>
422
423 <ul class="give-import-subscription-required-fields">
424 <li class="give-import-subscription-required-donorId" title="Please configure all required fields to start the import process.">
425 <span class="give-import-donation-required-text"><?php _e('Form ID', 'give'); ?></span>
426 </li>
427 <li class="give-import-subscription-required-donationFormId" title="Please configure all required fields to start the import process.">
428 <span class="give-import-donation-required-text"><?php _e('Donor ID or Donor Email', 'give'); ?></span>
429 </li>
430 <li class="give-import-subscription-required-period" title="Please configure all required fields to start the import process.">
431 <span class="give-import-donation-required-text"><?php _e('Period', 'give'); ?> (day, week, month, year)</span>
432 </li>
433 <li class="give-import-subscription-required-frequency" title="Please configure all required fields to start the import process.">
434 <span class="give-import-donation-required-text"><?php _e('Frequency', 'give'); ?></span>
435 </li>
436 <li class="give-import-subscription-required-amount" title="Please configure all required fields to start the import process.">
437 <span class="give-import-donation-required-text"><?php _e('Amount (donor facing amount)', 'give'); ?></span>
438 </li>
439 <li class="give-import-subscription-required-status" title="Please configure all required fields to start the import process.">
440 <span class="give-import-donation-required-text"><?php _e('Status', 'give'); ?> (active, expired, cancelled, suspended, paused, pending)</span>
441 </li>
442 </ul>
443
444 <p class="give-field-description"><?php _e('Select fields from your CSV file to map against subscription fields or to ignore during import.', 'give'); ?></p>
445 </th>
446 </tr>
447
448 <tr valign="top" class="give-import-dropdown">
449 <th><b><?php _e('Column name', 'give'); ?></b></th>
450 <th><b><?php _e('Map to field', 'give'); ?></b></th>
451 </tr>
452
453 <?php
454 $selectedOptions = [];
455 $raw_key = $this->get_importer($csv, 0, $delimiter);
456 $mapto = (array)(isset($_REQUEST['mapto']) ? $_REQUEST['mapto'] : []);
457
458 foreach ($raw_key as $index => $value) {
459 ?>
460 <tr valign="middle" class="give-import-option">
461 <th><?php echo esc_html($value); ?></th>
462 <th>
463 <?php $this->get_columns($index, $value, $mapto, $selectedOptions); ?>
464 </th>
465 </tr>
466 <?php
467 }
468 }
469 }
470
471 /**
472 * Determine selected option by heuristics
473 */
474 public function selected($option_value, $value)
475 {
476 $option_value = strtolower($option_value);
477 $value = strtolower($value);
478
479 $selected = '';
480 if (stristr($value, $option_value)) {
481 $selected = 'selected';
482 }
483
484 return $selected;
485 }
486
487 /**
488 * Print the columns from the CSV.
489 */
490 private function get_columns($index, $value = false, $mapto = [], &$selectedOptions = [])
491 {
492 $default = give_import_default_options();
493 $current_mapto = (string)(!empty($mapto[$index]) ? $mapto[$index] : '');
494 ?>
495 <select name="mapto[<?php echo esc_attr($index); ?>]">
496 <?php $this->get_dropdown_option_html($default, $current_mapto, $value, $selectedOptions); ?>
497
498 <optgroup label="<?php _e('Subscriptions', 'give'); ?>">
499 <?php $this->get_dropdown_option_html($this->get_subscription_options(), $current_mapto, $value, $selectedOptions); ?>
500 </optgroup>
501 </select>
502 <?php
503 }
504
505 /**
506 * Print the option html for select in importer
507 */
508 public function get_dropdown_option_html($options, $current_mapto, $value = false, &$selectedOptions = [])
509 {
510 foreach ($options as $option => $option_value) {
511 $ignore = [];
512 if (isset($option_value['ignore']) && is_array($option_value['ignore'])) {
513 $ignore = $option_value['ignore'];
514 unset($option_value['ignore']);
515 }
516
517 $option_value_texts = (array)$option_value;
518 $option_text = $option_value_texts[0];
519
520 $selected = false;
521
522 if ($current_mapto === $option && !in_array($option, $selectedOptions)) {
523 $selected = 'selected';
524 $selectedOptions[] = $option;
525 } else {
526 if (!in_array($value, $ignore) && !in_array($option, $selectedOptions)) {
527 foreach ($option_value_texts as $option_value_text) {
528 $selected = $this->selected($option_value_text, $value);
529 if ($selected) {
530 $selectedOptions[] = $option;
531 break;
532 }
533 }
534 // Extra heuristics: match header to option key by normalized token
535 if (!$selected) {
536 $normalize = static function ($str) {
537 $str = strtolower((string)$str);
538 return preg_replace('/[^a-z0-9]/', '', $str);
539 };
540
541 $valueNorm = $normalize($value);
542 $optionNorm = $normalize($option);
543
544 if ($valueNorm && $optionNorm && $valueNorm === $optionNorm) {
545 $selected = 'selected';
546 $selectedOptions[] = $option;
547 } else {
548 // Try normalized match against visible label too
549 $labelNorm = $normalize($option_text);
550 if ($labelNorm && $valueNorm && $labelNorm === $valueNorm) {
551 $selected = 'selected';
552 $selectedOptions[] = $option;
553 }
554 }
555 }
556 }
557 }
558 ?>
559 <option value="<?php echo esc_attr($option); ?>" <?php echo esc_html($selected); ?>><?php echo esc_html($option_text); ?></option>
560 <?php
561 }
562 }
563
564 /**
565 * Get column count of csv file.
566 */
567 public function get_csv_total($file_id)
568 {
569 $total = false;
570 if ($file_id) {
571 $file_dir = get_attached_file($file_id);
572 if ($file_dir) {
573 $total = $this->get_csv_data_from_file_dir($file_dir);
574 }
575 }
576
577 return $total;
578 }
579
580 /**
581 * Get data from File
582 */
583 public function get_csv_data_from_file_dir($file_dir)
584 {
585 $total = false;
586 if ($file_dir) {
587 $file = new SplFileObject($file_dir, 'r');
588 $file->seek(PHP_INT_MAX);
589 $total = $file->key() + 1;
590 }
591
592 return $total;
593 }
594
595 /**
596 * Read a slice of CSV rows for subscriptions import
597 */
598 public function get_subscription_data_from_csv($file_id, $start, $end, $delimiter = 'csv')
599 {
600 $delimiter = (string)apply_filters('give_import_delimiter_set', $delimiter);
601 $file_dir = give_get_file_data_by_file_id($file_id);
602 return give_get_raw_data_from_file($file_dir, $start, $end, $delimiter);
603 }
604
605 /**
606 * Get the CSV fields title from the CSV.
607 */
608 public function get_importer($file_id, $index = 0, $delimiter = 'csv')
609 {
610 $delimiter = (string)apply_filters('give_import_delimiter_set', $delimiter);
611
612 $raw_data = false;
613 $file_dir = get_attached_file($file_id);
614 if ($file_dir) {
615 if (false !== ($handle = fopen($file_dir, 'r'))) {
616 $raw_data = fgetcsv($handle, $index, $delimiter);
617 if (isset($raw_data[0])) {
618 $raw_data[0] = $this->remove_utf8_bom($raw_data[0]);
619 }
620 }
621 }
622
623 return $raw_data;
624 }
625
626 /**
627 * Remove UTF-8 BOM signature.
628 */
629 public function remove_utf8_bom($string)
630 {
631 if ('efbbbf' === substr(bin2hex($string), 0, 6)) {
632 $string = substr($string, 3);
633 }
634
635 return $string;
636 }
637
638 /**
639 * Render progress steps
640 */
641 public function render_progress()
642 {
643 $step = $this->get_step();
644 ?>
645 <ol class="give-progress-steps">
646 <li class="<?php echo esc_attr(1 === $step ? 'active' : ''); ?>">
647 <?php _e('Upload CSV file', 'give'); ?>
648 </li>
649 <li class="<?php echo esc_attr(2 === $step ? 'active' : ''); ?>">
650 <?php _e('Column mapping', 'give'); ?>
651 </li>
652 <li class="<?php echo esc_attr(3 === $step ? 'active' : ''); ?>">
653 <?php _e('Import', 'give'); ?>
654 </li>
655 <li class="<?php echo esc_attr(4 === $step ? 'active' : ''); ?>">
656 <?php _e('Done!', 'give'); ?>
657 </li>
658 </ol>
659 <?php
660 }
661
662 /**
663 * Will return the import step.
664 */
665 public function get_step()
666 {
667 $step = (int)(isset($_REQUEST['step']) ? give_clean($_REQUEST['step']) : 0);
668 $on_step = 1;
669
670 if (empty($step) || 1 === $step) {
671 $on_step = 1;
672 } elseif ($this->check_for_dropdown_or_import()) {
673 $on_step = 3;
674 } elseif (2 === $step) {
675 $on_step = 2;
676 } elseif (4 === $step) {
677 $on_step = 4;
678 }
679
680 return $on_step;
681 }
682
683 /**
684 * Render subscriptions import page
685 */
686 public function render_page()
687 {
688 include_once GIVE_PLUGIN_DIR . 'includes/admin/tools/views/html-admin-page-import-subscriptions.php';
689 }
690
691 /**
692 * Dry Run checkbox and helper
693 */
694 public function give_import_subscription_submit_button_render_media_csv()
695 {
696 $dry_run = isset($_POST['dry_run']) ? absint($_POST['dry_run']) : 1;
697 ?>
698 <div>
699 <label for="dry_run">
700 <input type="hidden" name="dry_run" value="0" />
701 <input type="checkbox" name="dry_run" id="dry_run" class="dry_run"
702 value="1" <?php checked(1, $dry_run); ?>>
703 <strong><?php _e('Dry Run', 'give'); ?></strong>
704 </label>
705 <p class="give-field-description">
706 <?php _e('Preview what the import would look like without making any changes.', 'give'); ?>
707 </p>
708 </div>
709 <?php
710 }
711
712 /**
713 * Change submit button text on first step
714 */
715 function give_import_subscription_submit_text_render_media_csv($text)
716 {
717 return __('Begin Import', 'give');
718 }
719
720 /**
721 * Add CSV upload HTMl
722 */
723 public function render_media_csv()
724 {
725 add_filter(
726 'give_import_subscription_submit_button_text',
727 [$this, 'give_import_subscription_submit_text_render_media_csv']
728 );
729 add_action(
730 'give_import_subscription_submit_button',
731 [$this, 'give_import_subscription_submit_button_render_media_csv']
732 );
733 ?>
734 <tr valign="top">
735 <th colspan="2">
736 <h2 id="give-import-title"><?php _e('Import subscriptions from a CSV file', 'give'); ?></h2>
737 <p class="give-field-description"><?php _e('This tool allows you to import subscription data via a CSV file.', 'give'); ?></p>
738 </th>
739 </tr>
740 <?php
741 $csv = (isset($_POST['csv']) ? give_clean($_POST['csv']) : '');
742 $csv_id = (isset($_POST['csv_id']) ? give_clean($_POST['csv_id']) : '');
743 $delimiter = (isset($_POST['delimiter']) ? give_clean($_POST['delimiter']) : 'csv');
744 $mode = empty($_POST['mode']) ? 'disabled' : (give_is_setting_enabled(give_clean($_POST['mode'])) ? 'enabled' : 'disabled');
745 $create_user = empty($_POST['create_user']) ? 'disabled' : (give_is_setting_enabled(give_clean($_POST['create_user'])) ? 'enabled' : 'disabled');
746 $delete_csv = empty($_POST['delete_csv']) ? 'enabled' : (give_is_setting_enabled(give_clean($_POST['delete_csv'])) ? 'enabled' : 'disabled');
747
748 if (empty($csv_id) || !$this->is_valid_csv($csv_id, $csv)) {
749 $csv_id = $csv = '';
750 }
751 $per_page = isset($_POST['per_page']) ? absint($_POST['per_page']) : self::$per_page;
752
753 $sample_file_text = sprintf(
754 '%s <a href="%s">%s</a>.',
755 __('Download the sample file', 'give'),
756 esc_url(GIVE_PLUGIN_URL . 'sample-data/sample-subscriptions.csv'),
757 __('here', 'give')
758 );
759
760 $csv_description = sprintf(
761 '%1$s %2$s',
762 __('The file must be a Comma Separated Values (CSV) file type only.', 'give'),
763 $sample_file_text
764 );
765
766 $settings = [
767 [
768 'id' => 'csv',
769 'name' => __('Choose a CSV file:', 'give'),
770 'type' => 'file',
771 'attributes' => [
772 'editing' => 'false',
773 'library' => 'text',
774 ],
775 'description' => $csv_description,
776 'fvalue' => 'url',
777 'default' => $csv,
778 ],
779 [
780 'id' => 'csv_id',
781 'type' => 'hidden',
782 'value' => $csv_id,
783 ],
784 [
785 'id' => 'delimiter',
786 'name' => __('CSV Delimiter:', 'give'),
787 'description' => __('If your CSV uses a different delimiter (like a tab), set that here.', 'give'),
788 'default' => $delimiter,
789 'type' => 'select',
790 'options' => [
791 'csv' => __('Comma', 'give'),
792 'tab-separated-values' => __('Tab', 'give'),
793 ],
794 ],
795 [
796 'id' => 'mode',
797 'name' => __('Test Mode:', 'give'),
798 'description' => __('Select whether these subscriptions should be marked as "test".', 'give'),
799 'default' => $mode,
800 'type' => 'radio_inline',
801 'options' => [
802 'enabled' => __('Enabled', 'give'),
803 'disabled' => __('Disabled', 'give'),
804 ],
805 ],
806 [
807 'id' => 'create_user',
808 'name' => __('Create WP users for new donors:', 'give'),
809 'description' => __('Automatically create a WordPress user account for newly created donors. This is required for donors to access their Donor Dashboard and manage their subscriptions.', 'give'),
810 'default' => $create_user,
811 'type' => 'radio_inline',
812 'options' => [
813 'enabled' => __('Enabled', 'give'),
814 'disabled' => __('Disabled', 'give'),
815 ],
816 ],
817 [
818 'id' => 'delete_csv',
819 'name' => __('Delete CSV after import:', 'give'),
820 'description' => __('Delete the uploaded CSV from the Media Library after import.', 'give'),
821 'default' => $delete_csv,
822 'type' => 'radio_inline',
823 'options' => [
824 'enabled' => __('Enabled', 'give'),
825 'disabled' => __('Disabled', 'give'),
826 ],
827 ],
828 [
829 'id' => 'per_page',
830 'name' => __('Process Rows Per Batch:', 'give'),
831 'type' => 'number',
832 'description' => __('Determine how many rows you would like to import per cycle.', 'give'),
833 'default' => $per_page,
834 'class' => 'give-text-small',
835 ],
836 ];
837
838 $settings = apply_filters('give_import_file_upload_html', $settings);
839
840 if (empty($this->is_csv_valid)) {
841 Give_Admin_Settings::output_fields($settings, 'give_settings');
842 } else {
843 ?>
844 <input type="hidden" name="is_csv_valid" class="is_csv_valid"
845 value="<?php echo esc_attr($this->is_csv_valid); ?>">
846 <?php
847 }
848 }
849
850 /**
851 * Run when user click on the submit button.
852 */
853 public function save()
854 {
855 if (!$this->is_nonce_valid()) {
856 wp_die();
857 }
858
859 $step = $this->get_step();
860
861 if (1 === $step) {
862 $csv_id = absint($_POST['csv_id']);
863
864 if ($this->is_valid_csv($csv_id, esc_url($_POST['csv']))) {
865 $url = give_import_page_url(
866 [
867 'step' => '2',
868 'importer-type' => $this->importer_type,
869 'csv' => $csv_id,
870 'delimiter' => isset($_REQUEST['delimiter']) ? give_clean($_REQUEST['delimiter']) : 'csv',
871 'mode' => empty($_POST['mode']) ? '0' : (give_is_setting_enabled(give_clean($_POST['mode'])) ? '1' : '0'),
872 'create_user' => empty($_POST['create_user']) ? '0' : (give_is_setting_enabled(give_clean($_POST['create_user'])) ? '1' : '0'),
873 'delete_csv' => empty($_POST['delete_csv']) ? '1' : (give_is_setting_enabled(give_clean($_POST['delete_csv'])) ? '1' : '0'),
874 'per_page' => isset($_POST['per_page']) ? absint($_POST['per_page']) : self::$per_page,
875 'dry_run' => isset($_POST['dry_run']) ? absint($_POST['dry_run']) : 0,
876 ]
877 );
878
879 $this->is_csv_valid = wp_nonce_url($url, 'give-save-settings', '_give-save-settings');
880 }
881 }
882 }
883
884 /**
885 * Check if user uploaded csv is valid or not.
886 */
887 private function is_valid_csv($csv = false, $match_url = '')
888 {
889 $is_valid_csv = true;
890
891 if ($csv) {
892 $csv_url = wp_get_attachment_url($csv);
893
894 $delimiter = (!empty($_REQUEST['delimiter']) ? give_clean($_REQUEST['delimiter']) : 'csv');
895
896 if (
897 !$csv_url ||
898 (!empty($match_url) && ($csv_url !== $match_url)) ||
899 (($mime_type = get_post_mime_type($csv)) && !strpos($mime_type, $delimiter))
900 ) {
901 $is_valid_csv = false;
902 Give_Admin_Settings::add_error('give-import-csv', __('Please upload or provide a valid CSV file.', 'give'));
903 }
904 } else {
905 $is_valid_csv = false;
906 Give_Admin_Settings::add_error('give-import-csv', __('Please upload or provide a valid CSV file.', 'give'));
907 }
908
909 return $is_valid_csv;
910 }
911
912 /**
913 * Get if current page import donations page or not
914 */
915 private function is_subscriptions_import_page()
916 {
917 return 'import' === give_get_current_setting_tab() &&
918 isset($_GET['importer-type']) &&
919 $this->importer_type === give_clean($_GET['importer-type']);
920 }
921
922 /**
923 * Nonce validation
924 */
925 private function is_nonce_valid()
926 {
927 return !empty($_REQUEST['_give-save-settings']) && wp_verify_nonce($_REQUEST['_give-save-settings'], 'give-save-settings');
928 }
929
930 /**
931 * Import a single subscription row from CSV
932 *
933 * @param array $raw_key
934 * @param array $row_data
935 * @param array $main_key
936 * @param array $import_setting
937 * @return bool|int|string
938 */
939 public function import_row($raw_key, $row_data, $main_key = [], $import_setting = [])
940 {
941 $report = $this->get_report();
942 $dry_run = isset($import_setting['dry_run']) ? (bool)$import_setting['dry_run'] : false;
943
944 if (
945 empty($row_data) || (is_array($row_data) && 0 === count(array_filter($row_data, function ($v) {
946 return $v !== null && $v !== '';
947 })))
948 ) {
949 return true;
950 }
951
952 if (!is_array($row_data) || count($row_data) !== count($raw_key)) {
953 $report['failed_subscription'] = (!empty($report['failed_subscription']) ? (absint($report['failed_subscription']) + 1) : 1);
954 $this->update_report($report);
955 return false;
956 }
957
958 $data = array_combine($raw_key, $row_data);
959
960 $required = ['form_id', 'period', 'frequency', 'amount', 'status'];
961 foreach ($required as $key) {
962 if (empty($data[$key]) && '0' !== (string)(isset($data[$key]) ? $data[$key] : '')) {
963 $report['failed_subscription'] = (!empty($report['failed_subscription']) ? (absint($report['failed_subscription']) + 1) : 1);
964 $report['errors'][] = sprintf(__('Row %1$d: Missing required field "%2$s"', 'give'), (int)(isset($import_setting['row_key']) ? $import_setting['row_key'] : 0), $key);
965 $this->update_report($report);
966 return 'Missing required field ' . $key;
967 }
968 }
969 if (empty($data['donor_id']) && empty($data['email'])) {
970 $report['failed_subscription'] = (!empty($report['failed_subscription']) ? (absint($report['failed_subscription']) + 1) : 1);
971 $report['errors'][] = sprintf(__('Row %d: Either donor_id or email is required to resolve the donor', 'give'), (int)(isset($import_setting['row_key']) ? $import_setting['row_key'] : 0));
972 $this->update_report($report);
973 return 'Missing donor identifier (donor_id or email)';
974 }
975
976 try {
977 $currency = !empty($data['currency']) && array_key_exists($data['currency'], give_get_currencies_list()) ? $data['currency'] : give_get_currency();
978
979 $attributes = [];
980 $attributes['donationFormId'] = (int)$data['form_id'];
981
982 $resolvedDonorId = 0;
983 if (!empty($data['donor_id'])) {
984 $resolvedDonorId = (int)$data['donor_id'];
985 } else {
986 try {
987 $email = (string)$data['email'];
988 $firstNameCsv = (string)(isset($data['first_name']) ? $data['first_name'] : '');
989 $lastNameCsv = (string)(isset($data['last_name']) ? $data['last_name'] : '');
990 $donorModel = give(\Give\DonationForms\Actions\GetOrCreateDonor::class)(null, $email, $firstNameCsv, $lastNameCsv, null, null);
991 if (!empty($import_setting['create_user']) && (int)$import_setting['create_user'] === 1) {
992 try {
993 $donorModel = give(\Give\Donors\Actions\CreateUserFromDonor::class)($donorModel);
994 } catch (\Throwable $e) {
995 }
996 }
997 $resolvedDonorId = (int)$donorModel->id;
998 } catch (\Throwable $e) {
999 $report['failed_subscription'] = (!empty($report['failed_subscription']) ? (absint($report['failed_subscription']) + 1) : 1);
1000 $this->update_report($report);
1001 return false;
1002 }
1003 }
1004 $attributes['donorId'] = $resolvedDonorId;
1005
1006 $rawPeriod = strtolower(trim((string)$data['period']));
1007 $periodAliases = [
1008 'daily' => 'day',
1009 'days' => 'day',
1010 'day' => 'day',
1011 'weekly' => 'week',
1012 'weeks' => 'week',
1013 'week' => 'week',
1014 'monthly' => 'month',
1015 'months' => 'month',
1016 'month' => 'month',
1017 'quarterly' => 'quarter',
1018 'quarters' => 'quarter',
1019 'qtr' => 'quarter',
1020 'qtrs' => 'quarter',
1021 'quarter' => 'quarter',
1022 'yearly' => 'year',
1023 'annually' => 'year',
1024 'annual' => 'year',
1025 'yrs' => 'year',
1026 'yr' => 'year',
1027 'years' => 'year',
1028 'year' => 'year',
1029 ];
1030 $normalizedPeriod = isset($periodAliases[$rawPeriod]) ? $periodAliases[$rawPeriod] : $rawPeriod;
1031 if (!\Give\Subscriptions\ValueObjects\SubscriptionPeriod::isValid($normalizedPeriod)) {
1032 throw new \UnexpectedValueException(sprintf(
1033 __('Invalid subscription period "%1$s". Valid options: %2$s. You can also use: daily, weekly, monthly, quarterly, yearly.', 'give'),
1034 (string)$data['period'],
1035 implode(', ', array_values(\Give\Subscriptions\ValueObjects\SubscriptionPeriod::toArray()))
1036 ));
1037 }
1038 $attributes['period'] = new \Give\Subscriptions\ValueObjects\SubscriptionPeriod($normalizedPeriod);
1039 $attributes['frequency'] = (int)$data['frequency'];
1040 $attributes['installments'] = isset($data['installments']) ? (int)$data['installments'] : 0;
1041 $attributes['transactionId'] = isset($data['transaction_id']) ? (string)$data['transaction_id'] : '';
1042
1043 if (!empty($data['mode'])) {
1044 $mode = strtolower((string)$data['mode']);
1045 } else {
1046 $mode = (isset($import_setting['mode']) && $import_setting['mode']) ? 'test' : (give_is_test_mode() ? 'test' : 'live');
1047 }
1048 $attributes['mode'] = new \Give\Subscriptions\ValueObjects\SubscriptionMode($mode);
1049
1050 $amountDecimal = is_string($data['amount']) ? preg_replace('/[\$,]/', '', $data['amount']) : $data['amount'];
1051 $attributes['amount'] = \Give\Framework\Support\ValueObjects\Money::fromDecimal($amountDecimal, $currency);
1052
1053 if (isset($data['fee_amount_recovered']) && $data['fee_amount_recovered'] !== '') {
1054 $feeDecimal = is_string($data['fee_amount_recovered']) ? preg_replace('/[\$,]/', '', $data['fee_amount_recovered']) : $data['fee_amount_recovered'];
1055 $attributes['feeAmountRecovered'] = \Give\Framework\Support\ValueObjects\Money::fromDecimal($feeDecimal, $currency);
1056 }
1057
1058 $attributes['status'] = new \Give\Subscriptions\ValueObjects\SubscriptionStatus(strtolower(trim((string)$data['status'])));
1059
1060 if (!empty($data['gateway_id'])) {
1061 $attributes['gatewayId'] = (string)$data['gateway_id'];
1062 }
1063 if (!empty($data['gateway_subscription_id'])) {
1064 $attributes['gatewaySubscriptionId'] = (string)$data['gateway_subscription_id'];
1065 }
1066
1067 if (!empty($data['created_at'])) {
1068 $attributes['createdAt'] = new \DateTime((string)$data['created_at']);
1069 }
1070 if (!empty($data['renews_at'])) {
1071 $attributes['renewsAt'] = new \DateTime((string)$data['renews_at']);
1072 }
1073
1074 if ($dry_run) {
1075 $report['create_subscription'] = (!empty($report['create_subscription']) ? (absint($report['create_subscription']) + 1) : 1);
1076 $this->update_report($report);
1077 return true;
1078 }
1079
1080 $subscription = \Give\Subscriptions\Models\Subscription::create($attributes);
1081
1082 if ($subscription && $subscription->id) {
1083 try {
1084 $donorModel = null;
1085 try {
1086 $donorModel = \Give\Donors\Models\Donor::find($subscription->donorId);
1087 } catch (\Throwable $e) {
1088 $donorModel = null;
1089 }
1090
1091 $donorEmail = ($donorModel && isset($donorModel->email)) ? (string)$donorModel->email : '';
1092 $donorName = ($donorModel && isset($donorModel->name)) ? (string)$donorModel->name : '';
1093 $firstName = '';
1094 $lastName = '';
1095 if ($donorName) {
1096 $parts = preg_split('/\s+/', trim($donorName));
1097 if ($parts) {
1098 $firstName = (string)array_shift($parts);
1099 $lastName = (string)trim(implode(' ', $parts));
1100 }
1101 }
1102
1103 $donationAttributes = [
1104 'subscriptionId' => $subscription->id,
1105 'gatewayId' => !empty($attributes['gatewayId']) ? $attributes['gatewayId'] : 'manual',
1106 'amount' => $subscription->amount,
1107 'status' => \Give\Donations\ValueObjects\DonationStatus::COMPLETE(),
1108 'type' => \Give\Donations\ValueObjects\DonationType::SUBSCRIPTION(),
1109 'donorId' => $subscription->donorId,
1110 'formId' => $subscription->donationFormId,
1111 'feeAmountRecovered' => $subscription->feeAmountRecovered,
1112 'mode' => $subscription->mode->isLive() ? \Give\Donations\ValueObjects\DonationMode::LIVE() : \Give\Donations\ValueObjects\DonationMode::TEST(),
1113 'firstName' => $firstName,
1114 'lastName' => $lastName,
1115 'email' => $donorEmail,
1116 ];
1117
1118 if (!empty($data['first_name'])) {
1119 $donationAttributes['firstName'] = (string)$data['first_name'];
1120 }
1121 if (!empty($data['last_name'])) {
1122 $donationAttributes['lastName'] = (string)$data['last_name'];
1123 }
1124 if (!empty($data['email'])) {
1125 $donationAttributes['email'] = (string)$data['email'];
1126 }
1127
1128 if (!empty($attributes['transactionId'])) {
1129 $donationAttributes['gatewayTransactionId'] = (string)$attributes['transactionId'];
1130 }
1131 if (!empty($subscription->createdAt)) {
1132 $donationAttributes['createdAt'] = $subscription->createdAt;
1133 }
1134
1135 $initialDonation = \Give\Donations\Models\Donation::create($donationAttributes);
1136
1137 if ($initialDonation && $initialDonation->id) {
1138 give()->subscriptions->updateLegacyParentPaymentId($subscription->id, $initialDonation->id);
1139 $this->update_legacy_after_initial_donation($initialDonation);
1140 }
1141 } catch (\Throwable $e) {
1142 $report['failed_subscription_initial_donation'] = (!empty($report['failed_subscription_initial_donation']) ? (absint($report['failed_subscription_initial_donation']) + 1) : 1);
1143 $report['errors'][] = sprintf(__('Row %1$d: Initial donation creation failed (%2$s)', 'give'), (int)(isset($import_setting['row_key']) ? $import_setting['row_key'] : 0), $e->getMessage());
1144 }
1145 $report['create_subscription'] = (!empty($report['create_subscription']) ? (absint($report['create_subscription']) + 1) : 1);
1146 $this->update_report($report);
1147 return (int)$subscription->id;
1148 }
1149
1150 $report['failed_subscription'] = (!empty($report['failed_subscription']) ? (absint($report['failed_subscription']) + 1) : 1);
1151 $this->update_report($report);
1152 return false;
1153 } catch (\Throwable $e) {
1154 $report['failed_subscription'] = (!empty($report['failed_subscription']) ? (absint($report['failed_subscription']) + 1) : 1);
1155 $report['errors'][] = sprintf(__('Row %1$d: %2$s', 'give'), (int)(isset($import_setting['row_key']) ? $import_setting['row_key'] : 0), $e->getMessage());
1156 $this->update_report($report);
1157 return $e->getMessage();
1158 }
1159 }
1160
1161 /**
1162 * Get current import report
1163 * @since 4.11.0
1164 */
1165 public function get_report()
1166 {
1167 return get_option('give_import_subscription_report', []);
1168 }
1169
1170 /**
1171 * Update import report
1172 * @since 4.11.0
1173 */
1174 private function update_report($value = [])
1175 {
1176 update_option('give_import_subscription_report', $value, false);
1177 }
1178
1179 /**
1180 * Reset import report
1181 * @since 4.11.0
1182 */
1183 public function reset_report()
1184 {
1185 update_option('give_import_subscription_report', [], false);
1186 }
1187
1188 /**
1189 * Update legacy donor totals and fee meta for newly created initial donation
1190 * @since 4.11.0
1191 */
1192 private function update_legacy_after_initial_donation(Donation $donation)
1193 {
1194 try {
1195 $donor = $donation->donor;
1196 if ($donor && isset($donor->id)) {
1197 give()->donors->updateLegacyColumns(
1198 $donor->id,
1199 [
1200 'purchase_value' => $this->get_donor_total_intended_amount((int)$donor->id),
1201 'purchase_count' => $donor->totalDonations(),
1202 ]
1203 );
1204 }
1205 if (null !== $donation->feeAmountRecovered) {
1206 give()->payment_meta->update_meta(
1207 $donation->id,
1208 '_give_fee_donation_amount',
1209 give_sanitize_amount_for_db(
1210 $donation->intendedAmount()->formatToDecimal(),
1211 ['currency' => $donation->amount->getCurrency()]
1212 )
1213 );
1214 }
1215 } catch (\Throwable $e) {
1216 }
1217 }
1218
1219 /**
1220 * Calculate donor total intended amount across donations
1221 * @since 4.11.0
1222 */
1223 private function get_donor_total_intended_amount($donorId)
1224 {
1225 return (float)DB::table('posts', 'posts')
1226 ->join(function ($join) {
1227 $join->leftJoin('give_donationmeta', 'donor_meta')
1228 ->on('posts.ID', 'donor_meta.donation_id')
1229 ->andOn('donor_meta.meta_key', DonationMetaKeys::DONOR_ID, true);
1230 })
1231 ->join(function ($join) {
1232 $join->leftJoin('give_donationmeta', 'amount_meta')
1233 ->on('posts.ID', 'amount_meta.donation_id')
1234 ->andOn('amount_meta.meta_key', DonationMetaKeys::AMOUNT, true);
1235 })
1236 ->join(function ($join) {
1237 $join->leftJoin('give_donationmeta', 'fee_meta')
1238 ->on('posts.ID', 'fee_meta.donation_id')
1239 ->andOn('fee_meta.meta_key', DonationMetaKeys::FEE_AMOUNT_RECOVERED, true);
1240 })
1241 ->where('posts.post_type', 'give_payment')
1242 ->where('donor_meta.meta_value', (int)$donorId)
1243 ->whereIn('posts.post_status', ['publish', 'give_subscription'])
1244 ->sum('IFNULL(amount_meta.meta_value, 0) - IFNULL(fee_meta.meta_value, 0)');
1245 }
1246
1247 /**
1248 * Subscription mapping options for CSV column selection
1249 * @since 4.11.0
1250 */
1251 public function get_subscription_options()
1252 {
1253 return (array)apply_filters(
1254 'give_import_subscription_options',
1255 [
1256 'form_id' => [__('Donation Form ID', 'give'), __('Form ID', 'give')],
1257 'donor_id' => [__('Donor ID', 'give')],
1258 'first_name' => [__('Donor First Name', 'give'), __('First Name', 'give')],
1259 'last_name' => [__('Donor Last Name', 'give'), __('Last Name', 'give')],
1260 'email' => [__('Donor Email', 'give'), __('Email', 'give')],
1261 'period' => [__('Period', 'give'), __('Subscription Period', 'give')],
1262 'frequency' => [__('Frequency', 'give')],
1263 'installments' => [__('Installments', 'give')],
1264 'amount' => [__('Amount', 'give'), __('Recurring Amount', 'give')],
1265 'fee_amount_recovered' => [__('Recovered Fee Amount', 'give')],
1266 'status' => [__('Status', 'give')],
1267 'mode' => [__('Mode', 'give'), __('Payment Mode', 'give')],
1268 'transaction_id' => [__('Transaction ID', 'give')],
1269 'gateway_id' => [__('Gateway ID', 'give'), __('Gateway', 'give')],
1270 'gateway_subscription_id' => [__('Gateway Subscription ID', 'give')],
1271 'created_at' => [__('Created At', 'give'), __('Start Date', 'give')],
1272 'renews_at' => [__('Renews At', 'give'), __('Next Renewal Date', 'give')],
1273 'currency' => [__('Currency', 'give')],
1274 ]
1275 );
1276 }
1277 }
1278
1279 Give_Import_Subscriptions::get_instance()->setup();
1280 }
1281