PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.1.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.1.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
← All changes | inc/api/donations-api.php +24 -115 1.5.1 → 1.1.0 View file →
@@ -121,20 +121,10 @@
121 121 },
122 122 ],
123 123 'status' => [
124 124 'required' => true,
125 - 'type' => 'string',
126 - // Sourced from the table's own whitelist rather than
127 - // restated: the two lists had already drifted — suspicious
128 - // is written on an amount mismatch and was missing here.
129 - 'enum' => Donations::get_valid_statuses(),
130 125 'sanitize_callback' => 'sanitize_text_field',
131 - // Required for the enum to be enforced at all. An arg with
132 - // a sanitize_callback and no validate_callback has its enum
133 - // silently skipped (see #340), so this endpoint answered
134 - // "updated successfully" to a status it had refused to
135 - // write.
136 - 'validate_callback' => 'rest_validate_request_arg',
126 + 'enum' => [ 'pending', 'processing', 'completed', 'failed', 'refunded', 'partially_refunded', 'cancelled' ],
137 127 ],
138 128 ],
139 129 ],
140 130
@@ -160,12 +150,10 @@
160 150 'permission_callback' => [ $this, 'check_permissions' ],
161 151 'args' => [
162 152 'action' => [
163 153 'required' => true,
164 - 'type' => 'string',
154 + 'sanitize_callback' => 'sanitize_text_field',
165 155 'enum' => [ 'delete', 'update_status' ],
166 - 'sanitize_callback' => 'sanitize_text_field',
167 - 'validate_callback' => 'rest_validate_request_arg',
168 156 ],
169 157 'ids' => [
170 158 'required' => true,
171 159 'validate_callback' => static function ( $param ) {
@@ -172,12 +160,10 @@
172 160 return is_array( $param ) && ! empty( $param );
173 161 },
174 162 ],
175 163 'status' => [
176 - 'type' => 'string',
177 - 'enum' => Donations::get_valid_statuses(),
178 164 'sanitize_callback' => 'sanitize_text_field',
179 - 'validate_callback' => 'rest_validate_request_arg',
165 + 'enum' => [ 'pending', 'processing', 'completed', 'failed', 'refunded', 'partially_refunded', 'cancelled' ],
180 166 ],
181 167 ],
182 168 ],
183 169
@@ -202,16 +188,10 @@
202 188 'sanitize_callback' => 'absint',
203 189 ],
204 190 'refund_type' => [
205 191 'required' => true,
206 - 'type' => 'string',
192 + 'sanitize_callback' => 'sanitize_text_field',
207 193 'enum' => [ 'full', 'partial' ],
208 - 'sanitize_callback' => 'sanitize_text_field',
209 - // The last arg in this file carrying the #340 shape: an
210 - // enum that reads as enforced and is not. rest_validate_
211 - // request_arg() reads the schema's type, so the type above
212 - // is not decoration.
213 - 'validate_callback' => 'rest_validate_request_arg',
214 194 ],
215 195 'refund_notes' => [
216 196 'sanitize_callback' => 'sanitize_textarea_field',
217 197 ],
@@ -342,12 +322,10 @@
342 322 * @return WP_REST_Response|WP_Error Response object.
343 323 * @since 0.0.1
344 324 */
345 325 public function get_donations( $request ) {
346 - $page = $request->get_param( 'page' ) ?? 1;
347 - // Clamp to a minimum of 1 so the total_pages calculation below can never
348 - // divide by zero (per_page=0 would otherwise trigger a DivisionByZeroError).
349 - $per_page = max( 1, absint( $request->get_param( 'per_page' ) ?? 20 ) );
326 + $page = $request->get_param( 'page' ) ?? 1;
327 + $per_page = $request->get_param( 'per_page' ) ?? 20;
350 328 $search = $request->get_param( 'search' ) ?? '';
351 329 $status = $request->get_param( 'status' ) ?? 'all';
352 330 $campaign = $request->get_param( 'campaign' ) ?? '';
353 331 $donor = $request->get_param( 'donor' ) ?? '';
@@ -375,9 +353,9 @@
375 353 strtoupper( $order ) // using whitelist validation in the method.
376 354 );
377 355
378 356 // Get total count.
379 - $total = Donations::count_admin_list( $status, ! empty( $campaign ) ? absint( $campaign ) : 0, sanitize_text_field( $search ) );
357 + $total = Donations::get_total_donations_by_status( $status, ! empty( $campaign ) ? absint( $campaign ) : 0 );
380 358 }
381 359
382 360 // Format donations data.
383 361 $donations = [];
@@ -606,39 +584,14 @@
606 584 );
607 585 }
608 586
609 587 $old_status = $donation['payment_status'] ?? 'pending';
610 - $updated = Donations::update_status( $donation_id, $status );
588 + Donations::update_status( $donation_id, $status );
611 589
612 - // Strictly false, which is update_status() refusing the value. A 0 is
613 - // $wpdb->update() reporting that no row changed, which cannot mean "no
614 - // such row" here because the 404 above already proved it exists, and
615 - // cannot mean "same status" either because update() always writes
616 - // updated_at. Treating both as success is how a refused write looked
617 - // like a successful one to every client.
618 - //
619 - // Note for anyone comparing this with bulk_action(): that path has no
620 - // existence check, so a 0 there does mean "no such row" and is
621 - // correctly counted as a failure. The two are not in conflict.
622 - if ( false === $updated ) {
623 - return new WP_Error(
624 - 'donation_status_not_updated',
625 - __( 'The donation status could not be updated.', 'suredonation' ),
626 - [ 'status' => 500 ]
627 - );
628 - }
629 -
630 590 // If status changed to completed, update donor stats.
631 - //
632 - // Guarded, not plain: an admin completing a still-pending donation here
633 - // does not stop the gateway webhook arriving for the same row later
634 - // (Stripe retries for days), and the webhook's donor block has no
635 - // "still pending" check of its own. Without a marker written here, that
636 - // webhook would record the same donation a second time and double the
637 - // donor's total, count and largest gift.
638 591 if ( 'completed' !== $old_status && 'completed' === $status ) {
639 592 if ( ! empty( $donation['donor_id'] ) ) {
640 - Donors::record_donation_once( $donation['donor_id'], floatval( $donation['amount'] ), $donation_id );
593 + Donors::record_donation( $donation['donor_id'], floatval( $donation['amount'] ) );
641 594 }
642 595 }
643 596
644 597 return new WP_REST_Response(
@@ -689,26 +642,8 @@
689 642 public function bulk_action( $request ) {
690 643 $action = $request->get_param( 'action' );
691 644 $ids = $request->get_param( 'ids' );
692 645
693 - if ( ! is_array( $ids ) ) {
694 - $ids = [];
695 - }
696 -
697 - // Cap bulk operations at 200 IDs per request. Each ID triggers a
698 - // per-row SELECT + DELETE / UPDATE — an arbitrarily large array in one
699 - // request would chew through the database serially and time out the
700 - // response. 200 is enough headroom for any realistic admin UI
701 - // selection; larger jobs should be split client-side (parity with the
702 - // donors bulk-action endpoint).
703 - if ( count( $ids ) > 200 ) {
704 - return new WP_Error(
705 - 'too_many_items',
706 - __( 'Bulk actions are limited to 200 donations per request.', 'suredonation' ),
707 - [ 'status' => 400 ]
708 - );
709 - }
710 -
711 646 $success_count = 0;
712 647 $error_count = 0;
713 648
714 649 foreach ( $ids as $id ) {
@@ -822,10 +757,9 @@
822 757 __( 'Stripe is not connected. Please configure Stripe in settings.', 'suredonation' ),
823 758 [ 'status' => 400 ]
824 759 );
825 760 }
826 - $refund_account_id = isset( $donation['stripe_account_id'] ) && is_string( $donation['stripe_account_id'] ) ? $donation['stripe_account_id'] : '';
827 - $refund_result = Stripe_Helper::create_refund( $transaction_id, $refund_amount, 'requested_by_customer', $refund_account_id );
761 + $refund_result = Stripe_Helper::create_refund( $transaction_id, $refund_amount, 'requested_by_customer' );
828 762 }
829 763
830 764 if ( is_wp_error( $refund_result ) ) {
831 765 return new WP_Error(
@@ -1172,12 +1106,13 @@
1172 1106 'sanitize_callback' => 'wp_kses_post',
1173 1107 ],
1174 1108 'payment_status' => [
1175 1109 'default' => 'pending',
1176 - 'type' => 'string',
1177 - 'enum' => Donations::get_valid_statuses(),
1110 + 'enum' => [ 'pending', 'processing', 'completed', 'failed', 'refunded', 'partially_refunded', 'cancelled' ],
1178 1111 'sanitize_callback' => 'sanitize_text_field',
1179 - 'validate_callback' => 'rest_validate_request_arg',
1112 + 'validate_callback' => static function ( $param ) {
1113 + return in_array( $param, [ 'pending', 'processing', 'completed', 'failed', 'refunded', 'partially_refunded', 'cancelled' ], true );
1114 + },
1180 1115 ],
1181 1116 'gateway' => [
1182 1117 'sanitize_callback' => 'sanitize_text_field',
1183 1118 ],
@@ -1210,13 +1145,12 @@
1210 1145 * @return int Amount in smallest currency unit.
1211 1146 * @since 0.0.1
1212 1147 */
1213 1148 private function amount_to_stripe_format( $amount, $currency ) {
1214 - // Delegates rather than repeating the zero-decimal list: the abilities
1215 - // layer guards refunds with Payment_Helper, so a second hardcoded list
1216 - // here could disagree with the guard about what a currency's minor unit
1217 - // is. Payment_Helper derives it from the currency data table.
1218 - return Payment_Helper::amount_to_stripe_format( $amount, $currency );
1149 + $zero_decimal = [ 'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF' ];
1150 + return in_array( strtoupper( $currency ), $zero_decimal, true )
1151 + ? (int) round( $amount )
1152 + : (int) round( $amount * 100 );
1219 1153 }
1220 1154
1221 1155 /**
1222 1156 * Convert amount from Stripe's smallest currency unit.
@@ -1226,9 +1160,12 @@
1226 1160 * @return float Amount in major currency unit.
1227 1161 * @since 0.0.1
1228 1162 */
1229 1163 private function amount_from_stripe_format( $amount, $currency ) {
1230 - return Payment_Helper::amount_from_stripe_format( $amount, $currency );
1164 + $zero_decimal = [ 'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF' ];
1165 + return in_array( strtoupper( $currency ), $zero_decimal, true )
1166 + ? (float) $amount
1167 + : (float) $amount / 100;
1231 1168 }
1232 1169
1233 1170 /**
1234 1171 * Format donation data for API response.
@@ -1261,41 +1198,14 @@
1261 1198 if ( ! is_array( $donation_data ) ) {
1262 1199 $donation_data = [];
1263 1200 }
1264 1201
1265 - // Build the persisted submitted fields list (label/value/group). The
1266 - // group is the parent block label (e.g. "Address") used to nest
1267 - // sub-fields on the entry screen; '' for standalone fields.
1268 - $submitted_fields = [];
1269 - if ( isset( $donation_data['fields'] ) && is_array( $donation_data['fields'] ) ) {
1270 - foreach ( $donation_data['fields'] as $field ) {
1271 - if ( ! is_array( $field ) ) {
1272 - continue;
1273 - }
1274 - // sanitize_text_field (not esc_html) for REST data: the values are
1275 - // already sanitized at write time and React escapes on render, so
1276 - // esc_html here would double-encode (e.g. "Cats & Dogs" -> "Cats & Dogs").
1277 - $submitted_fields[] = [
1278 - 'label' => sanitize_text_field( Helper::get_string_value( $field['label'] ?? '' ) ),
1279 - // Checkbox fields store a canonical untranslated token so the
1280 - // stored column stays locale-stable; it is translated here, on
1281 - // read, for the entry screen. Non-checkbox values pass through.
1282 - 'value' => sanitize_text_field( Helper::format_checkbox_field_value( $field['value'] ?? '' ) ),
1283 - 'group' => sanitize_text_field( Helper::get_string_value( $field['group'] ?? '' ) ),
1284 - ];
1285 - }
1286 - }
1287 -
1288 1202 return [
1289 1203 'id' => $donation_id,
1290 1204 'campaign_id' => $campaign_id,
1291 - // Plain-text titles rendered by React (which escapes text nodes and does
1292 - // not decode HTML entities). get_the_title() runs wptexturize, whose
1293 - // default replacements are entities (e.g. " - " -> "–"), so decode
1294 - // them here; wp_kses_post would leave the entity and it would show raw.
1295 - 'campaign_title' => $campaign_id ? html_entity_decode( wp_strip_all_tags( (string) get_the_title( $campaign_id ) ), ENT_QUOTES, 'UTF-8' ) : '',
1205 + 'campaign_title' => $campaign_id ? wp_kses_post( (string) get_the_title( $campaign_id ) ) : '',
1296 1206 'form_id' => $form_id,
1297 - 'form_title' => $form_id ? html_entity_decode( wp_strip_all_tags( (string) get_the_title( $form_id ) ), ENT_QUOTES, 'UTF-8' ) : '',
1207 + 'form_title' => $form_id ? wp_kses_post( (string) get_the_title( $form_id ) ) : '',
1298 1208 'form_edit_url' => $form_edit_url,
1299 1209 'donor_id' => isset( $donation['donor_id'] ) ? Helper::get_integer_value( $donation['donor_id'] ) : 0,
1300 1210 'donor_name' => esc_html( Helper::get_string_value( $donation['donor_name'] ?? '' ) ),
1301 1211 'donor_email' => sanitize_email( Helper::get_string_value( $donation['donor_email'] ?? '' ) ),
@@ -1316,9 +1226,8 @@
1316 1226 'subscription_status' => esc_html( Helper::get_string_value( $donation['subscription_status'] ?? '' ) ),
1317 1227 'parent_subscription_id' => isset( $donation['parent_subscription_id'] ) ? Helper::get_integer_value( $donation['parent_subscription_id'] ) : 0,
1318 1228 'subscription_interval' => esc_html( Helper::get_string_value( $donation_data['subscription_interval'] ?? '' ) ),
1319 1229 'billing_cycles' => esc_html( Helper::get_string_value( $donation_data['billing_cycles'] ?? '' ) ),
1320 - 'fields' => $submitted_fields,
1321 1230 'created_at' => esc_html( Helper::get_string_value( $donation['created_at'] ?? '' ) ),
1322 1231 'updated_at' => esc_html( Helper::get_string_value( $donation['updated_at'] ?? '' ) ),
1323 1232 'logs' => $logs,
1324 1233 ];