PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.0.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.0.0
1.6.1 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 / donation-mapper.php

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

387 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Donation mapper for the GiveWP migration tool.
4 *
5 * Imports GiveWP donations (give_payment posts) into the
6 * suredonation_donations table. Skips rows already imported (matched by
7 * import_source_id + import_source pair), resolves the donor via Donor_Mapper, the campaign
8 * via campaign_map populated by Campaign_Mapper, and translates gateway
9 * slug + status via Status_Map.
10 *
11 * Email suppression is engaged for the duration of the batch via
12 * Email_Suppressor so receipts/admin notifications are not blasted out
13 * to donors when their historical records are inserted.
14 *
15 * @package SureDonation
16 */
17
18 namespace SureDonation\Inc\Import\Givewp;
19
20 use SureDonation\Inc\Database\Tables\Donations;
21 use SureDonation\Inc\Database\Tables\Donors;
22 use SureDonation\Inc\Traits\Get_Instance;
23
24 // Exit if accessed directly.
25 defined( 'ABSPATH' ) || exit;
26
27 /**
28 * Donation_Mapper class.
29 *
30 * @since 1.0.0
31 */
32 class Donation_Mapper {
33 use Get_Instance;
34
35 /**
36 * GiveWP payment meta keys that we map to dedicated columns.
37 * Everything else gets preserved into donation_data.givewp.meta as
38 * a raw key/value blob for later inspection.
39 */
40 const STANDARD_META_KEYS = [
41 '_give_payment_total',
42 '_give_payment_form_id',
43 '_give_payment_form_title',
44 '_give_payment_donor_id',
45 '_give_payment_donor_email',
46 '_give_payment_user_id',
47 '_give_payment_currency',
48 '_give_payment_gateway',
49 '_give_payment_mode',
50 '_give_payment_transaction_id',
51 '_give_donor_billing_first_name',
52 '_give_donor_billing_last_name',
53 '_give_donor_billing_address1',
54 '_give_donor_billing_address2',
55 '_give_donor_billing_city',
56 '_give_donor_billing_state',
57 '_give_donor_billing_zip',
58 '_give_donor_billing_country',
59 '_give_payment_customer_id',
60 ];
61
62 /**
63 * Process a batch of GiveWP payments.
64 *
65 * @param array $progress Session progress (passed by reference).
66 * @param int $offset Current offset within this phase.
67 * @return int Number of source rows processed in this batch.
68 * @since 1.0.0
69 */
70 public function process_batch( &$progress, $offset ) {
71 $source = Source::get_instance();
72 $form_ids = isset( $progress['options']['campaign_ids'] ) && is_array( $progress['options']['campaign_ids'] )
73 ? $progress['options']['campaign_ids']
74 : [];
75 $payments = $source->get_payments_batch( (int) $offset, Importer::BATCH_SIZE, $form_ids );
76
77 if ( empty( $payments ) ) {
78 return 0;
79 }
80
81 $suppressor = Email_Suppressor::get_instance();
82 $suppressor->activate();
83
84 try {
85 foreach ( $payments as $payment ) {
86 $payment_id_for_log = isset( $payment->ID ) ? (int) $payment->ID : 0;
87 try {
88 $this->process_one( $payment, $progress );
89 } catch ( \Throwable $t ) {
90 // One bad record can't kill the batch — log and move
91 // on so the rest of the migration runs to completion.
92 $this->log_error(
93 $progress,
94 $payment_id_for_log,
95 sprintf(
96 /* translators: %s: exception message */
97 __( 'Unhandled exception while processing donation: %s', 'suredonation' ),
98 $t->getMessage()
99 )
100 );
101 }
102 }
103 } finally {
104 $suppressor->deactivate();
105 }
106
107 return count( $payments );
108 }
109
110 /**
111 * Map a single GiveWP payment into a SureDonation donations row.
112 *
113 * @param object $payment GiveWP wp_posts row.
114 * @param array $progress Session progress (passed by reference).
115 * @return void
116 * @since 1.0.0
117 */
118 private function process_one( $payment, &$progress ) {
119 $give_payment_id = isset( $payment->ID ) ? (int) $payment->ID : 0;
120 if ( $give_payment_id <= 0 ) {
121 ++$progress['results']['donations']['errors'];
122 return;
123 }
124
125 // Duplicate detection via the import_source_id + import_source pair.
126 if ( $this->already_imported( $give_payment_id ) ) {
127 ++$progress['results']['donations']['skipped'];
128 return;
129 }
130
131 $source = Source::get_instance();
132 $meta = $source->get_payment_meta( $give_payment_id );
133 $email = isset( $meta['_give_payment_donor_email'] ) ? sanitize_email( $meta['_give_payment_donor_email'] ) : '';
134 $amount = $source->extract_donation_amount( $meta );
135
136 if ( '' === $email ) {
137 $this->log_error(
138 $progress,
139 $give_payment_id,
140 sprintf(
141 /* translators: %s: raw value from GiveWP payment meta */
142 __( 'Missing donor email (got "%s").', 'suredonation' ),
143 isset( $meta['_give_payment_donor_email'] ) ? (string) $meta['_give_payment_donor_email'] : ''
144 )
145 );
146 return;
147 }
148
149 if ( $amount <= 0 ) {
150 $this->log_error(
151 $progress,
152 $give_payment_id,
153 sprintf(
154 /* translators: %s: raw value from GiveWP payment meta */
155 __( 'Donation amount is zero or missing (raw _give_payment_total "%s").', 'suredonation' ),
156 isset( $meta['_give_payment_total'] ) ? (string) $meta['_give_payment_total'] : ''
157 )
158 );
159 return;
160 }
161
162 // Gateway/status translation.
163 $give_gateway = isset( $meta['_give_payment_gateway'] ) ? (string) $meta['_give_payment_gateway'] : '';
164 $gateway = Status_Map::map_gateway( $give_gateway );
165 $payment_status = Status_Map::map_donation_status( isset( $payment->post_status ) ? (string) $payment->post_status : '' );
166
167 // Track the per-gateway breakdown for results.
168 $progress['results']['donations']['gateway_breakdown'][ $gateway ] = isset( $progress['results']['donations']['gateway_breakdown'][ $gateway ] )
169 ? (int) $progress['results']['donations']['gateway_breakdown'][ $gateway ] + 1
170 : 1;
171
172 $donor_id = Donor_Mapper::get_instance()->get_or_create_for_payment( $meta, $progress );
173 if ( $donor_id <= 0 ) {
174 $this->log_error( $progress, $give_payment_id, __( 'Failed to resolve donor.', 'suredonation' ) );
175 return;
176 }
177
178 $give_form_id = isset( $meta['_give_payment_form_id'] ) ? absint( $meta['_give_payment_form_id'] ) : 0;
179 $campaign_id = 0;
180 if ( $give_form_id > 0 && isset( $progress['campaign_map'][ $give_form_id ] ) ) {
181 $campaign_id = (int) $progress['campaign_map'][ $give_form_id ];
182 }
183
184 $currency = isset( $meta['_give_payment_currency'] ) ? sanitize_text_field( $meta['_give_payment_currency'] ) : 'USD';
185 $payment_mode = isset( $meta['_give_payment_mode'] ) ? sanitize_text_field( $meta['_give_payment_mode'] ) : 'live';
186 $transaction_id = isset( $meta['_give_payment_transaction_id'] ) ? sanitize_text_field( $meta['_give_payment_transaction_id'] ) : '';
187 $customer_id = isset( $meta['_give_payment_customer_id'] ) ? sanitize_text_field( $meta['_give_payment_customer_id'] ) : '';
188
189 $first_name = isset( $meta['_give_donor_billing_first_name'] ) ? sanitize_text_field( $meta['_give_donor_billing_first_name'] ) : '';
190 $last_name = isset( $meta['_give_donor_billing_last_name'] ) ? sanitize_text_field( $meta['_give_donor_billing_last_name'] ) : '';
191 $donor_name = trim( $first_name . ' ' . $last_name );
192
193 $donation_data = [
194 'givewp' => [
195 'source_id' => $give_payment_id,
196 'import_id' => isset( $progress['import_id'] ) ? (string) $progress['import_id'] : '',
197 'form_id' => $give_form_id,
198 'form_title' => isset( $meta['_give_payment_form_title'] ) ? sanitize_text_field( $meta['_give_payment_form_title'] ) : '',
199 'gateway_raw' => $give_gateway,
200 'gateway_live' => Status_Map::is_gateway_live( $gateway ),
201 'meta' => $this->extract_extra_meta( $meta ),
202 ],
203 ];
204
205 $data = [
206 'campaign_id' => $campaign_id,
207 'donor_id' => $donor_id,
208 'form_id' => 0,
209 'amount' => (string) $amount,
210 'currency' => '' !== $currency ? $currency : 'USD',
211 'transaction_id' => $transaction_id,
212 'customer_id' => $customer_id,
213 'gateway' => $gateway,
214 'payment_status' => $payment_status,
215 'payment_mode' => 'test' === $payment_mode ? 'test' : 'live',
216 'donor_name' => $donor_name,
217 'donor_email' => $email,
218 'donation_type' => 'one-time',
219 'donation_data' => $donation_data,
220 'created_at' => isset( $payment->post_date_gmt ) ? (string) $payment->post_date_gmt : current_time( 'mysql', true ),
221 'import_source_id' => $give_payment_id,
222 'import_source' => 'givewp',
223 ];
224
225 $donation_id = Donations::add( $data );
226 if ( ! $donation_id ) {
227 global $wpdb;
228 $db_error = $wpdb->last_error ? (string) $wpdb->last_error : __( 'unknown DB error', 'suredonation' );
229 $this->log_error(
230 $progress,
231 $give_payment_id,
232 sprintf(
233 /* translators: 1: gateway slug, 2: amount, 3: DB error message */
234 __( 'Failed to insert donation row (gateway=%1$s, amount=%2$s): %3$s', 'suredonation' ),
235 $gateway,
236 (string) $amount,
237 $db_error
238 )
239 );
240 return;
241 }
242
243 // Update donor aggregates (count, total, largest, first/last date)
244 // using the actual payment date — Donors::record_donation() uses
245 // current_time() for last_donation_date, which would falsely stamp
246 // every imported donor with the import timestamp.
247 $this->update_donor_aggregates( $donor_id, $amount, (string) $data['created_at'], $payment_status );
248
249 ++$progress['results']['donations']['imported'];
250 }
251
252 /**
253 * Update an imported donor's aggregate columns after a donation insert.
254 *
255 * Mirrors Donors::record_donation() but takes the actual donation date
256 * rather than stamping current_time(), so donors imported with
257 * historical payments get the correct first/last contribution
258 * timestamps. Numeric aggregates (donation_count, total_donated,
259 * largest_donation) only accumulate for revenue-bearing statuses to
260 * match how SureDonation reports them in the dashboard.
261 *
262 * @param int $donor_id SureDonation donor row ID.
263 * @param float $amount Donation amount.
264 * @param string $donation_date ISO/MySQL datetime of the donation (GMT).
265 * @param string $payment_status SureDonation payment status enum value.
266 * @return void
267 * @since 1.0.0
268 */
269 private function update_donor_aggregates( $donor_id, $amount, $donation_date, $payment_status ) {
270 $donor = Donors::get( (int) $donor_id );
271 if ( ! is_array( $donor ) ) {
272 return;
273 }
274
275 $updates = [];
276
277 // first/last date track ALL imported payments regardless of
278 // status — cancelled or failed payments are still real
279 // historical engagement worth surfacing in the donor profile.
280 $current_first = ! empty( $donor['first_donation_date'] ) ? (string) $donor['first_donation_date'] : '';
281 $current_last = ! empty( $donor['last_donation_date'] ) ? (string) $donor['last_donation_date'] : '';
282
283 if ( '' !== $donation_date ) {
284 if ( '' === $current_first || strtotime( $donation_date ) < strtotime( $current_first ) ) {
285 $updates['first_donation_date'] = $donation_date;
286 }
287 if ( '' === $current_last || strtotime( $donation_date ) > strtotime( $current_last ) ) {
288 $updates['last_donation_date'] = $donation_date;
289 }
290 }
291
292 // Revenue-bearing counters: only completed / partially_refunded
293 // contribute (consistent with Donations::get_dashboard_stats and
294 // the campaign stats query).
295 if ( in_array( $payment_status, [ 'completed', 'partially_refunded' ], true ) ) {
296 $current_total = isset( $donor['total_donated'] ) && is_numeric( $donor['total_donated'] ) ? (float) $donor['total_donated'] : 0.0;
297 $current_count = isset( $donor['donation_count'] ) && is_numeric( $donor['donation_count'] ) ? (int) $donor['donation_count'] : 0;
298 $current_largest = isset( $donor['largest_donation'] ) && is_numeric( $donor['largest_donation'] ) ? (float) $donor['largest_donation'] : 0.0;
299
300 $updates['total_donated'] = $current_total + (float) $amount;
301 $updates['donation_count'] = $current_count + 1;
302 if ( (float) $amount > $current_largest ) {
303 $updates['largest_donation'] = (float) $amount;
304 }
305 }
306
307 if ( ! empty( $updates ) ) {
308 Donors::update( (int) $donor_id, $updates );
309 }
310 }
311
312 /**
313 * Check if a GiveWP payment has already been imported in any prior session.
314 *
315 * @param int $give_payment_id GiveWP payment ID.
316 * @return bool
317 * @since 1.0.0
318 */
319 private function already_imported( $give_payment_id ) {
320 global $wpdb;
321 $table = $wpdb->prefix . 'suredonation_donations';
322
323 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Migration scope, one row lookup.
324 $existing = $wpdb->get_var(
325 $wpdb->prepare(
326 'SELECT id FROM %i WHERE import_source_id = %d AND import_source = "givewp" LIMIT 1',
327 $table,
328 absint( $give_payment_id )
329 )
330 );
331
332 return is_numeric( $existing ) && (int) $existing > 0;
333 }
334
335 /**
336 * Extract non-standard meta keys for preservation in donation_data.givewp.meta.
337 *
338 * Any GiveWP add-on meta not represented by a dedicated SureDonation
339 * column is preserved here as a raw key/value blob so no data is lost.
340 *
341 * @param array $meta Flat assoc of GiveWP payment meta.
342 * @return array<string,string>
343 * @since 1.0.0
344 */
345 private function extract_extra_meta( $meta ) {
346 if ( ! is_array( $meta ) ) {
347 return [];
348 }
349
350 $extra = [];
351 foreach ( $meta as $key => $value ) {
352 if ( in_array( $key, self::STANDARD_META_KEYS, true ) ) {
353 continue;
354 }
355 // Skip empty and obviously-irrelevant keys.
356 if ( '' === $value || null === $value ) {
357 continue;
358 }
359 $extra[ sanitize_key( $key ) ] = is_scalar( $value ) ? (string) $value : '';
360 }
361 return $extra;
362 }
363
364 /**
365 * Append an error entry to the donations error log, capping at 50.
366 *
367 * @param array $progress Progress (passed by reference).
368 * @param int $source_id GiveWP payment ID.
369 * @param string $message Error message.
370 * @return void
371 * @since 1.0.0
372 */
373 private function log_error( &$progress, $source_id, $message ) {
374 ++$progress['results']['donations']['errors'];
375 if ( ! isset( $progress['results']['donations']['error_log'] ) || ! is_array( $progress['results']['donations']['error_log'] ) ) {
376 $progress['results']['donations']['error_log'] = [];
377 }
378 $progress['results']['donations']['error_log'][] = [
379 'source_id' => (int) $source_id,
380 'message' => (string) $message,
381 ];
382 if ( count( $progress['results']['donations']['error_log'] ) > 50 ) {
383 $progress['results']['donations']['error_log'] = array_slice( $progress['results']['donations']['error_log'], -50 );
384 }
385 }
386 }
387