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 / campaign-mapper.php

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

325 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Maps Charitable campaigns onto SureDonation campaigns.
4 *
5 * One SureDonation campaign per selected Charitable campaign post, with
6 * source/import provenance meta for idempotency and rollback, the goal
7 * written into SureDonation's canonical campaign-meta blob, the raw
8 * Charitable settings preserved in a JSON payload, and a default donation
9 * form auto-created (mirrors the GiveWP campaign mapper).
10 *
11 * @package SureDonation
12 * @since 1.5.1
13 */
14
15 namespace SureDonation\Inc\Import\Charitable;
16
17 use SureDonation\Inc\Campaigns\Campaign_Cpt;
18 use SureDonation\Inc\Helper;
19 use SureDonation\Inc\Post_Types\Donation_Form;
20 use SureDonation\Inc\Traits\Get_Instance;
21
22 // Exit if accessed directly.
23 defined( 'ABSPATH' ) || exit;
24
25 /**
26 * Campaign_Mapper class.
27 *
28 * @phpstan-import-type ImportProgress from Session
29 *
30 * @since 1.5.1
31 */
32 class Campaign_Mapper {
33 use Get_Instance;
34
35 /**
36 * Post meta on the SD campaign holding the Charitable campaign post ID.
37 *
38 * @since 1.5.1
39 */
40 public const META_SOURCE_ID = '_suredonation_charitable_source_id';
41
42 /**
43 * Post meta on the SD campaign holding the import session UUID (rollback scope).
44 *
45 * @since 1.5.1
46 */
47 public const META_IMPORT_ID = '_suredonation_charitable_import_id';
48
49 /**
50 * Reverse pointer written onto the Charitable campaign post.
51 *
52 * @since 1.5.1
53 */
54 public const META_CHARITABLE_MARKER = '_suredonation_imported_to_campaign_id';
55
56 /**
57 * Process a batch of Charitable campaigns.
58 *
59 * @param ImportProgress $progress Session progress (passed by reference).
60 * @param int $offset Current offset within this phase.
61 * @return int Number of source rows processed in this batch.
62 * @since 1.5.1
63 */
64 public function process_batch( &$progress, $offset ) {
65 $source = Source::get_instance();
66 $campaign_ids = isset( $progress['options']['campaign_ids'] ) && is_array( $progress['options']['campaign_ids'] )
67 ? $progress['options']['campaign_ids']
68 : [];
69 $campaigns = $source->get_campaigns_batch( (int) $offset, Importer::BATCH_SIZE, $campaign_ids );
70
71 if ( empty( $campaigns ) ) {
72 return 0;
73 }
74
75 foreach ( $campaigns as $campaign ) {
76 $charitable_id = isset( $campaign->ID ) ? (int) $campaign->ID : 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- WP $wpdb->posts row.
77 if ( $charitable_id <= 0 ) {
78 ++$progress['results']['campaigns']['errors'];
79 continue;
80 }
81
82 $existing_id = $this->find_existing_campaign( $charitable_id );
83
84 if ( $existing_id > 0 ) {
85 ++$progress['results']['campaigns']['skipped'];
86 $progress['campaign_map'][ $charitable_id ] = $existing_id;
87 continue;
88 }
89
90 try {
91 $result = $this->insert_campaign( $campaign, $progress );
92 } catch ( \Throwable $t ) {
93 ++$progress['results']['campaigns']['errors'];
94 $progress['results']['campaigns']['error_log'] = $this->append_error_log(
95 $progress['results']['campaigns']['error_log'],
96 $charitable_id,
97 sprintf(
98 /* translators: %s: exception message */
99 __( 'Unhandled exception while importing campaign: %s', 'suredonation' ),
100 $t->getMessage()
101 )
102 );
103 continue;
104 }
105
106 if ( is_wp_error( $result ) ) {
107 ++$progress['results']['campaigns']['errors'];
108 $progress['results']['campaigns']['error_log'] = $this->append_error_log(
109 $progress['results']['campaigns']['error_log'],
110 $charitable_id,
111 $result->get_error_message()
112 );
113 continue;
114 }
115
116 $progress['campaign_map'][ $charitable_id ] = (int) $result;
117 ++$progress['results']['campaigns']['imported'];
118 }
119
120 return count( $campaigns );
121 }
122
123 /**
124 * Find an existing SureDonation campaign for a given Charitable campaign ID.
125 *
126 * @param int $charitable_id Charitable campaign post ID.
127 * @return int Campaign ID or 0 if none.
128 * @since 1.5.1
129 */
130 private function find_existing_campaign( $charitable_id ) {
131 global $wpdb;
132
133 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Migration scope, one-shot lookup per campaign.
134 $post_id = $wpdb->get_var(
135 $wpdb->prepare(
136 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s LIMIT 1",
137 self::META_SOURCE_ID,
138 (string) (int) $charitable_id
139 )
140 );
141
142 return is_numeric( $post_id ) ? (int) $post_id : 0;
143 }
144
145 /**
146 * Insert a single SureDonation campaign from a Charitable campaign row.
147 *
148 * Migrates title, description, author, dates, and the fundraising goal;
149 * preserves the remaining Charitable settings (suggested amounts, end
150 * date, minimum amount, button text, display toggles) in a JSON payload.
151 * Draft Charitable campaigns import as published/active — same deliberate
152 * decision as the GiveWP importer.
153 *
154 * @param object $campaign Charitable wp_posts row.
155 * @param array<string, mixed> $progress Current progress (used for the import_id).
156 * @return int|\WP_Error New campaign ID on success.
157 * @since 1.5.1
158 */
159 private function insert_campaign( $campaign, $progress ) {
160 $source = Source::get_instance();
161
162 $charitable_id = isset( $campaign->ID ) ? (int) $campaign->ID : 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- WP $wpdb->posts row.
163 $post_title = isset( $campaign->post_title ) ? (string) $campaign->post_title : '';
164 if ( '' === trim( $post_title ) ) {
165 /* translators: %d: Charitable campaign ID. */
166 $post_title = sprintf( __( 'Imported campaign %d', 'suredonation' ), $charitable_id );
167 }
168
169 $meta = $source->get_campaign_meta( $charitable_id );
170
171 $description = isset( $meta['_campaign_description'] ) ? (string) $meta['_campaign_description'] : '';
172 if ( '' === trim( wp_strip_all_tags( $description ) ) ) {
173 $description = isset( $campaign->post_content ) ? (string) $campaign->post_content : '';
174 }
175 $post_excerpt = wp_kses_post( wp_strip_all_tags( $description ) );
176
177 $campaign_id = wp_insert_post(
178 [
179 'post_type' => Campaign_Cpt::POST_TYPE,
180 'post_status' => 'publish',
181 'post_title' => sanitize_text_field( $post_title ),
182 'post_content' => '',
183 'post_excerpt' => $post_excerpt,
184 'post_author' => isset( $campaign->post_author ) ? (int) $campaign->post_author : 0,
185 'post_date' => isset( $campaign->post_date ) ? (string) $campaign->post_date : '',
186 'post_date_gmt' => isset( $campaign->post_date_gmt ) ? (string) $campaign->post_date_gmt : '',
187 ],
188 true
189 );
190
191 if ( is_wp_error( $campaign_id ) ) {
192 return $campaign_id;
193 }
194
195 update_post_meta( $campaign_id, self::META_SOURCE_ID, $charitable_id );
196 if ( ! empty( $progress['import_id'] ) ) {
197 update_post_meta( $campaign_id, self::META_IMPORT_ID, is_scalar( $progress['import_id'] ) ? (string) $progress['import_id'] : '' );
198 }
199
200 // Reverse pointer so the Charitable side records where it went.
201 update_post_meta( $charitable_id, self::META_CHARITABLE_MARKER, $campaign_id );
202
203 // Write to SureDonation's canonical _suredonation_campaign_meta blob —
204 // the key the All Campaigns UI reads. Charitable goals are always
205 // amount-based, so goal_type is fixed to raised_amount.
206 $goal_amount = isset( $meta['_campaign_goal'] ) && is_numeric( $meta['_campaign_goal'] ) ? (float) $meta['_campaign_goal'] : 0;
207 Helper::update_campaign_meta(
208 $campaign_id,
209 [
210 'goal_amount' => $goal_amount,
211 'goal_type' => 'raised_amount',
212 'campaign_status' => 'active',
213 ]
214 );
215
216 // Preserve the migration-relevant Charitable settings in JSON meta so
217 // the admin UI can surface them later (no first-class SD columns yet).
218 $payload = $this->build_raw_payload( $meta );
219 if ( ! empty( $payload ) ) {
220 update_post_meta( $campaign_id, '_suredonation_charitable_campaign', wp_json_encode( $payload ) );
221 }
222
223 // Default form auto-creation — having no form is worse than a basic
224 // SureDonation template the admin can edit. The template doesn't carry
225 // Charitable's suggested amounts / fields (that structure is preserved
226 // in the raw payload above for a future enhancement).
227 if ( ! Campaign_Cpt::get_default_form_id( $campaign_id ) ) {
228 $new_form_id = Donation_Form::create_default_form_for_campaign( $campaign_id, $post_title );
229 if ( $new_form_id ) {
230 update_post_meta( $campaign_id, Campaign_Cpt::META_DEFAULT_FORM_ID, $new_form_id );
231 }
232 }
233
234 return $campaign_id;
235 }
236
237 /**
238 * Build the preserved raw payload from a Charitable campaign's meta.
239 *
240 * @param array<string, string> $meta Flat campaign meta map.
241 * @return array<string, mixed>
242 * @since 1.5.1
243 */
244 private function build_raw_payload( $meta ) {
245 $payload = [];
246
247 // End date — '0'/empty means the campaign never ends.
248 if ( ! empty( $meta['_campaign_end_date'] ) && '0' !== (string) $meta['_campaign_end_date'] ) {
249 $payload['end_date'] = sanitize_text_field( (string) $meta['_campaign_end_date'] );
250 }
251
252 // Suggested donation amounts — a serialized list of amount/description pairs.
253 if ( ! empty( $meta['_campaign_suggested_donations'] ) ) {
254 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, WordPress.PHP.NoSilencedErrors.Discouraged -- hardened native unserialize with class instantiation disabled.
255 $suggested = @unserialize( (string) $meta['_campaign_suggested_donations'], [ 'allowed_classes' => false ] );
256 if ( is_array( $suggested ) ) {
257 $clean = [];
258 foreach ( $suggested as $entry ) {
259 if ( ! is_array( $entry ) || ! isset( $entry['amount'] ) || ! is_numeric( $entry['amount'] ) ) {
260 continue;
261 }
262 $clean[] = [
263 'amount' => (float) $entry['amount'],
264 'description' => isset( $entry['description'] ) ? sanitize_text_field( (string) $entry['description'] ) : '',
265 ];
266 }
267 if ( ! empty( $clean ) ) {
268 $payload['suggested_donations'] = $clean;
269 }
270 }
271 }
272
273 if ( isset( $meta['_campaign_suggested_donations_default'] ) && is_numeric( $meta['_campaign_suggested_donations_default'] ) ) {
274 $payload['suggested_donations_default'] = (float) $meta['_campaign_suggested_donations_default'];
275 }
276
277 if ( isset( $meta['_campaign_allow_custom_donations'] ) ) {
278 $payload['allow_custom_donations'] = ! empty( $meta['_campaign_allow_custom_donations'] );
279 }
280
281 // NOTE: the stored key is _campaign_minimum_donation_amount — the
282 // Charitable class getter reads a different (never-written) key.
283 if ( isset( $meta['_campaign_minimum_donation_amount'] ) && is_numeric( $meta['_campaign_minimum_donation_amount'] ) ) {
284 $payload['minimum_donation_amount'] = (float) $meta['_campaign_minimum_donation_amount'];
285 }
286
287 if ( ! empty( $meta['_campaign_donate_button_text'] ) ) {
288 $payload['donate_button_text'] = sanitize_text_field( (string) $meta['_campaign_donate_button_text'] );
289 }
290
291 foreach ( $meta as $key => $value ) {
292 if ( 0 === strpos( (string) $key, '_campaign_hide_' ) ) {
293 $payload[ sanitize_key( substr( (string) $key, 10 ) ) ] = ! empty( $value );
294 }
295 }
296
297 $payload['has_settings_v2'] = ! empty( $meta['campaign_settings_v2'] );
298
299 return $payload;
300 }
301
302 /**
303 * Append an error entry, capped so a pathological source can't bloat the session.
304 *
305 * @param array<int, array<string, mixed>> $error_log Existing log.
306 * @param int $source_id Source row ID.
307 * @param string $message Error message.
308 * @return array<int, array<string, mixed>>
309 * @since 1.5.1
310 */
311 private function append_error_log( $error_log, $source_id, $message ) {
312 $error_log = is_array( $error_log ) ? $error_log : [];
313 if ( count( $error_log ) >= 50 ) {
314 return $error_log;
315 }
316
317 $error_log[] = [
318 'source_id' => (int) $source_id,
319 'message' => sanitize_text_field( $message ),
320 ];
321
322 return $error_log;
323 }
324 }
325