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 / abilities / config-ability.php

config-ability.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.6.0, at inc/abilities/config-ability.php

2,077 lines 80.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Abilities API Configuration
4 *
5 * Defines all ability configurations for the SureDonation plugin.
6 *
7 * @package SureDonation
8 * @since 0.0.1
9 */
10
11 namespace SureDonation\Inc\Abilities;
12
13 use SureDonation\Inc\Database\Tables\Donations;
14 use SureDonation\Inc\Helper;
15
16 // Exit if accessed directly.
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 /**
22 * Config_Ability class.
23 *
24 * @since 0.0.1
25 */
26 class Config_Ability {
27 /**
28 * Setting key gating abilities that create or modify records.
29 *
30 * @since 1.5.0
31 */
32 public const GATE_UPDATE = 'allow_updates';
33
34 /**
35 * Setting key gating abilities that destroy records.
36 *
37 * @since 1.5.0
38 */
39 public const GATE_DELETE = 'allow_delete';
40
41 /**
42 * Cached abilities.
43 *
44 * @var array<string, array<string, mixed>>|null
45 */
46 private static $abilities = null;
47
48 /**
49 * Whether a write/delete gate is open.
50 *
51 * These gates are opt-IN: an absent key is closed, exactly as an explicit
52 * `false` is. That has to match two other places or the product lies to the
53 * admin — `Settings_API::get_ai_settings()` reports an absent key as `false`
54 * via its defaults, and the settings screen renders the switch off. An
55 * earlier revision treated absent as OPEN for parity with SureForms, which
56 * meant a site whose `ai_settings` was written programmatically with only
57 * `enable_abilities` (WP-CLI, a migration, another plugin) enforced deletes
58 * and gateway refunds as enabled while the UI stated they were disabled.
59 *
60 * SureForms can afford absent-means-open because its toggles are flat
61 * options that always exist once its settings page has been saved. Ours live
62 * inside a serialized array that other code paths write, so the absent state
63 * is reachable and must fail closed.
64 *
65 * Agents are not shown tools they cannot call: Runtime::register() skips a
66 * gated-off ability entirely rather than registering one whose permission
67 * callback always fails.
68 *
69 * @param string $gate One of the GATE_* constants.
70 * @return bool True when abilities behind this gate may register and run.
71 * @since 1.5.0
72 */
73 public static function is_gate_open( $gate ) {
74 if ( '' === $gate ) {
75 return true;
76 }
77
78 $ai_option = Helper::get_suredonation_option( 'ai_settings', [] );
79 $ai_settings = is_array( $ai_option ) ? $ai_option : [];
80
81 // Absent means closed. Settings_API::get_ai_settings() reports both gates
82 // as false by default and the settings screen renders them off, so
83 // treating an absent key as open would let the plugin enforce writes and
84 // permanent deletes it is telling the admin are disabled. Reachable on a
85 // partial PATCH of just {enable_abilities:true}, or a site that toggled
86 // abilities before these sub-keys shipped. Deny is the only safe
87 // fallthrough for a gate whose whole purpose is a deliberate second
88 // decision about destructive or money-moving operations.
89 return ! empty( $ai_settings[ $gate ] );
90 }
91
92 /**
93 * Get all ability configurations.
94 *
95 * @return array<string, array<string, mixed>> Ability definitions.
96 */
97 public static function get_abilities() {
98 if ( null !== self::$abilities ) {
99 return self::$abilities;
100 }
101
102 $runtime = new Runtime();
103
104 $perm_read = static function () use ( $runtime ) {
105 return $runtime->permission_callback( 'manage_options' );
106 };
107
108 $perm_edit = static function () use ( $runtime ) {
109 return self::is_gate_open( self::GATE_UPDATE ) && $runtime->permission_callback( 'manage_options' );
110 };
111
112 $perm_delete = static function () use ( $runtime ) {
113 return self::is_gate_open( self::GATE_DELETE ) && $runtime->permission_callback( 'manage_options' );
114 };
115
116 $abilities = array_merge(
117 self::get_campaign_abilities( $runtime, $perm_read, $perm_edit, $perm_delete ),
118 self::get_donation_abilities( $runtime, $perm_read, $perm_edit, $perm_delete ),
119 self::get_donor_abilities( $runtime, $perm_read, $perm_edit ),
120 self::get_form_abilities( $runtime, $perm_read, $perm_edit, $perm_delete ),
121 self::get_analytics_abilities( $runtime, $perm_read )
122 );
123
124 /**
125 * Filter SureDonation ability configurations.
126 *
127 * @param array $abilities Ability definitions.
128 */
129 $abilities = apply_filters( 'suredonation_config_abilities', $abilities );
130 if ( ! is_array( $abilities ) ) {
131 $abilities = [];
132 }
133
134 self::$abilities = $abilities;
135
136 return $abilities;
137 }
138
139 /**
140 * Get a single ability config by name.
141 *
142 * @param string $ability_name Ability identifier.
143 * @return array<string, mixed>|false Ability config or false.
144 */
145 public static function get_ability( $ability_name ) {
146 if ( null === self::$abilities ) {
147 self::$abilities = self::get_abilities();
148 }
149 return self::$abilities[ $ability_name ] ?? false;
150 }
151
152 /**
153 * Get ability input schema.
154 *
155 * @param string $ability_name Ability identifier.
156 * @return array<string, mixed>|false Input schema or false.
157 */
158 public static function get_ability_input_schema( $ability_name ) {
159 $ability = self::get_ability( $ability_name );
160 if ( false === $ability ) {
161 return false;
162 }
163 $schema = $ability['input_schema'] ?? false;
164 return is_array( $schema ) ? $schema : false;
165 }
166
167 /**
168 * Build meta block for an ability.
169 *
170 * Three consumers read this block and each needs a different key:
171 *
172 * - `show_in_rest` gates the core `wp-abilities/v1` REST controllers. It
173 * defaults to false in WP_Ability, so omitting it makes an ability
174 * invisible to `GET /abilities` and unrunnable via `/run`.
175 * - `annotations` must use core's key names (`readonly`, `destructive`,
176 * `idempotent`). The MCP-spec spellings (`readOnlyHint` and friends) are
177 * not recognised by core, which leaves its own keys null.
178 * - `tool_type` is a TOP-LEVEL key read by MCP clients to classify the
179 * operation. Without it a client has to guess from the tool name, and a
180 * mutating ability whose name starts with a read-ish verb can slip past
181 * an approval gate.
182 *
183 * Public rather than private: this is the single place that encodes the meta
184 * contract, and SureDonation Pro registers its own abilities through the
185 * `suredonation_config_abilities` filter. Pro hand-rolling this block would
186 * guarantee drift the moment any consumer's expectations change.
187 *
188 * @param string $tool_type One of read|write|list|search|action|delete.
189 * @param float $priority Priority level (1.0 read, 2.0 write, 3.0 destructive).
190 * @param bool $read_only Whether the ability only reads data.
191 * @param bool $destructive Whether the ability destroys data.
192 * @param bool $idempotent Whether repeated calls produce the same result.
193 * @param string $instructions Optional guidance for the calling model.
194 * @return array<string, mixed> Meta configuration.
195 */
196 public static function build_meta( $tool_type = 'read', $priority = 1.0, $read_only = true, $destructive = false, $idempotent = true, $instructions = '' ) {
197 $annotations = [
198 'readonly' => $read_only,
199 'destructive' => $destructive,
200 'idempotent' => $idempotent,
201 'priority' => $priority,
202 // Deliberate MCP-spec spelling among core's snake_case annotation keys:
203 // core does not define this one, and clients read the camelCase name.
204 'openWorldHint' => false,
205 ];
206
207 if ( '' !== $instructions ) {
208 $annotations['instructions'] = $instructions;
209 }
210
211 return [
212 'show_in_rest' => true,
213 'tool_type' => $tool_type,
214 'annotations' => $annotations,
215 'mcp' => [
216 'public' => false,
217 'type' => 'tool',
218 ],
219 ];
220 }
221
222 /**
223 * Get campaign ability configurations.
224 *
225 * @param Runtime $runtime Runtime instance.
226 * @param callable $perm_read Read permission closure.
227 * @param callable $perm_edit Edit permission closure.
228 * @param callable $perm_delete Delete permission closure.
229 * @return array<string, array<string, mixed>> Campaign abilities.
230 */
231 private static function get_campaign_abilities( $runtime, $perm_read, $perm_edit, $perm_delete ) {
232 $ns = SUREDONATION_ABILITY_API_NAMESPACE;
233
234 return [
235 $ns . 'list-campaigns' => [
236 'label' => __( 'List campaigns', 'suredonation' ),
237 'description' => __( 'Returns a paginated list of fundraising campaigns with optional search, status filter, and sorting.', 'suredonation' ),
238 'category' => 'suredonation',
239 'permission_callback' => $perm_read,
240 'input_schema' => [
241 'type' => 'object',
242 'properties' => [
243 'search' => [
244 'type' => 'string',
245 'description' => __( 'Search campaigns by title.', 'suredonation' ),
246 'default' => '',
247 ],
248 'status' => [
249 'type' => 'string',
250 'enum' => [ 'all', 'publish', 'draft', 'trash', 'paused' ],
251 'default' => 'all',
252 'description' => __( 'Filter by status. "publish", "draft" and "trash" are WordPress post statuses; "paused" matches published campaigns whose campaign status is paused.', 'suredonation' ),
253 ],
254 'sort_by' => [
255 'type' => 'string',
256 'enum' => [ 'date', 'title', 'status' ],
257 'default' => 'date',
258 'description' => __( 'Column to sort by.', 'suredonation' ),
259 ],
260 'order' => [
261 'type' => 'string',
262 'enum' => [ 'ASC', 'DESC' ],
263 'default' => 'DESC',
264 'description' => __( 'Sort direction.', 'suredonation' ),
265 ],
266 'page' => [
267 'type' => 'integer',
268 'default' => 1,
269 'description' => __( 'Page number (1-based).', 'suredonation' ),
270 ],
271 'per_page' => [
272 'type' => 'integer',
273 'default' => 20,
274 'description' => __( 'Results per page (max 100).', 'suredonation' ),
275 ],
276 ],
277 ],
278 'output_schema' => [
279 'type' => 'object',
280 'properties' => [
281 'campaigns' => [
282 'type' => 'array',
283 'items' => [
284 'type' => 'object',
285 'properties' => [
286 'id' => [ 'type' => 'integer' ],
287 'title' => [ 'type' => 'string' ],
288 'status' => [ 'type' => 'string' ],
289 'goal_type' => [ 'type' => 'string' ],
290 'goal' => [ 'type' => 'number' ],
291 'raised' => [ 'type' => 'number' ],
292 'donors' => [ 'type' => 'integer' ],
293 'progress' => [ 'type' => 'number' ],
294 'created_at' => [ 'type' => 'string' ],
295 'modified_at' => [ 'type' => 'string' ],
296 'post_status' => [ 'type' => 'string' ],
297 'currency' => [ 'type' => 'string' ],
298 'terms_text' => [ 'type' => 'string' ],
299 'thank_you_message' => [ 'type' => 'string' ],
300 'featured_image' => [ 'type' => 'integer' ],
301 'featured_image_url' => [ 'type' => 'string' ],
302 'has_page' => [ 'type' => 'boolean' ],
303 'permalink' => [ 'type' => 'string' ],
304 'author' => [ 'type' => 'string' ],
305 'edit_url' => [ 'type' => 'string' ],
306 'default_form_id' => [ 'type' => 'integer' ],
307 ],
308 ],
309 ],
310 'total' => [
311 'type' => 'integer',
312 'description' => __( 'Total matching campaigns.', 'suredonation' ),
313 ],
314 'total_pages' => [
315 'type' => 'integer',
316 'description' => __( 'Total pages.', 'suredonation' ),
317 ],
318 ],
319 ],
320 'execute_callback' => static function ( $input ) use ( $runtime ) {
321 return $runtime->list_campaigns( $input );
322 },
323 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
324 ],
325
326 $ns . 'get-campaign' => [
327 'label' => __( 'Get campaign', 'suredonation' ),
328 'description' => __( 'Returns a single fundraising campaign by ID with real-time stats including total raised, donor count, and progress.', 'suredonation' ),
329 'category' => 'suredonation',
330 'permission_callback' => $perm_read,
331 'input_schema' => [
332 'type' => 'object',
333 'required' => [ 'id' ],
334 'properties' => [
335 'id' => [
336 'type' => 'integer',
337 'description' => __( 'The campaign ID.', 'suredonation' ),
338 ],
339 ],
340 ],
341 'output_schema' => [
342 'type' => 'object',
343 'properties' => [
344 'id' => [ 'type' => 'integer' ],
345 'title' => [ 'type' => 'string' ],
346 'description' => [ 'type' => 'string' ],
347 'status' => [ 'type' => 'string' ],
348 'goal_type' => [ 'type' => 'string' ],
349 'goal' => [ 'type' => 'number' ],
350 'raised' => [ 'type' => 'number' ],
351 'donors' => [ 'type' => 'integer' ],
352 'progress' => [ 'type' => 'number' ],
353 'donation_count' => [ 'type' => 'integer' ],
354 'average_donation' => [ 'type' => 'number' ],
355 'largest_donation' => [ 'type' => 'number' ],
356 'is_goal_reached' => [ 'type' => 'boolean' ],
357 'require_terms' => [ 'type' => 'boolean' ],
358 'created_at' => [ 'type' => 'string' ],
359 'modified_at' => [ 'type' => 'string' ],
360 'post_status' => [ 'type' => 'string' ],
361 'currency' => [ 'type' => 'string' ],
362 'terms_text' => [ 'type' => 'string' ],
363 'thank_you_message' => [ 'type' => 'string' ],
364 'featured_image' => [ 'type' => 'integer' ],
365 'featured_image_url' => [ 'type' => 'string' ],
366 'has_page' => [ 'type' => 'boolean' ],
367 'permalink' => [ 'type' => 'string' ],
368 'author' => [ 'type' => 'string' ],
369 'edit_url' => [ 'type' => 'string' ],
370 'default_form_id' => [ 'type' => 'integer' ],
371 ],
372 ],
373 'execute_callback' => static function ( $input ) use ( $runtime ) {
374 return $runtime->get_campaign( $input );
375 },
376 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
377 ],
378
379 $ns . 'create-campaign' => [
380 'label' => __( 'Create campaign', 'suredonation' ),
381 'description' => __( 'Creates a new fundraising campaign with title, description, goal settings, and optional fee coverage/terms configuration.', 'suredonation' ),
382 'category' => 'suredonation',
383 'permission_callback' => $perm_edit,
384 'input_schema' => [
385 'type' => 'object',
386 'required' => [ 'title' ],
387 'properties' => [
388 'title' => [
389 'type' => 'string',
390 'description' => __( 'Campaign title.', 'suredonation' ),
391 ],
392 'description' => [
393 'type' => 'string',
394 'format' => 'html',
395 'description' => __( 'Campaign description (HTML allowed).', 'suredonation' ),
396 'default' => '',
397 ],
398 'goal_type' => [
399 'type' => 'string',
400 'enum' => [ 'raised_amount', 'donation_count' ],
401 'default' => 'raised_amount',
402 'description' => __( 'Goal type: track by amount raised or donation count.', 'suredonation' ),
403 ],
404 'goal_amount' => [
405 'type' => 'number',
406 'description' => __( 'Goal amount (0 for no goal).', 'suredonation' ),
407 'default' => 0,
408 ],
409 'campaign_status' => [
410 'type' => 'string',
411 'enum' => [ 'active', 'paused', 'completed' ],
412 'default' => 'active',
413 'description' => __( 'Campaign status.', 'suredonation' ),
414 ],
415 'require_terms' => [
416 'type' => 'boolean',
417 'default' => false,
418 'description' => __( 'Require terms acceptance before donating.', 'suredonation' ),
419 ],
420 'terms_text' => [
421 'type' => 'string',
422 'default' => '',
423 'description' => __( 'Terms and conditions text.', 'suredonation' ),
424 ],
425 'thank_you_message' => [
426 'type' => 'string',
427 'default' => '',
428 'description' => __( 'Message shown to the donor after a successful donation.', 'suredonation' ),
429 ],
430 'featured_image' => [
431 'type' => 'integer',
432 'default' => 0,
433 'description' => __( 'Attachment ID to use as the campaign featured image (0 for none).', 'suredonation' ),
434 ],
435 ],
436 ],
437 'output_schema' => [
438 'type' => 'object',
439 'properties' => [
440 'id' => [
441 'type' => 'integer',
442 'description' => __( 'New campaign ID.', 'suredonation' ),
443 ],
444 'title' => [ 'type' => 'string' ],
445 'status' => [ 'type' => 'string' ],
446 'message' => [ 'type' => 'string' ],
447 ],
448 ],
449 'execute_callback' => static function ( $input ) use ( $runtime ) {
450 return $runtime->create_campaign( $input );
451 },
452 'meta' => self::build_meta( 'write', 2.0, false, false, false ),
453 'gate' => self::GATE_UPDATE,
454 ],
455
456 $ns . 'update-campaign' => [
457 'label' => __( 'Update campaign', 'suredonation' ),
458 'description' => __( 'Updates an existing campaign. All fields except ID are optional — only provided fields are updated.', 'suredonation' ),
459 'category' => 'suredonation',
460 'permission_callback' => $perm_edit,
461 'input_schema' => [
462 'type' => 'object',
463 'required' => [ 'id' ],
464 'properties' => [
465 'id' => [
466 'type' => 'integer',
467 'description' => __( 'Campaign ID to update.', 'suredonation' ),
468 ],
469 'title' => [
470 'type' => 'string',
471 'description' => __( 'Campaign title.', 'suredonation' ),
472 ],
473 'description' => [
474 'type' => 'string',
475 'format' => 'html',
476 'description' => __( 'Campaign description (HTML allowed).', 'suredonation' ),
477 ],
478 'goal_type' => [
479 'type' => 'string',
480 'enum' => [ 'raised_amount', 'donation_count' ],
481 'description' => __( 'Goal type.', 'suredonation' ),
482 ],
483 'goal_amount' => [
484 'type' => 'number',
485 'description' => __( 'Goal amount.', 'suredonation' ),
486 ],
487 'campaign_status' => [
488 'type' => 'string',
489 'enum' => [ 'active', 'paused', 'completed' ],
490 'description' => __( 'Campaign status.', 'suredonation' ),
491 ],
492 'require_terms' => [
493 'type' => 'boolean',
494 'description' => __( 'Require terms acceptance.', 'suredonation' ),
495 ],
496 'terms_text' => [
497 'type' => 'string',
498 'description' => __( 'Terms and conditions text.', 'suredonation' ),
499 ],
500 'thank_you_message' => [
501 'type' => 'string',
502 'description' => __( 'Message shown to the donor after a successful donation.', 'suredonation' ),
503 ],
504 'featured_image' => [
505 'type' => 'integer',
506 'description' => __( 'Attachment ID to use as the campaign featured image (0 clears it).', 'suredonation' ),
507 ],
508 ],
509 ],
510 'output_schema' => [
511 'type' => 'object',
512 'properties' => [
513 'id' => [ 'type' => 'integer' ],
514 'title' => [ 'type' => 'string' ],
515 'status' => [ 'type' => 'string' ],
516 'message' => [ 'type' => 'string' ],
517 ],
518 ],
519 'execute_callback' => static function ( $input ) use ( $runtime ) {
520 return $runtime->update_campaign( $input );
521 },
522 'meta' => self::build_meta( 'write', 2.0, false, false, false ),
523 'gate' => self::GATE_UPDATE,
524 ],
525
526 $ns . 'delete-campaign' => [
527 'label' => __( 'Delete campaign', 'suredonation' ),
528 'description' => __( 'Permanently deletes a campaign by ID, along with its donation forms. Refused when the campaign has donations recorded against it, since those are financial records. This action cannot be undone.', 'suredonation' ),
529 'category' => 'suredonation',
530 'permission_callback' => $perm_delete,
531 'input_schema' => [
532 'type' => 'object',
533 'required' => [ 'id' ],
534 'properties' => [
535 'id' => [
536 'type' => 'integer',
537 'description' => __( 'Campaign ID to delete.', 'suredonation' ),
538 ],
539 ],
540 ],
541 'output_schema' => [
542 'type' => 'object',
543 'properties' => [
544 'id' => [ 'type' => 'integer' ],
545 'deleted_forms' => [
546 'type' => 'array',
547 'items' => [ 'type' => 'integer' ],
548 'description' => __( 'IDs of the campaign donation forms deleted alongside it.', 'suredonation' ),
549 ],
550 'kept_forms' => [
551 'type' => 'array',
552 'items' => [ 'type' => 'integer' ],
553 'description' => __( 'Forms left in place because they still have donations recorded against them.', 'suredonation' ),
554 ],
555 'message' => [ 'type' => 'string' ],
556 ],
557 ],
558 'execute_callback' => static function ( $input ) use ( $runtime ) {
559 return $runtime->delete_campaign( $input );
560 },
561 'meta' => self::build_meta( 'delete', 3.0, false, true, false, __( 'Permanent and not undoable. It also deletes the campaign\'s donation forms. Confirm with the user before executing.', 'suredonation' ) ),
562 'gate' => self::GATE_DELETE,
563 ],
564
565 $ns . 'duplicate-campaign' => [
566 'label' => __( 'Duplicate campaign', 'suredonation' ),
567 'description' => __( 'Creates a copy of an existing campaign as a draft. Copies title (with " (Copy)" suffix), description, and settings.', 'suredonation' ),
568 'category' => 'suredonation',
569 'permission_callback' => $perm_edit,
570 'input_schema' => [
571 'type' => 'object',
572 'required' => [ 'id' ],
573 'properties' => [
574 'id' => [
575 'type' => 'integer',
576 'description' => __( 'Campaign ID to duplicate.', 'suredonation' ),
577 ],
578 ],
579 ],
580 'output_schema' => [
581 'type' => 'object',
582 'properties' => [
583 'id' => [
584 'type' => 'integer',
585 'description' => __( 'New campaign ID.', 'suredonation' ),
586 ],
587 'title' => [ 'type' => 'string' ],
588 'message' => [ 'type' => 'string' ],
589 ],
590 ],
591 'execute_callback' => static function ( $input ) use ( $runtime ) {
592 return $runtime->duplicate_campaign( $input );
593 },
594 'meta' => self::build_meta( 'write', 2.0, false, false, false ),
595 'gate' => self::GATE_UPDATE,
596 ],
597
598 $ns . 'get-campaign-form-locations' => [
599 'label' => __( 'Get campaign form locations', 'suredonation' ),
600 'description' => __( 'Finds all pages and posts where a campaign donation form block is embedded. Returns page IDs, titles, and edit/view URLs.', 'suredonation' ),
601 'category' => 'suredonation',
602 'permission_callback' => $perm_read,
603 'input_schema' => [
604 'type' => 'object',
605 'required' => [ 'id' ],
606 'properties' => [
607 'id' => [
608 'type' => 'integer',
609 'description' => __( 'Campaign ID.', 'suredonation' ),
610 ],
611 ],
612 ],
613 'output_schema' => [
614 'type' => 'object',
615 'properties' => [
616 'locations' => [
617 'type' => 'array',
618 'items' => [
619 'type' => 'object',
620 'properties' => [
621 'id' => [ 'type' => 'integer' ],
622 'title' => [ 'type' => 'string' ],
623 'type' => [ 'type' => 'string' ],
624 'status' => [ 'type' => 'string' ],
625 'modified_at' => [ 'type' => 'string' ],
626 'edit_url' => [ 'type' => 'string' ],
627 'view_url' => [ 'type' => 'string' ],
628 ],
629 ],
630 ],
631 ],
632 ],
633 'execute_callback' => static function ( $input ) use ( $runtime ) {
634 return $runtime->get_campaign_form_locations( $input );
635 },
636 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
637 ],
638
639 $ns . 'update-campaign-status' => [
640 'label' => __( 'Publish, draft or trash a campaign', 'suredonation' ),
641 'description' => __( 'Changes a campaign\'s WordPress post status: publish makes it live, draft hides it, trash removes it from listings without deleting it. This is different from the campaign business status (active/paused/completed), which update-campaign sets via campaign_status.', 'suredonation' ),
642 'category' => 'suredonation',
643 'permission_callback' => $perm_edit,
644 'gate' => self::GATE_UPDATE,
645 'input_schema' => [
646 'type' => 'object',
647 'required' => [ 'id', 'status' ],
648 'properties' => [
649 'id' => [
650 'type' => 'integer',
651 'description' => __( 'Campaign ID.', 'suredonation' ),
652 ],
653 'status' => [
654 'type' => 'string',
655 'enum' => [ 'publish', 'draft', 'trash' ],
656 'description' => __( 'The new post status.', 'suredonation' ),
657 ],
658 ],
659 ],
660 'output_schema' => [
661 'type' => 'object',
662 'properties' => [
663 'id' => [ 'type' => 'integer' ],
664 'post_status' => [ 'type' => 'string' ],
665 'previous_status' => [ 'type' => 'string' ],
666 'changed' => [ 'type' => 'boolean' ],
667 'message' => [ 'type' => 'string' ],
668 ],
669 ],
670 'execute_callback' => static function ( $input ) use ( $runtime ) {
671 return $runtime->update_campaign_status( $input );
672 },
673 'meta' => self::build_meta( 'write', 2.0, false, false, true ),
674 ],
675 ];
676 }
677
678 /**
679 * Get donation ability configurations.
680 *
681 * @param Runtime $runtime Runtime instance.
682 * @param callable $perm_read Read permission closure.
683 * @param callable $perm_edit Edit permission closure.
684 * @param callable $perm_delete Delete permission closure.
685 * @return array<string, array<string, mixed>> Donation abilities.
686 */
687 private static function get_donation_abilities( $runtime, $perm_read, $perm_edit, $perm_delete ) {
688 $ns = SUREDONATION_ABILITY_API_NAMESPACE;
689
690 return [
691 $ns . 'list-donations' => [
692 'label' => __( 'List donations', 'suredonation' ),
693 'description' => __( 'Returns a paginated list of donations with optional search, status filter, campaign filter, and sorting.', 'suredonation' ),
694 'category' => 'suredonation',
695 'permission_callback' => $perm_read,
696 'input_schema' => [
697 'type' => 'object',
698 'properties' => [
699 'search' => [
700 'type' => 'string',
701 'description' => __( 'Search donations by donor name, donor email, or transaction ID.', 'suredonation' ),
702 'default' => '',
703 ],
704 'status' => [
705 'type' => 'string',
706 // Sourced from the table's whitelist so this cannot drift
707 // from what the codebase actually writes, as the
708 // write schemas below had.
709 'enum' => array_merge( [ 'all' ], Donations::get_valid_statuses() ),
710 'default' => 'all',
711 'description' => __( 'Filter by payment status.', 'suredonation' ),
712 ],
713 'campaign_id' => [
714 'type' => 'integer',
715 'default' => 0,
716 'description' => __( 'Filter by campaign ID (0 for all campaigns).', 'suredonation' ),
717 ],
718 'sort_by' => [
719 'type' => 'string',
720 'enum' => [ 'id', 'created_at', 'updated_at', 'amount', 'donor_name', 'donor_email', 'payment_status', 'campaign_id', 'subscription_status' ],
721 'default' => 'created_at',
722 'description' => __( 'Column to sort by.', 'suredonation' ),
723 ],
724 'order' => [
725 'type' => 'string',
726 'enum' => [ 'ASC', 'DESC' ],
727 'default' => 'DESC',
728 'description' => __( 'Sort direction.', 'suredonation' ),
729 ],
730 'page' => [
731 'type' => 'integer',
732 'default' => 1,
733 'description' => __( 'Page number (1-based).', 'suredonation' ),
734 ],
735 'per_page' => [
736 'type' => 'integer',
737 'default' => 20,
738 'description' => __( 'Results per page (max 100).', 'suredonation' ),
739 ],
740 ],
741 ],
742 'output_schema' => [
743 'type' => 'object',
744 'properties' => [
745 'donations' => [
746 'type' => 'array',
747 'items' => [
748 'type' => 'object',
749 'properties' => [
750 'id' => [ 'type' => 'integer' ],
751 'campaign_id' => [ 'type' => 'integer' ],
752 'campaign_title' => [ 'type' => 'string' ],
753 'donor_name' => [ 'type' => 'string' ],
754 'donor_email' => [ 'type' => 'string' ],
755 'amount' => [ 'type' => 'number' ],
756 'currency' => [ 'type' => 'string' ],
757 'payment_status' => [ 'type' => 'string' ],
758 'donation_type' => [ 'type' => 'string' ],
759 'gateway' => [ 'type' => 'string' ],
760 'form_id' => [ 'type' => 'integer' ],
761 'form_title' => [ 'type' => 'string' ],
762 'subscription_id' => [ 'type' => 'string' ],
763 'subscription_status' => [ 'type' => 'string' ],
764 'created_at' => [ 'type' => 'string' ],
765 ],
766 ],
767 ],
768 'total' => [
769 'type' => 'integer',
770 'description' => __( 'Total matching donations.', 'suredonation' ),
771 ],
772 'total_pages' => [
773 'type' => 'integer',
774 'description' => __( 'Total pages.', 'suredonation' ),
775 ],
776 ],
777 ],
778 'execute_callback' => static function ( $input ) use ( $runtime ) {
779 return $runtime->list_donations( $input );
780 },
781 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
782 ],
783
784 $ns . 'get-donation' => [
785 'label' => __( 'Get donation', 'suredonation' ),
786 'description' => __( 'Returns a single donation by ID with full details including donor info, payment data, transaction ID, and activity logs.', 'suredonation' ),
787 'category' => 'suredonation',
788 'permission_callback' => $perm_read,
789 'input_schema' => [
790 'type' => 'object',
791 'required' => [ 'id' ],
792 'properties' => [
793 'id' => [
794 'type' => 'integer',
795 'description' => __( 'The donation ID.', 'suredonation' ),
796 ],
797 ],
798 ],
799 'output_schema' => [
800 'type' => 'object',
801 'properties' => [
802 'id' => [ 'type' => 'integer' ],
803 'campaign_id' => [ 'type' => 'integer' ],
804 'campaign_title' => [ 'type' => 'string' ],
805 'donor_id' => [ 'type' => 'integer' ],
806 'donor_name' => [ 'type' => 'string' ],
807 'donor_email' => [ 'type' => 'string' ],
808 'donor_phone' => [ 'type' => 'string' ],
809 'amount' => [ 'type' => 'number' ],
810 'fees_covered' => [ 'type' => 'number' ],
811 'refunded_amount' => [ 'type' => 'number' ],
812 'currency' => [ 'type' => 'string' ],
813 'donation_type' => [ 'type' => 'string' ],
814 'is_anonymous' => [ 'type' => 'boolean' ],
815 'donor_comment' => [ 'type' => 'string' ],
816 'donor_comment_status' => [
817 'type' => 'string',
818 'enum' => [ 'approved', 'pending', 'rejected' ],
819 ],
820 'payment_status' => [ 'type' => 'string' ],
821 'payment_mode' => [ 'type' => 'string' ],
822 'gateway' => [ 'type' => 'string' ],
823 'transaction_id' => [ 'type' => 'string' ],
824 'form_id' => [ 'type' => 'integer' ],
825 'form_title' => [ 'type' => 'string' ],
826 'stripe_customer_id' => [ 'type' => 'string' ],
827 'stripe_account_id' => [
828 'type' => 'string',
829 'description' => __( 'Connected Stripe account that processed this donation.', 'suredonation' ),
830 ],
831 'subscription_id' => [ 'type' => 'string' ],
832 'subscription_status' => [ 'type' => 'string' ],
833 'parent_subscription_id' => [ 'type' => 'integer' ],
834 'subscription_interval' => [ 'type' => 'string' ],
835 'billing_cycles' => [ 'type' => 'string' ],
836 'receipt_sent' => [ 'type' => 'boolean' ],
837 'receipt_pdf_url' => [ 'type' => 'string' ],
838 'import_source' => [ 'type' => 'string' ],
839 'fields' => [
840 'type' => 'array',
841 'description' => __( 'Field values the donor submitted, as label/value/group triples.', 'suredonation' ),
842 'items' => [
843 'type' => 'object',
844 'properties' => [
845 'label' => [ 'type' => 'string' ],
846 'value' => [ 'type' => 'string' ],
847 'group' => [ 'type' => 'string' ],
848 ],
849 ],
850 ],
851 'created_at' => [ 'type' => 'string' ],
852 'updated_at' => [ 'type' => 'string' ],
853 'logs' => [ 'type' => 'array' ],
854 ],
855 ],
856 'execute_callback' => static function ( $input ) use ( $runtime ) {
857 return $runtime->get_donation( $input );
858 },
859 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
860 ],
861
862 $ns . 'get-donation-notes' => [
863 'label' => __( 'Get donation notes', 'suredonation' ),
864 'description' => __( 'Returns paginated notes for a donation. Notes are admin-added comments for internal tracking.', 'suredonation' ),
865 'category' => 'suredonation',
866 'permission_callback' => $perm_read,
867 'input_schema' => [
868 'type' => 'object',
869 'required' => [ 'id' ],
870 'properties' => [
871 'id' => [
872 'type' => 'integer',
873 'description' => __( 'The donation ID.', 'suredonation' ),
874 ],
875 'page' => [
876 'type' => 'integer',
877 'default' => 1,
878 'description' => __( 'Page number.', 'suredonation' ),
879 ],
880 'per_page' => [
881 'type' => 'integer',
882 'default' => 10,
883 'description' => __( 'Notes per page (max 100).', 'suredonation' ),
884 ],
885 ],
886 ],
887 'output_schema' => [
888 'type' => 'object',
889 'properties' => [
890 'notes' => [
891 'type' => 'array',
892 'items' => [
893 'type' => 'object',
894 'properties' => [
895 'id' => [ 'type' => 'string' ],
896 'content' => [ 'type' => 'string' ],
897 'author_id' => [ 'type' => 'integer' ],
898 'author_name' => [ 'type' => 'string' ],
899 'created_at' => [ 'type' => 'string' ],
900 ],
901 ],
902 ],
903 'total' => [
904 'type' => 'integer',
905 'description' => __( 'Total notes.', 'suredonation' ),
906 ],
907 'total_pages' => [
908 'type' => 'integer',
909 'description' => __( 'Total pages.', 'suredonation' ),
910 ],
911 ],
912 ],
913 'execute_callback' => static function ( $input ) use ( $runtime ) {
914 return $runtime->get_donation_notes( $input );
915 },
916 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
917 ],
918
919 $ns . 'add-donation-note' => [
920 'label' => __( 'Add donation note', 'suredonation' ),
921 'description' => __( 'Adds an internal note to a donation for admin tracking purposes.', 'suredonation' ),
922 'category' => 'suredonation',
923 'permission_callback' => $perm_edit,
924 'input_schema' => [
925 'type' => 'object',
926 'required' => [ 'id', 'note' ],
927 'properties' => [
928 'id' => [
929 'type' => 'integer',
930 'description' => __( 'The donation ID.', 'suredonation' ),
931 ],
932 'note' => [
933 'type' => 'string',
934 'description' => __( 'The note content.', 'suredonation' ),
935 ],
936 ],
937 ],
938 'output_schema' => [
939 'type' => 'object',
940 'properties' => [
941 'note_id' => [
942 'type' => 'string',
943 'description' => __( 'The new note ID.', 'suredonation' ),
944 ],
945 'message' => [ 'type' => 'string' ],
946 ],
947 ],
948 'execute_callback' => static function ( $input ) use ( $runtime ) {
949 return $runtime->add_donation_note( $input );
950 },
951 'meta' => self::build_meta( 'write', 2.0, false, false, false ),
952 'gate' => self::GATE_UPDATE,
953 ],
954
955 $ns . 'update-donation-status' => [
956 'label' => __( 'Update donation status', 'suredonation' ),
957 'description' => __( 'Changes a donation\'s payment status. Use refund-donation instead when money should actually move: this only changes the record.', 'suredonation' ),
958 'category' => 'suredonation',
959 'permission_callback' => $perm_edit,
960 'gate' => self::GATE_UPDATE,
961 'input_schema' => [
962 'type' => 'object',
963 'required' => [ 'id', 'status' ],
964 'properties' => [
965 'id' => [
966 'type' => 'integer',
967 'description' => __( 'The donation ID.', 'suredonation' ),
968 ],
969 'status' => [
970 'type' => 'string',
971 'enum' => Donations::get_valid_statuses(),
972 'description' => __( 'The new payment status.', 'suredonation' ),
973 ],
974 ],
975 ],
976 'output_schema' => [
977 'type' => 'object',
978 'properties' => [
979 'id' => [ 'type' => 'integer' ],
980 'payment_status' => [ 'type' => 'string' ],
981 'previous_status' => [ 'type' => 'string' ],
982 'changed' => [
983 'type' => 'boolean',
984 'description' => __( 'False when the donation already had that status.', 'suredonation' ),
985 ],
986 'message' => [ 'type' => 'string' ],
987 ],
988 ],
989 'execute_callback' => static function ( $input ) use ( $runtime ) {
990 return $runtime->update_donation_status( $input );
991 },
992 'meta' => self::build_meta( 'write', 2.0, false, false, true ),
993 ],
994
995 $ns . 'refund-donation' => [
996 'label' => __( 'Refund donation', 'suredonation' ),
997 'description' => __( 'Refunds a donation through the gateway that processed it (Stripe or PayPal), fully or partially. Moves real money and cannot be undone. Amounts are in the donation currency (for example 25.50), not cents. Omit the amount to refund everything still refundable. This also emails the donor a refund notification and fires any connected refund automations. For a recurring donation it refunds the one charge only and does not cancel the subscription.', 'suredonation' ),
998 'category' => 'suredonation',
999 'permission_callback' => $perm_edit,
1000 'gate' => self::GATE_UPDATE,
1001 'input_schema' => [
1002 'type' => 'object',
1003 'required' => [ 'id' ],
1004 'properties' => [
1005 'id' => [
1006 'type' => 'integer',
1007 'description' => __( 'The donation ID.', 'suredonation' ),
1008 ],
1009 'amount' => [
1010 'type' => 'number',
1011 'default' => 0,
1012 'description' => __( 'Amount to refund in the donation currency. 0 or omitted refunds the full remaining balance.', 'suredonation' ),
1013 ],
1014 'transaction_id' => [
1015 'type' => 'string',
1016 'default' => '',
1017 'description' => __( 'Optional safety check. When supplied it must match the donation\'s gateway transaction ID.', 'suredonation' ),
1018 ],
1019 'notes' => [
1020 'type' => 'string',
1021 'default' => '',
1022 'description' => __( 'Internal note recorded against the refund.', 'suredonation' ),
1023 ],
1024 ],
1025 ],
1026 'output_schema' => [
1027 'type' => 'object',
1028 'properties' => [
1029 'id' => [ 'type' => 'integer' ],
1030 'refunded' => [
1031 'type' => 'number',
1032 'description' => __( 'Amount refunded by this call, in the donation currency.', 'suredonation' ),
1033 ],
1034 'currency' => [ 'type' => 'string' ],
1035 'refunded_total' => [
1036 'type' => 'number',
1037 'description' => __( 'Total refunded against this donation so far.', 'suredonation' ),
1038 ],
1039 'payment_status' => [ 'type' => 'string' ],
1040 'message' => [ 'type' => 'string' ],
1041 ],
1042 ],
1043 'execute_callback' => static function ( $input ) use ( $runtime ) {
1044 return $runtime->refund_donation( $input );
1045 },
1046 'meta' => self::build_meta( 'action', 3.0, false, true, false, __( 'Moves real money through the payment gateway and cannot be undone, emails the donor a refund notification, and does not cancel a subscription. Always confirm the donation and the amount with the user before executing.', 'suredonation' ) ),
1047 ],
1048
1049 $ns . 'create-donation' => [
1050 'label' => __( 'Record a donation', 'suredonation' ),
1051 'description' => __( 'Records a donation taken outside the online checkout — a cheque, cash, or bank transfer. This does NOT charge anyone: it only creates the record. Never use it to take a card payment.', 'suredonation' ),
1052 'category' => 'suredonation',
1053 'permission_callback' => $perm_edit,
1054 'gate' => self::GATE_UPDATE,
1055 'input_schema' => [
1056 'type' => 'object',
1057 'required' => [ 'campaign_id', 'amount' ],
1058 'properties' => [
1059 'campaign_id' => [
1060 'type' => 'integer',
1061 'description' => __( 'Campaign the donation belongs to.', 'suredonation' ),
1062 ],
1063 'amount' => [
1064 'type' => 'number',
1065 'description' => __( 'Donation amount in the store currency.', 'suredonation' ),
1066 ],
1067 'donor_name' => [
1068 'type' => 'string',
1069 'default' => '',
1070 'description' => __( 'Donor name.', 'suredonation' ),
1071 ],
1072 'donor_email' => [
1073 'type' => 'string',
1074 'default' => '',
1075 'description' => __( 'Donor email address.', 'suredonation' ),
1076 ],
1077 'donor_phone' => [
1078 'type' => 'string',
1079 'default' => '',
1080 'description' => __( 'Donor phone number.', 'suredonation' ),
1081 ],
1082 'donor_comment' => [
1083 'type' => 'string',
1084 'default' => '',
1085 'description' => __( 'Comment left by the donor.', 'suredonation' ),
1086 ],
1087 'payment_status' => [
1088 'type' => 'string',
1089 'enum' => Donations::get_valid_statuses(),
1090 'default' => 'pending',
1091 'description' => __( 'Status to record. Defaults to "pending" so recording a donation does not send donor receipts or fire completion automations; pass "completed" explicitly for a gift that has already cleared.', 'suredonation' ),
1092 ],
1093 'donation_type' => [
1094 'type' => 'string',
1095 'enum' => [ 'one-time', 'recurring', 'renewal' ],
1096 'default' => 'one-time',
1097 'description' => __( 'Donation type.', 'suredonation' ),
1098 ],
1099 'gateway' => [
1100 'type' => 'string',
1101 'default' => 'offline',
1102 'description' => __( 'How the donation was taken (for example "offline").', 'suredonation' ),
1103 ],
1104 'transaction_id' => [
1105 'type' => 'string',
1106 'default' => '',
1107 'description' => __( 'External reference, such as a cheque number.', 'suredonation' ),
1108 ],
1109 'fees_covered' => [
1110 'type' => 'number',
1111 'default' => 0,
1112 'description' => __( 'Amount the donor added to cover processing fees.', 'suredonation' ),
1113 ],
1114 'is_anonymous' => [
1115 'type' => 'boolean',
1116 'default' => false,
1117 'description' => __( 'Whether the donation should be shown anonymously.', 'suredonation' ),
1118 ],
1119 ],
1120 ],
1121 'output_schema' => [
1122 'type' => 'object',
1123 'properties' => [
1124 'id' => [ 'type' => 'integer' ],
1125 'campaign_id' => [ 'type' => 'integer' ],
1126 'amount' => [ 'type' => 'number' ],
1127 'payment_status' => [ 'type' => 'string' ],
1128 'message' => [ 'type' => 'string' ],
1129 ],
1130 ],
1131 'execute_callback' => static function ( $input ) use ( $runtime ) {
1132 return $runtime->create_donation( $input );
1133 },
1134 'meta' => self::build_meta( 'write', 2.0, false, false, false ),
1135 ],
1136
1137 $ns . 'delete-donation-note' => [
1138 'label' => __( 'Delete donation note', 'suredonation' ),
1139 'description' => __( 'Permanently removes an internal note from a donation. Get the note ID from get-donation-notes first.', 'suredonation' ),
1140 'category' => 'suredonation',
1141 'permission_callback' => $perm_delete,
1142 'gate' => self::GATE_DELETE,
1143 'input_schema' => [
1144 'type' => 'object',
1145 'required' => [ 'id', 'note_id' ],
1146 'properties' => [
1147 'id' => [
1148 'type' => 'integer',
1149 'description' => __( 'The donation ID.', 'suredonation' ),
1150 ],
1151 'note_id' => [
1152 'type' => 'string',
1153 'description' => __( 'The note ID, as returned by get-donation-notes.', 'suredonation' ),
1154 ],
1155 ],
1156 ],
1157 'output_schema' => [
1158 'type' => 'object',
1159 'properties' => [
1160 'id' => [ 'type' => 'integer' ],
1161 'note_id' => [ 'type' => 'string' ],
1162 'message' => [ 'type' => 'string' ],
1163 ],
1164 ],
1165 'execute_callback' => static function ( $input ) use ( $runtime ) {
1166 return $runtime->delete_donation_note( $input );
1167 },
1168 'meta' => self::build_meta( 'delete', 3.0, false, true, false, __( 'Permanently removes the note. Confirm with the user before executing.', 'suredonation' ) ),
1169 ],
1170 ];
1171 }
1172
1173 /**
1174 * Get donor ability configurations.
1175 *
1176 * @param Runtime $runtime Runtime instance.
1177 * @param callable $perm_read Read permission closure.
1178 * @param callable $perm_edit Edit permission closure.
1179 * @return array<string, array<string, mixed>> Donor abilities.
1180 */
1181 private static function get_donor_abilities( $runtime, $perm_read, $perm_edit ) {
1182 $ns = SUREDONATION_ABILITY_API_NAMESPACE;
1183
1184 $donor_detail_schema = [
1185 'type' => 'object',
1186 'properties' => [
1187 'id' => [ 'type' => 'integer' ],
1188 'name' => [ 'type' => 'string' ],
1189 'email' => [ 'type' => 'string' ],
1190 'phone' => [ 'type' => 'string' ],
1191 'company' => [ 'type' => 'string' ],
1192 'address' => [ 'type' => 'string' ],
1193 'stripe_customer_id' => [ 'type' => 'string' ],
1194 'user_id' => [ 'type' => 'integer' ],
1195 'donor_status' => [ 'type' => 'string' ],
1196 'total_donated' => [ 'type' => 'number' ],
1197 'donation_count' => [ 'type' => 'integer' ],
1198 'largest_donation' => [ 'type' => 'number' ],
1199 'first_donation_date' => [ 'type' => 'string' ],
1200 'last_donation_date' => [ 'type' => 'string' ],
1201 'donor_tags' => [ 'type' => 'array' ],
1202 'created_at' => [ 'type' => 'string' ],
1203 'updated_at' => [ 'type' => 'string' ],
1204 ],
1205 ];
1206
1207 return [
1208 $ns . 'list-donors' => [
1209 'label' => __( 'List donors', 'suredonation' ),
1210 'description' => __( 'Returns a paginated list of donors with optional search, status filter, campaign filter, date range, and sorting.', 'suredonation' ),
1211 'category' => 'suredonation',
1212 'permission_callback' => $perm_read,
1213 'input_schema' => [
1214 'type' => 'object',
1215 'properties' => [
1216 'search' => [
1217 'type' => 'string',
1218 'default' => '',
1219 'description' => __( 'Search donors by name or email.', 'suredonation' ),
1220 ],
1221 'campaign_id' => [
1222 'type' => 'integer',
1223 'default' => 0,
1224 'description' => __( 'Only donors who gave to this campaign (0 for all campaigns).', 'suredonation' ),
1225 ],
1226 'after' => [
1227 'type' => 'string',
1228 'default' => '',
1229 'description' => __( 'Only donors whose last donation was on or after this date (YYYY-MM-DD).', 'suredonation' ),
1230 ],
1231 'before' => [
1232 'type' => 'string',
1233 'default' => '',
1234 'description' => __( 'Only donors whose last donation was on or before this date (YYYY-MM-DD).', 'suredonation' ),
1235 ],
1236 'status' => [
1237 'type' => 'string',
1238 'enum' => [ 'all', 'active', 'inactive', 'blocked' ],
1239 'default' => 'all',
1240 'description' => __( 'Filter by donor status.', 'suredonation' ),
1241 ],
1242 'sort_by' => [
1243 'type' => 'string',
1244 'enum' => [ 'id', 'created_at', 'updated_at', 'name', 'email', 'total_donated', 'donation_count', 'last_donation_date' ],
1245 'default' => 'created_at',
1246 'description' => __( 'Column to sort by.', 'suredonation' ),
1247 ],
1248 'order' => [
1249 'type' => 'string',
1250 'enum' => [ 'ASC', 'DESC' ],
1251 'default' => 'DESC',
1252 'description' => __( 'Sort direction.', 'suredonation' ),
1253 ],
1254 'page' => [
1255 'type' => 'integer',
1256 'default' => 1,
1257 'description' => __( 'Page number (1-based).', 'suredonation' ),
1258 ],
1259 'per_page' => [
1260 'type' => 'integer',
1261 'default' => 20,
1262 'description' => __( 'Results per page (max 100).', 'suredonation' ),
1263 ],
1264 ],
1265 ],
1266 'output_schema' => [
1267 'type' => 'object',
1268 'properties' => [
1269 'donors' => [
1270 'type' => 'array',
1271 'items' => [
1272 'type' => 'object',
1273 'properties' => [
1274 'id' => [ 'type' => 'integer' ],
1275 'name' => [ 'type' => 'string' ],
1276 'email' => [ 'type' => 'string' ],
1277 'phone' => [ 'type' => 'string' ],
1278 'company' => [ 'type' => 'string' ],
1279 'address' => [ 'type' => 'string' ],
1280 'stripe_customer_id' => [ 'type' => 'string' ],
1281 'donor_status' => [ 'type' => 'string' ],
1282 'total_donated' => [ 'type' => 'number' ],
1283 'donation_count' => [ 'type' => 'integer' ],
1284 'largest_donation' => [ 'type' => 'number' ],
1285 'first_donation_date' => [ 'type' => 'string' ],
1286 'last_donation_date' => [ 'type' => 'string' ],
1287 'created_at' => [ 'type' => 'string' ],
1288 ],
1289 ],
1290 ],
1291 'total' => [
1292 'type' => 'integer',
1293 'description' => __( 'Total matching donors.', 'suredonation' ),
1294 ],
1295 'total_pages' => [
1296 'type' => 'integer',
1297 'description' => __( 'Total pages.', 'suredonation' ),
1298 ],
1299 ],
1300 ],
1301 'execute_callback' => static function ( $input ) use ( $runtime ) {
1302 return $runtime->list_donors( $input );
1303 },
1304 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
1305 ],
1306
1307 $ns . 'get-donor' => [
1308 'label' => __( 'Get donor', 'suredonation' ),
1309 'description' => __( 'Returns a single donor by ID with full stats including total donated, donation count, largest donation, and donation dates.', 'suredonation' ),
1310 'category' => 'suredonation',
1311 'permission_callback' => $perm_read,
1312 'input_schema' => [
1313 'type' => 'object',
1314 'required' => [ 'id' ],
1315 'properties' => [
1316 'id' => [
1317 'type' => 'integer',
1318 'description' => __( 'The donor ID.', 'suredonation' ),
1319 ],
1320 ],
1321 ],
1322 'output_schema' => $donor_detail_schema,
1323 'execute_callback' => static function ( $input ) use ( $runtime ) {
1324 return $runtime->get_donor( $input );
1325 },
1326 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
1327 ],
1328
1329 $ns . 'get-donor-by-email' => [
1330 'label' => __( 'Get donor by email', 'suredonation' ),
1331 'description' => __( 'Looks up a donor by email address. Returns full donor details if found, or an error if no donor exists with that email.', 'suredonation' ),
1332 'category' => 'suredonation',
1333 'permission_callback' => $perm_read,
1334 'input_schema' => [
1335 'type' => 'object',
1336 'required' => [ 'email' ],
1337 'properties' => [
1338 'email' => [
1339 'type' => 'string',
1340 'description' => __( 'The donor email address.', 'suredonation' ),
1341 ],
1342 ],
1343 ],
1344 'output_schema' => $donor_detail_schema,
1345 'execute_callback' => static function ( $input ) use ( $runtime ) {
1346 return $runtime->get_donor_by_email( $input );
1347 },
1348 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
1349 ],
1350
1351 $ns . 'get-top-donors' => [
1352 'label' => __( 'Get top donors', 'suredonation' ),
1353 'description' => __( 'Returns the top active donors ranked by total donated amount. Blocked and inactive donors are excluded. Useful for identifying major supporters and generating donor reports.', 'suredonation' ),
1354 'category' => 'suredonation',
1355 'permission_callback' => $perm_read,
1356 'input_schema' => [
1357 'type' => 'object',
1358 'properties' => [
1359 'limit' => [
1360 'type' => 'integer',
1361 'default' => 10,
1362 'description' => __( 'Number of top donors to return (max 100).', 'suredonation' ),
1363 ],
1364 ],
1365 ],
1366 'output_schema' => [
1367 'type' => 'object',
1368 'properties' => [
1369 'donors' => [
1370 'type' => 'array',
1371 'items' => [
1372 'type' => 'object',
1373 'properties' => [
1374 'id' => [ 'type' => 'integer' ],
1375 'name' => [ 'type' => 'string' ],
1376 'email' => [ 'type' => 'string' ],
1377 'total_donated' => [ 'type' => 'number' ],
1378 'donation_count' => [ 'type' => 'integer' ],
1379 ],
1380 ],
1381 ],
1382 ],
1383 ],
1384 'execute_callback' => static function ( $input ) use ( $runtime ) {
1385 return $runtime->get_top_donors( $input );
1386 },
1387 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
1388 ],
1389
1390 $ns . 'get-donor-donations' => [
1391 'label' => __( 'Get donor donation history', 'suredonation' ),
1392 'description' => __( 'Returns a paginated donation history for one donor, newest first.', 'suredonation' ),
1393 'category' => 'suredonation',
1394 'permission_callback' => $perm_read,
1395 'input_schema' => [
1396 'type' => 'object',
1397 'required' => [ 'id' ],
1398 'properties' => [
1399 'id' => [
1400 'type' => 'integer',
1401 'description' => __( 'The donor ID.', 'suredonation' ),
1402 ],
1403 'page' => [
1404 'type' => 'integer',
1405 'default' => 1,
1406 'description' => __( 'Page number (1-based).', 'suredonation' ),
1407 ],
1408 'per_page' => [
1409 'type' => 'integer',
1410 'default' => 10,
1411 'description' => __( 'Results per page (max 100).', 'suredonation' ),
1412 ],
1413 ],
1414 ],
1415 'output_schema' => [
1416 'type' => 'object',
1417 'properties' => [
1418 'donor_id' => [ 'type' => 'integer' ],
1419 'donations' => [
1420 'type' => 'array',
1421 'items' => [
1422 'type' => 'object',
1423 'properties' => [
1424 'id' => [ 'type' => 'integer' ],
1425 'campaign_id' => [ 'type' => 'integer' ],
1426 'campaign_title' => [ 'type' => 'string' ],
1427 'donor_name' => [ 'type' => 'string' ],
1428 'donor_email' => [ 'type' => 'string' ],
1429 'amount' => [ 'type' => 'number' ],
1430 'currency' => [ 'type' => 'string' ],
1431 'payment_status' => [ 'type' => 'string' ],
1432 'created_at' => [ 'type' => 'string' ],
1433 ],
1434 'additionalProperties' => false,
1435 ],
1436 ],
1437 'total' => [ 'type' => 'integer' ],
1438 'total_pages' => [ 'type' => 'integer' ],
1439 ],
1440 ],
1441 'execute_callback' => static function ( $input ) use ( $runtime ) {
1442 return $runtime->get_donor_donations( $input );
1443 },
1444 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
1445 ],
1446
1447 $ns . 'update-donor' => [
1448 'label' => __( 'Update donor', 'suredonation' ),
1449 'description' => __( 'Updates a donor\'s contact details, status or tags. Only the fields you send are changed; everything else is left as-is. Donation totals are derived from donations and cannot be set here.', 'suredonation' ),
1450 'category' => 'suredonation',
1451 'permission_callback' => $perm_edit,
1452 'gate' => self::GATE_UPDATE,
1453 'input_schema' => [
1454 'type' => 'object',
1455 'required' => [ 'id' ],
1456 'properties' => [
1457 'id' => [
1458 'type' => 'integer',
1459 'description' => __( 'The donor ID.', 'suredonation' ),
1460 ],
1461 'name' => [
1462 'type' => 'string',
1463 'description' => __( 'Donor name.', 'suredonation' ),
1464 ],
1465 'email' => [
1466 'type' => 'string',
1467 'description' => __( 'Donor email address.', 'suredonation' ),
1468 ],
1469 'phone' => [
1470 'type' => 'string',
1471 'description' => __( 'Donor phone number.', 'suredonation' ),
1472 ],
1473 'company' => [
1474 'type' => 'string',
1475 'description' => __( 'Donor company.', 'suredonation' ),
1476 ],
1477 'address' => [
1478 'type' => 'string',
1479 'description' => __( 'Donor address.', 'suredonation' ),
1480 ],
1481 'donor_status' => [
1482 'type' => 'string',
1483 'enum' => [ 'active', 'inactive', 'blocked' ],
1484 'description' => __( 'Donor status. Blocked donors are excluded from top-donor reports.', 'suredonation' ),
1485 ],
1486 'donor_tags' => [
1487 'type' => 'array',
1488 'items' => [ 'type' => 'string' ],
1489 'description' => __( 'Replaces the donor\'s tags with this list.', 'suredonation' ),
1490 ],
1491 ],
1492 ],
1493 'output_schema' => [
1494 'type' => 'object',
1495 'properties' => [
1496 'id' => [ 'type' => 'integer' ],
1497 'updated' => [
1498 'type' => 'array',
1499 'items' => [ 'type' => 'string' ],
1500 'description' => __( 'Names of the fields this call changed.', 'suredonation' ),
1501 ],
1502 'donor' => [ 'type' => 'object' ],
1503 'message' => [ 'type' => 'string' ],
1504 ],
1505 ],
1506 'execute_callback' => static function ( $input ) use ( $runtime ) {
1507 return $runtime->update_donor( $input );
1508 },
1509 'meta' => self::build_meta( 'write', 2.0, false, false, true ),
1510 ],
1511 ];
1512 }
1513
1514 /**
1515 * Get form ability configurations.
1516 *
1517 * @param Runtime $runtime Runtime instance.
1518 * @param callable $perm_read Read permission closure.
1519 * @param callable $perm_edit Edit permission closure.
1520 * @param callable $perm_delete Delete permission closure.
1521 * @return array<string, array<string, mixed>> Form abilities.
1522 */
1523 private static function get_form_abilities( $runtime, $perm_read, $perm_edit, $perm_delete ) {
1524 $ns = SUREDONATION_ABILITY_API_NAMESPACE;
1525
1526 return [
1527 $ns . 'list-forms' => [
1528 'label' => __( 'List donation forms', 'suredonation' ),
1529 'description' => __( 'Returns donation forms with optional campaign filter and status filter. Forms are the front-end donation widgets linked to campaigns.', 'suredonation' ),
1530 'category' => 'suredonation',
1531 'permission_callback' => $perm_read,
1532 'input_schema' => [
1533 'type' => 'object',
1534 'properties' => [
1535 'campaign_id' => [
1536 'type' => 'integer',
1537 'default' => 0,
1538 'description' => __( 'Filter by campaign ID (0 for all campaigns).', 'suredonation' ),
1539 ],
1540 'status' => [
1541 'type' => 'string',
1542 'enum' => [ 'any', 'publish', 'draft', 'trash' ],
1543 'default' => 'any',
1544 'description' => __( 'Filter by form status.', 'suredonation' ),
1545 ],
1546 'per_page' => [
1547 'type' => 'integer',
1548 'default' => 20,
1549 'description' => __( 'Results per page (max 100).', 'suredonation' ),
1550 ],
1551 'page' => [
1552 'type' => 'integer',
1553 'default' => 1,
1554 'description' => __( 'Page number (1-based).', 'suredonation' ),
1555 ],
1556 ],
1557 ],
1558 'output_schema' => [
1559 'type' => 'object',
1560 'properties' => [
1561 'total' => [
1562 'type' => 'integer',
1563 'description' => __( 'Total matching forms.', 'suredonation' ),
1564 ],
1565 'total_pages' => [
1566 'type' => 'integer',
1567 'description' => __( 'Total pages.', 'suredonation' ),
1568 ],
1569 'forms' => [
1570 'type' => 'array',
1571 'items' => [
1572 'type' => 'object',
1573 'properties' => [
1574 'id' => [ 'type' => 'integer' ],
1575 'title' => [ 'type' => 'string' ],
1576 'status' => [ 'type' => 'string' ],
1577 'campaign_id' => [ 'type' => 'integer' ],
1578 'campaign_name' => [ 'type' => 'string' ],
1579 'entries' => [ 'type' => 'integer' ],
1580 'revenue' => [ 'type' => 'number' ],
1581 'is_default' => [ 'type' => 'boolean' ],
1582 'created_at' => [ 'type' => 'string' ],
1583 'modified_at' => [ 'type' => 'string' ],
1584 'edit_url' => [ 'type' => 'string' ],
1585 ],
1586 ],
1587 ],
1588 ],
1589 ],
1590 'execute_callback' => static function ( $input ) use ( $runtime ) {
1591 return $runtime->list_forms( $input );
1592 },
1593 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
1594 ],
1595
1596 $ns . 'get-form' => [
1597 'label' => __( 'Get donation form', 'suredonation' ),
1598 'description' => __( 'Returns a single donation form by ID with campaign association, status, and edit URL.', 'suredonation' ),
1599 'category' => 'suredonation',
1600 'permission_callback' => $perm_read,
1601 'input_schema' => [
1602 'type' => 'object',
1603 'required' => [ 'id' ],
1604 'properties' => [
1605 'id' => [
1606 'type' => 'integer',
1607 'description' => __( 'The donation form ID.', 'suredonation' ),
1608 ],
1609 ],
1610 ],
1611 'output_schema' => [
1612 'type' => 'object',
1613 'properties' => [
1614 'id' => [ 'type' => 'integer' ],
1615 'title' => [ 'type' => 'string' ],
1616 'status' => [ 'type' => 'string' ],
1617 'campaign_id' => [ 'type' => 'integer' ],
1618 'campaign_name' => [ 'type' => 'string' ],
1619 'entries' => [ 'type' => 'integer' ],
1620 'revenue' => [ 'type' => 'number' ],
1621 'is_default' => [ 'type' => 'boolean' ],
1622 'created_at' => [ 'type' => 'string' ],
1623 'modified_at' => [ 'type' => 'string' ],
1624 'edit_url' => [ 'type' => 'string' ],
1625 ],
1626 ],
1627 'execute_callback' => static function ( $input ) use ( $runtime ) {
1628 return $runtime->get_form( $input );
1629 },
1630 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
1631 ],
1632
1633 $ns . 'update-form' => [
1634 'label' => __( 'Move a form to another campaign', 'suredonation' ),
1635 'description' => __( 'Reassigns a donation form to a different campaign. Donations already recorded through the form keep their original campaign.', 'suredonation' ),
1636 'category' => 'suredonation',
1637 'permission_callback' => $perm_edit,
1638 'gate' => self::GATE_UPDATE,
1639 'input_schema' => [
1640 'type' => 'object',
1641 'required' => [ 'id', 'campaign_id' ],
1642 'properties' => [
1643 'id' => [
1644 'type' => 'integer',
1645 'description' => __( 'The donation form ID.', 'suredonation' ),
1646 ],
1647 'campaign_id' => [
1648 'type' => 'integer',
1649 'description' => __( 'Campaign to attach the form to.', 'suredonation' ),
1650 ],
1651 ],
1652 ],
1653 'output_schema' => [
1654 'type' => 'object',
1655 'properties' => [
1656 'id' => [ 'type' => 'integer' ],
1657 'title' => [ 'type' => 'string' ],
1658 'status' => [ 'type' => 'string' ],
1659 'campaign_id' => [ 'type' => 'integer' ],
1660 'campaign_name' => [ 'type' => 'string' ],
1661 'entries' => [ 'type' => 'integer' ],
1662 'revenue' => [ 'type' => 'number' ],
1663 'is_default' => [ 'type' => 'boolean' ],
1664 'created_at' => [ 'type' => 'string' ],
1665 'modified_at' => [ 'type' => 'string' ],
1666 'edit_url' => [ 'type' => 'string' ],
1667 'message' => [ 'type' => 'string' ],
1668 ],
1669 ],
1670 'execute_callback' => static function ( $input ) use ( $runtime ) {
1671 return $runtime->update_form( $input );
1672 },
1673 'meta' => self::build_meta( 'write', 2.0, false, false, true ),
1674 ],
1675
1676 $ns . 'duplicate-form' => [
1677 'label' => __( 'Duplicate donation form', 'suredonation' ),
1678 'description' => __( 'Creates a copy of a donation form, including its fields and settings.', 'suredonation' ),
1679 'category' => 'suredonation',
1680 'permission_callback' => $perm_edit,
1681 'gate' => self::GATE_UPDATE,
1682 'input_schema' => [
1683 'type' => 'object',
1684 'required' => [ 'id' ],
1685 'properties' => [
1686 'id' => [
1687 'type' => 'integer',
1688 'description' => __( 'The donation form ID to copy.', 'suredonation' ),
1689 ],
1690 ],
1691 ],
1692 'output_schema' => [
1693 'type' => 'object',
1694 'properties' => [
1695 'id' => [
1696 'type' => 'integer',
1697 'description' => __( 'The new form ID.', 'suredonation' ),
1698 ],
1699 'source_id' => [ 'type' => 'integer' ],
1700 'title' => [ 'type' => 'string' ],
1701 'message' => [ 'type' => 'string' ],
1702 ],
1703 ],
1704 'execute_callback' => static function ( $input ) use ( $runtime ) {
1705 return $runtime->duplicate_form( $input );
1706 },
1707 'meta' => self::build_meta( 'write', 2.0, false, false, false ),
1708 ],
1709
1710 $ns . 'set-default-form' => [
1711 'label' => __( 'Set a campaign\'s default form', 'suredonation' ),
1712 'description' => __( 'Chooses which donation form a campaign renders. A campaign can have several forms attached but renders only its default.', 'suredonation' ),
1713 'category' => 'suredonation',
1714 'permission_callback' => $perm_edit,
1715 'gate' => self::GATE_UPDATE,
1716 'input_schema' => [
1717 'type' => 'object',
1718 'required' => [ 'form_id', 'campaign_id' ],
1719 'properties' => [
1720 'form_id' => [
1721 'type' => 'integer',
1722 'description' => __( 'Form to make the default.', 'suredonation' ),
1723 ],
1724 'campaign_id' => [
1725 'type' => 'integer',
1726 'description' => __( 'Campaign to set it on.', 'suredonation' ),
1727 ],
1728 ],
1729 ],
1730 'output_schema' => [
1731 'type' => 'object',
1732 'properties' => [
1733 'campaign_id' => [ 'type' => 'integer' ],
1734 'default_form_id' => [ 'type' => 'integer' ],
1735 'message' => [ 'type' => 'string' ],
1736 ],
1737 ],
1738 'execute_callback' => static function ( $input ) use ( $runtime ) {
1739 return $runtime->set_default_form( $input );
1740 },
1741 'meta' => self::build_meta( 'write', 2.0, false, false, true ),
1742 ],
1743
1744 $ns . 'manage-form' => [
1745 'label' => __( 'Trash, restore or delete a form', 'suredonation' ),
1746 'description' => __( 'Moves a donation form to the trash, restores it, or deletes it permanently. Trashing is reversible; deleting is not. Donations already recorded through the form are never removed.', 'suredonation' ),
1747 'category' => 'suredonation',
1748 'permission_callback' => $perm_delete,
1749 'gate' => self::GATE_DELETE,
1750 'input_schema' => [
1751 'type' => 'object',
1752 'required' => [ 'id', 'action' ],
1753 'properties' => [
1754 'id' => [
1755 'type' => 'integer',
1756 'description' => __( 'The donation form ID.', 'suredonation' ),
1757 ],
1758 'action' => [
1759 'type' => 'string',
1760 'enum' => [ 'trash', 'restore', 'delete' ],
1761 'description' => __( 'What to do with the form. "delete" is permanent.', 'suredonation' ),
1762 ],
1763 ],
1764 ],
1765 'output_schema' => [
1766 'type' => 'object',
1767 'properties' => [
1768 'id' => [ 'type' => 'integer' ],
1769 'action' => [ 'type' => 'string' ],
1770 'post_status' => [ 'type' => 'string' ],
1771 'message' => [ 'type' => 'string' ],
1772 ],
1773 ],
1774 'execute_callback' => static function ( $input ) use ( $runtime ) {
1775 return $runtime->manage_form( $input );
1776 },
1777 'meta' => self::build_meta( 'delete', 3.0, false, true, false, 'The "delete" action is permanent and not undoable. Prefer "trash", and confirm with the user before deleting.' ),
1778 ],
1779 ];
1780 }
1781
1782 /**
1783 * Get analytics ability configurations.
1784 *
1785 * @param Runtime $runtime Runtime instance.
1786 * @param callable $perm_read Read permission closure.
1787 * @return array<string, array<string, mixed>> Analytics abilities.
1788 */
1789 private static function get_analytics_abilities( $runtime, $perm_read ) {
1790 $ns = SUREDONATION_ABILITY_API_NAMESPACE;
1791
1792 return [
1793 $ns . 'get-donation-trends' => [
1794 'label' => __( 'Get donation trends', 'suredonation' ),
1795 'description' => __( 'Returns donation trend data grouped by day, week, or month for a single currency. Supports date-range and campaign filtering; defaults to the last 30 days in the store currency. Useful for charts and analytics.', 'suredonation' ),
1796 'category' => 'suredonation',
1797 'permission_callback' => $perm_read,
1798 'input_schema' => [
1799 'type' => 'object',
1800 'properties' => [
1801 'after' => [
1802 'type' => 'string',
1803 'default' => '',
1804 'description' => __( 'Start date (YYYY-MM-DD). Empty defaults to 30 days ago.', 'suredonation' ),
1805 ],
1806 'before' => [
1807 'type' => 'string',
1808 'default' => '',
1809 'description' => __( 'End date (YYYY-MM-DD). Empty defaults to today.', 'suredonation' ),
1810 ],
1811 'group' => [
1812 'type' => 'string',
1813 'enum' => [ 'day', 'week', 'month' ],
1814 'default' => 'day',
1815 'description' => __( 'Group results by time period.', 'suredonation' ),
1816 ],
1817 'currency' => [
1818 'type' => 'string',
1819 'default' => '',
1820 'description' => __( 'Three-letter currency code to report on. Defaults to the store currency. Amounts across currencies are never summed together.', 'suredonation' ),
1821 ],
1822 'campaign_id' => [
1823 'type' => 'integer',
1824 'default' => 0,
1825 'description' => __( 'Limit to one campaign (0 for all campaigns).', 'suredonation' ),
1826 ],
1827 'payment_mode' => [
1828 'type' => 'string',
1829 'enum' => [ 'test', 'live' ],
1830 'default' => '',
1831 'description' => __( 'Report on test or live donations. Defaults to the store\'s current mode.', 'suredonation' ),
1832 ],
1833 ],
1834 ],
1835 'output_schema' => [
1836 'type' => 'object',
1837 'properties' => [
1838 'trends' => [
1839 'type' => 'array',
1840 'items' => [
1841 'type' => 'object',
1842 'properties' => [
1843 'period' => [ 'type' => 'string' ],
1844 'donation_count' => [ 'type' => 'integer' ],
1845 'total_amount' => [ 'type' => 'number' ],
1846 ],
1847 ],
1848 ],
1849 'currency' => [ 'type' => 'string' ],
1850 'after' => [
1851 'type' => 'string',
1852 'description' => __( 'Start of the window actually queried.', 'suredonation' ),
1853 ],
1854 'before' => [
1855 'type' => 'string',
1856 'description' => __( 'End of the window actually queried.', 'suredonation' ),
1857 ],
1858 'payment_mode' => [ 'type' => 'string' ],
1859 ],
1860 ],
1861 'execute_callback' => static function ( $input ) use ( $runtime ) {
1862 return $runtime->get_donation_trends( $input );
1863 },
1864 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
1865 ],
1866
1867 $ns . 'get-dashboard-stats' => [
1868 'label' => __( 'Get donation dashboard stats', 'suredonation' ),
1869 'description' => __( 'Returns the site-wide donation totals: number of donations, amount raised, unique donors, average and largest donation, and how many campaigns are published. The single best call for answering "how are we doing?".', 'suredonation' ),
1870 'category' => 'suredonation',
1871 'permission_callback' => $perm_read,
1872 'input_schema' => [
1873 'type' => 'object',
1874 'properties' => [
1875 'currency' => [
1876 'type' => 'string',
1877 'default' => '',
1878 'description' => __( 'Three-letter currency code to report on. Defaults to the store currency. Amounts across currencies are never summed together.', 'suredonation' ),
1879 ],
1880 'payment_mode' => [
1881 'type' => 'string',
1882 'enum' => [ 'test', 'live' ],
1883 'default' => '',
1884 'description' => __( 'Report on test or live donations. Defaults to the store\'s current mode. Test and live figures are never summed together.', 'suredonation' ),
1885 ],
1886 ],
1887 ],
1888 'output_schema' => [
1889 'type' => 'object',
1890 'properties' => [
1891 'total_donations' => [ 'type' => 'integer' ],
1892 'total_raised' => [ 'type' => 'number' ],
1893 'unique_donors' => [ 'type' => 'integer' ],
1894 'average_donation' => [ 'type' => 'number' ],
1895 'largest_donation' => [ 'type' => 'number' ],
1896 'published_campaigns' => [ 'type' => 'integer' ],
1897 'currency' => [ 'type' => 'string' ],
1898 'payment_mode' => [ 'type' => 'string' ],
1899 ],
1900 ],
1901 'execute_callback' => static function ( $input ) use ( $runtime ) {
1902 return $runtime->get_dashboard_stats( $input );
1903 },
1904 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
1905 ],
1906
1907 $ns . 'get-recent-donations' => [
1908 'label' => __( 'Get recent donations', 'suredonation' ),
1909 'description' => __( 'Returns the most recent donations across all campaigns, newest first.', 'suredonation' ),
1910 'category' => 'suredonation',
1911 'permission_callback' => $perm_read,
1912 'input_schema' => [
1913 'type' => 'object',
1914 'properties' => [
1915 'limit' => [
1916 'type' => 'integer',
1917 'default' => 5,
1918 'description' => __( 'How many donations to return (max 100).', 'suredonation' ),
1919 ],
1920 'currency' => [
1921 'type' => 'string',
1922 'default' => '',
1923 'description' => __( 'Three-letter currency code to report on. Defaults to the store currency. Amounts across currencies are never summed together.', 'suredonation' ),
1924 ],
1925 'payment_mode' => [
1926 'type' => 'string',
1927 'enum' => [ 'test', 'live' ],
1928 'default' => '',
1929 'description' => __( 'Report on test or live donations. Defaults to the store\'s current mode. Test and live figures are never summed together.', 'suredonation' ),
1930 ],
1931 ],
1932 ],
1933 'output_schema' => [
1934 'type' => 'object',
1935 'properties' => [
1936 'donations' => [
1937 'type' => 'array',
1938 'items' => [
1939 'type' => 'object',
1940 'properties' => [
1941 'id' => [ 'type' => 'integer' ],
1942 'campaign_id' => [ 'type' => 'integer' ],
1943 'campaign_title' => [ 'type' => 'string' ],
1944 'donor_name' => [ 'type' => 'string' ],
1945 'donor_email' => [ 'type' => 'string' ],
1946 'amount' => [ 'type' => 'number' ],
1947 'currency' => [ 'type' => 'string' ],
1948 'payment_status' => [ 'type' => 'string' ],
1949 'created_at' => [ 'type' => 'string' ],
1950 ],
1951 'additionalProperties' => false,
1952 ],
1953 ],
1954 'currency' => [ 'type' => 'string' ],
1955 'payment_mode' => [ 'type' => 'string' ],
1956 ],
1957 ],
1958 'execute_callback' => static function ( $input ) use ( $runtime ) {
1959 return $runtime->get_recent_donations( $input );
1960 },
1961 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
1962 ],
1963
1964 $ns . 'get-top-campaigns' => [
1965 'label' => __( 'Get top campaigns', 'suredonation' ),
1966 'description' => __( 'Returns the campaigns that have raised the most, ranked by amount raised.', 'suredonation' ),
1967 'category' => 'suredonation',
1968 'permission_callback' => $perm_read,
1969 'input_schema' => [
1970 'type' => 'object',
1971 'properties' => [
1972 'limit' => [
1973 'type' => 'integer',
1974 'default' => 5,
1975 'description' => __( 'How many campaigns to return (max 100).', 'suredonation' ),
1976 ],
1977 'currency' => [
1978 'type' => 'string',
1979 'default' => '',
1980 'description' => __( 'Three-letter currency code to report on. Defaults to the store currency. Amounts across currencies are never summed together.', 'suredonation' ),
1981 ],
1982 'payment_mode' => [
1983 'type' => 'string',
1984 'enum' => [ 'test', 'live' ],
1985 'default' => '',
1986 'description' => __( 'Report on test or live donations. Defaults to the store\'s current mode. Test and live figures are never summed together.', 'suredonation' ),
1987 ],
1988 ],
1989 ],
1990 'output_schema' => [
1991 'type' => 'object',
1992 'properties' => [
1993 'campaigns' => [
1994 'type' => 'array',
1995 'items' => [
1996 'type' => 'object',
1997 'properties' => [
1998 'id' => [ 'type' => 'integer' ],
1999 'title' => [ 'type' => 'string' ],
2000 'total_raised' => [ 'type' => 'number' ],
2001 'donation_count' => [ 'type' => 'integer' ],
2002 ],
2003 ],
2004 ],
2005 'currency' => [ 'type' => 'string' ],
2006 'payment_mode' => [ 'type' => 'string' ],
2007 ],
2008 ],
2009 'execute_callback' => static function ( $input ) use ( $runtime ) {
2010 return $runtime->get_top_campaigns( $input );
2011 },
2012 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
2013 ],
2014
2015 $ns . 'get-settings' => [
2016 'label' => __( 'Get donation settings', 'suredonation' ),
2017 'description' => __( 'Returns the non-sensitive store settings: currency and how its symbol is positioned, whether payments are in test or live mode, and the donor/spam options. Payment credentials and the AI settings that gate these abilities are never returned.', 'suredonation' ),
2018 'category' => 'suredonation',
2019 'permission_callback' => $perm_read,
2020 'input_schema' => [
2021 'type' => 'object',
2022 'properties' => [],
2023 ],
2024 'output_schema' => [
2025 'type' => 'object',
2026 'properties' => [
2027 'currency' => [ 'type' => 'string' ],
2028 'currency_symbol' => [ 'type' => 'string' ],
2029 'currency_sign_position' => [ 'type' => 'string' ],
2030 'payment_mode' => [
2031 'type' => 'string',
2032 'description' => __( 'test or live. Global, not per gateway.', 'suredonation' ),
2033 ],
2034 'honeypot_enabled' => [ 'type' => 'boolean' ],
2035 'create_wp_user' => [ 'type' => 'boolean' ],
2036 ],
2037 ],
2038 'execute_callback' => static function ( $input ) use ( $runtime ) {
2039 return $runtime->get_settings( $input );
2040 },
2041 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
2042 ],
2043
2044 $ns . 'get-payment-gateways' => [
2045 'label' => __( 'Get payment gateway status', 'suredonation' ),
2046 'description' => __( 'Returns which payment gateways are connected and whether the store is in test or live mode. Connection state only, never credentials. Useful for diagnosing why a donation form offers no payment options.', 'suredonation' ),
2047 'category' => 'suredonation',
2048 'permission_callback' => $perm_read,
2049 'input_schema' => [
2050 'type' => 'object',
2051 'properties' => [],
2052 ],
2053 'output_schema' => [
2054 'type' => 'object',
2055 'properties' => [
2056 'payment_mode' => [ 'type' => 'string' ],
2057 'gateways' => [
2058 'type' => 'array',
2059 'items' => [
2060 'type' => 'object',
2061 'properties' => [
2062 'id' => [ 'type' => 'string' ],
2063 'connected' => [ 'type' => 'boolean' ],
2064 ],
2065 ],
2066 ],
2067 ],
2068 ],
2069 'execute_callback' => static function ( $input ) use ( $runtime ) {
2070 return $runtime->get_payment_gateways( $input );
2071 },
2072 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
2073 ],
2074 ];
2075 }
2076 }
2077