PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
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/abilities/config-ability.php +1153 -167 1.2.0 → 1.6.1 View file →
@@ -9,8 +9,9 @@
9 9 */
10 10
11 11 namespace SureDonation\Inc\Abilities;
12 12
13 +use SureDonation\Inc\Database\Tables\Donations;
13 14 use SureDonation\Inc\Helper;
14 15
15 16 // Exit if accessed directly.
16 17 if ( ! defined( 'ABSPATH' ) ) {
@@ -23,8 +24,22 @@
23 24 * @since 0.0.1
24 25 */
25 26 class Config_Ability {
26 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 + /**
27 42 * Cached abilities.
28 43 *
29 44 * @var array<string, array<string, mixed>>|null
30 45 */
@@ -30,8 +45,52 @@
30 45 */
31 46 private static $abilities = null;
32 47
33 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 + /**
34 93 * Get all ability configurations.
35 94 *
36 95 * @return array<string, array<string, mixed>> Ability definitions.
37 96 */
@@ -39,29 +98,27 @@
39 98 if ( null !== self::$abilities ) {
40 99 return self::$abilities;
41 100 }
42 101
43 - $runtime = new Runtime();
44 - $ai_option = Helper::get_suredonation_option( 'ai_settings', [] );
45 - $ai_settings = is_array( $ai_option ) ? $ai_option : [];
102 + $runtime = new Runtime();
46 103
47 104 $perm_read = static function () use ( $runtime ) {
48 105 return $runtime->permission_callback( 'manage_options' );
49 106 };
50 107
51 - $perm_edit = static function () use ( $runtime, $ai_settings ) {
52 - return ! empty( $ai_settings['allow_updates'] ) && $runtime->permission_callback( 'manage_options' );
108 + $perm_edit = static function () use ( $runtime ) {
109 + return self::is_gate_open( self::GATE_UPDATE ) && $runtime->permission_callback( 'manage_options' );
53 110 };
54 111
55 - $perm_delete = static function () use ( $runtime, $ai_settings ) {
56 - return ! empty( $ai_settings['allow_delete'] ) && $runtime->permission_callback( 'manage_options' );
112 + $perm_delete = static function () use ( $runtime ) {
113 + return self::is_gate_open( self::GATE_DELETE ) && $runtime->permission_callback( 'manage_options' );
57 114 };
58 115
59 116 $abilities = array_merge(
60 117 self::get_campaign_abilities( $runtime, $perm_read, $perm_edit, $perm_delete ),
61 - self::get_donation_abilities( $runtime, $perm_read, $perm_edit ),
62 - self::get_donor_abilities( $runtime, $perm_read ),
63 - self::get_form_abilities( $runtime, $perm_read ),
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 ),
64 121 self::get_analytics_abilities( $runtime, $perm_read )
65 122 );
66 123
67 124 /**
@@ -109,25 +166,55 @@
109 166
110 167 /**
111 168 * Build meta block for an ability.
112 169 *
113 - * @param float $priority Priority level (1.0 read, 2.0 write, 3.0 destructive).
114 - * @param bool $read_only Whether the ability only reads data.
115 - * @param bool $destructive Whether the ability destroys data.
116 - * @param bool $idempotent Whether repeated calls produce the same result.
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.
117 194 * @return array<string, mixed> Meta configuration.
118 195 */
119 - private static function build_meta( $priority = 1.0, $read_only = true, $destructive = false, $idempotent = true ) {
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 +
120 211 return [
121 - 'annotations' => [
122 - 'priority' => $priority,
123 - 'readOnlyHint' => $read_only,
124 - 'destructiveHint' => $destructive,
125 - 'idempotentHint' => $idempotent,
126 - 'openWorldHint' => false,
127 - ],
128 - 'mcp' => [
129 - 'public' => true,
212 + 'show_in_rest' => true,
213 + 'tool_type' => $tool_type,
214 + 'annotations' => $annotations,
215 + 'mcp' => [
216 + 'public' => false,
130 217 'type' => 'tool',
131 218 ],
132 219 ];
133 220 }
@@ -159,11 +246,11 @@
159 246 'default' => '',
160 247 ],
161 248 'status' => [
162 249 'type' => 'string',
163 - 'enum' => [ 'all', 'publish', 'draft' ],
250 + 'enum' => [ 'all', 'publish', 'draft', 'trash', 'paused' ],
164 251 'default' => 'all',
165 - 'description' => __( 'Filter by post status.', 'suredonation' ),
252 + 'description' => __( 'Filter by status. "publish", "draft" and "trash" are WordPress post statuses; "paused" matches published campaigns whose campaign status is paused.', 'suredonation' ),
166 253 ],
167 254 'sort_by' => [
168 255 'type' => 'string',
169 256 'enum' => [ 'date', 'title', 'status' ],
@@ -195,18 +282,29 @@
195 282 'type' => 'array',
196 283 'items' => [
197 284 'type' => 'object',
198 285 'properties' => [
199 - 'id' => [ 'type' => 'integer' ],
200 - 'title' => [ 'type' => 'string' ],
201 - 'status' => [ 'type' => 'string' ],
202 - 'goal_type' => [ 'type' => 'string' ],
203 - 'goal' => [ 'type' => 'number' ],
204 - 'raised' => [ 'type' => 'number' ],
205 - 'donors' => [ 'type' => 'integer' ],
206 - 'progress' => [ 'type' => 'number' ],
207 - 'created_at' => [ 'type' => 'string' ],
208 - 'modified_at' => [ 'type' => 'string' ],
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' ],
209 307 ],
210 308 ],
211 309 ],
212 310 'total' => [
@@ -221,9 +319,9 @@
221 319 ],
222 320 'execute_callback' => static function ( $input ) use ( $runtime ) {
223 321 return $runtime->list_campaigns( $input );
224 322 },
225 - 'meta' => self::build_meta( 1.0, true, false, true ),
323 + 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
226 324 ],
227 325
228 326 $ns . 'get-campaign' => [
229 327 'label' => __( 'Get campaign', 'suredonation' ),
@@ -242,30 +340,41 @@
242 340 ],
243 341 'output_schema' => [
244 342 'type' => 'object',
245 343 'properties' => [
246 - 'id' => [ 'type' => 'integer' ],
247 - 'title' => [ 'type' => 'string' ],
248 - 'description' => [ 'type' => 'string' ],
249 - 'status' => [ 'type' => 'string' ],
250 - 'goal_type' => [ 'type' => 'string' ],
251 - 'goal' => [ 'type' => 'number' ],
252 - 'raised' => [ 'type' => 'number' ],
253 - 'donors' => [ 'type' => 'integer' ],
254 - 'progress' => [ 'type' => 'number' ],
255 - 'donation_count' => [ 'type' => 'integer' ],
256 - 'average_donation' => [ 'type' => 'number' ],
257 - 'largest_donation' => [ 'type' => 'number' ],
258 - 'is_goal_reached' => [ 'type' => 'boolean' ],
259 - 'require_terms' => [ 'type' => 'boolean' ],
260 - 'created_at' => [ 'type' => 'string' ],
261 - 'modified_at' => [ 'type' => 'string' ],
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' ],
262 371 ],
263 372 ],
264 373 'execute_callback' => static function ( $input ) use ( $runtime ) {
265 374 return $runtime->get_campaign( $input );
266 375 },
267 - 'meta' => self::build_meta( 1.0, true, false, true ),
376 + 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
268 377 ],
269 378
270 379 $ns . 'create-campaign' => [
271 380 'label' => __( 'Create campaign', 'suredonation' ),
@@ -275,45 +384,55 @@
275 384 'input_schema' => [
276 385 'type' => 'object',
277 386 'required' => [ 'title' ],
278 387 'properties' => [
279 - 'title' => [
388 + 'title' => [
280 389 'type' => 'string',
281 390 'description' => __( 'Campaign title.', 'suredonation' ),
282 391 ],
283 - 'description' => [
392 + 'description' => [
284 393 'type' => 'string',
285 394 'format' => 'html',
286 395 'description' => __( 'Campaign description (HTML allowed).', 'suredonation' ),
287 396 'default' => '',
288 397 ],
289 - 'goal_type' => [
398 + 'goal_type' => [
290 399 'type' => 'string',
291 400 'enum' => [ 'raised_amount', 'donation_count' ],
292 401 'default' => 'raised_amount',
293 402 'description' => __( 'Goal type: track by amount raised or donation count.', 'suredonation' ),
294 403 ],
295 - 'goal_amount' => [
404 + 'goal_amount' => [
296 405 'type' => 'number',
297 406 'description' => __( 'Goal amount (0 for no goal).', 'suredonation' ),
298 407 'default' => 0,
299 408 ],
300 - 'campaign_status' => [
409 + 'campaign_status' => [
301 410 'type' => 'string',
302 411 'enum' => [ 'active', 'paused', 'completed' ],
303 412 'default' => 'active',
304 413 'description' => __( 'Campaign status.', 'suredonation' ),
305 414 ],
306 - 'require_terms' => [
415 + 'require_terms' => [
307 416 'type' => 'boolean',
308 417 'default' => false,
309 418 'description' => __( 'Require terms acceptance before donating.', 'suredonation' ),
310 419 ],
311 - 'terms_text' => [
420 + 'terms_text' => [
312 421 'type' => 'string',
313 422 'default' => '',
314 423 'description' => __( 'Terms and conditions text.', 'suredonation' ),
315 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 + ],
316 435 ],
317 436 ],
318 437 'output_schema' => [
319 438 'type' => 'object',
@@ -329,9 +448,10 @@
329 448 ],
330 449 'execute_callback' => static function ( $input ) use ( $runtime ) {
331 450 return $runtime->create_campaign( $input );
332 451 },
333 - 'meta' => self::build_meta( 2.0, false, false, false ),
452 + 'meta' => self::build_meta( 'write', 2.0, false, false, false ),
453 + 'gate' => self::GATE_UPDATE,
334 454 ],
335 455
336 456 $ns . 'update-campaign' => [
337 457 'label' => __( 'Update campaign', 'suredonation' ),
@@ -341,43 +461,51 @@
341 461 'input_schema' => [
342 462 'type' => 'object',
343 463 'required' => [ 'id' ],
344 464 'properties' => [
345 - 'id' => [
465 + 'id' => [
346 466 'type' => 'integer',
347 467 'description' => __( 'Campaign ID to update.', 'suredonation' ),
348 468 ],
349 - 'title' => [
469 + 'title' => [
350 470 'type' => 'string',
351 471 'description' => __( 'Campaign title.', 'suredonation' ),
352 472 ],
353 - 'description' => [
473 + 'description' => [
354 474 'type' => 'string',
355 475 'format' => 'html',
356 476 'description' => __( 'Campaign description (HTML allowed).', 'suredonation' ),
357 477 ],
358 - 'goal_type' => [
478 + 'goal_type' => [
359 479 'type' => 'string',
360 480 'enum' => [ 'raised_amount', 'donation_count' ],
361 481 'description' => __( 'Goal type.', 'suredonation' ),
362 482 ],
363 - 'goal_amount' => [
483 + 'goal_amount' => [
364 484 'type' => 'number',
365 485 'description' => __( 'Goal amount.', 'suredonation' ),
366 486 ],
367 - 'campaign_status' => [
487 + 'campaign_status' => [
368 488 'type' => 'string',
369 489 'enum' => [ 'active', 'paused', 'completed' ],
370 490 'description' => __( 'Campaign status.', 'suredonation' ),
371 491 ],
372 - 'require_terms' => [
492 + 'require_terms' => [
373 493 'type' => 'boolean',
374 494 'description' => __( 'Require terms acceptance.', 'suredonation' ),
375 495 ],
376 - 'terms_text' => [
496 + 'terms_text' => [
377 497 'type' => 'string',
378 498 'description' => __( 'Terms and conditions text.', 'suredonation' ),
379 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 + ],
380 508 ],
381 509 ],
382 510 'output_schema' => [
383 511 'type' => 'object',
@@ -390,14 +518,15 @@
390 518 ],
391 519 'execute_callback' => static function ( $input ) use ( $runtime ) {
392 520 return $runtime->update_campaign( $input );
393 521 },
394 - 'meta' => self::build_meta( 2.0, false, false, false ),
522 + 'meta' => self::build_meta( 'write', 2.0, false, false, false ),
523 + 'gate' => self::GATE_UPDATE,
395 524 ],
396 525
397 526 $ns . 'delete-campaign' => [
398 527 'label' => __( 'Delete campaign', 'suredonation' ),
399 - 'description' => __( 'Permanently deletes a campaign by ID. This action cannot be undone.', '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' ),
400 529 'category' => 'suredonation',
401 530 'permission_callback' => $perm_delete,
402 531 'input_schema' => [
403 532 'type' => 'object',
@@ -411,16 +540,27 @@
411 540 ],
412 541 'output_schema' => [
413 542 'type' => 'object',
414 543 'properties' => [
415 - 'id' => [ 'type' => 'integer' ],
416 - 'message' => [ 'type' => 'string' ],
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' ],
417 556 ],
418 557 ],
419 558 'execute_callback' => static function ( $input ) use ( $runtime ) {
420 559 return $runtime->delete_campaign( $input );
421 560 },
422 - 'meta' => self::build_meta( 3.0, false, true, false ),
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,
423 563 ],
424 564
425 565 $ns . 'duplicate-campaign' => [
426 566 'label' => __( 'Duplicate campaign', 'suredonation' ),
@@ -450,9 +590,10 @@
450 590 ],
451 591 'execute_callback' => static function ( $input ) use ( $runtime ) {
452 592 return $runtime->duplicate_campaign( $input );
453 593 },
454 - 'meta' => self::build_meta( 2.0, false, false, false ),
594 + 'meta' => self::build_meta( 'write', 2.0, false, false, false ),
595 + 'gate' => self::GATE_UPDATE,
455 596 ],
456 597
457 598 $ns . 'get-campaign-form-locations' => [
458 599 'label' => __( 'Get campaign form locations', 'suredonation' ),
@@ -476,14 +617,15 @@
476 617 'type' => 'array',
477 618 'items' => [
478 619 'type' => 'object',
479 620 'properties' => [
480 - 'id' => [ 'type' => 'integer' ],
481 - 'title' => [ 'type' => 'string' ],
482 - 'type' => [ 'type' => 'string' ],
483 - 'status' => [ 'type' => 'string' ],
484 - 'edit_url' => [ 'type' => 'string' ],
485 - 'view_url' => [ 'type' => 'string' ],
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' ],
486 628 ],
487 629 ],
488 630 ],
489 631 ],
@@ -490,10 +632,47 @@
490 632 ],
491 633 'execute_callback' => static function ( $input ) use ( $runtime ) {
492 634 return $runtime->get_campaign_form_locations( $input );
493 635 },
494 - 'meta' => self::build_meta( 1.0, true, false, true ),
636 + 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
495 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 + ],
496 675 ];
497 676 }
498 677
499 678 /**
@@ -498,18 +677,19 @@
498 677
499 678 /**
500 679 * Get donation ability configurations.
501 680 *
502 - * @param Runtime $runtime Runtime instance.
503 - * @param callable $perm_read Read permission closure.
504 - * @param callable $perm_edit Edit permission closure.
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.
505 685 * @return array<string, array<string, mixed>> Donation abilities.
506 686 */
507 - private static function get_donation_abilities( $runtime, $perm_read, $perm_edit ) {
687 + private static function get_donation_abilities( $runtime, $perm_read, $perm_edit, $perm_delete ) {
508 688 $ns = SUREDONATION_ABILITY_API_NAMESPACE;
509 689
510 690 return [
511 - $ns . 'list-donations' => [
691 + $ns . 'list-donations' => [
512 692 'label' => __( 'List donations', 'suredonation' ),
513 693 'description' => __( 'Returns a paginated list of donations with optional search, status filter, campaign filter, and sorting.', 'suredonation' ),
514 694 'category' => 'suredonation',
515 695 'permission_callback' => $perm_read,
@@ -517,14 +697,17 @@
517 697 'type' => 'object',
518 698 'properties' => [
519 699 'search' => [
520 700 'type' => 'string',
521 - 'description' => __( 'Search donations by donor name or email.', 'suredonation' ),
701 + 'description' => __( 'Search donations by donor name, donor email, or transaction ID.', 'suredonation' ),
522 702 'default' => '',
523 703 ],
524 704 'status' => [
525 705 'type' => 'string',
526 - 'enum' => [ 'all', 'pending', 'processing', 'completed', 'failed', 'refunded', 'partially_refunded', 'cancelled' ],
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() ),
527 710 'default' => 'all',
528 711 'description' => __( 'Filter by payment status.', 'suredonation' ),
529 712 ],
530 713 'campaign_id' => [
@@ -533,9 +716,9 @@
533 716 'description' => __( 'Filter by campaign ID (0 for all campaigns).', 'suredonation' ),
534 717 ],
535 718 'sort_by' => [
536 719 'type' => 'string',
537 - 'enum' => [ 'created_at', 'amount', 'donor_name', 'payment_status' ],
720 + 'enum' => [ 'id', 'created_at', 'updated_at', 'amount', 'donor_name', 'donor_email', 'payment_status', 'campaign_id', 'subscription_status' ],
538 721 'default' => 'created_at',
539 722 'description' => __( 'Column to sort by.', 'suredonation' ),
540 723 ],
541 724 'order' => [
@@ -563,19 +746,23 @@
563 746 'type' => 'array',
564 747 'items' => [
565 748 'type' => 'object',
566 749 'properties' => [
567 - 'id' => [ 'type' => 'integer' ],
568 - 'campaign_id' => [ 'type' => 'integer' ],
569 - 'campaign_title' => [ 'type' => 'string' ],
570 - 'donor_name' => [ 'type' => 'string' ],
571 - 'donor_email' => [ 'type' => 'string' ],
572 - 'amount' => [ 'type' => 'number' ],
573 - 'currency' => [ 'type' => 'string' ],
574 - 'payment_status' => [ 'type' => 'string' ],
575 - 'donation_type' => [ 'type' => 'string' ],
576 - 'gateway' => [ 'type' => 'string' ],
577 - 'created_at' => [ 'type' => 'string' ],
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' ],
578 765 ],
579 766 ],
580 767 ],
581 768 'total' => [
@@ -590,12 +777,12 @@
590 777 ],
591 778 'execute_callback' => static function ( $input ) use ( $runtime ) {
592 779 return $runtime->list_donations( $input );
593 780 },
594 - 'meta' => self::build_meta( 1.0, true, false, true ),
781 + 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
595 782 ],
596 783
597 - $ns . 'get-donation' => [
784 + $ns . 'get-donation' => [
598 785 'label' => __( 'Get donation', 'suredonation' ),
599 786 'description' => __( 'Returns a single donation by ID with full details including donor info, payment data, transaction ID, and activity logs.', 'suredonation' ),
600 787 'category' => 'suredonation',
601 788 'permission_callback' => $perm_read,
@@ -611,38 +798,69 @@
611 798 ],
612 799 'output_schema' => [
613 800 'type' => 'object',
614 801 'properties' => [
615 - 'id' => [ 'type' => 'integer' ],
616 - 'campaign_id' => [ 'type' => 'integer' ],
617 - 'campaign_title' => [ 'type' => 'string' ],
618 - 'donor_id' => [ 'type' => 'integer' ],
619 - 'donor_name' => [ 'type' => 'string' ],
620 - 'donor_email' => [ 'type' => 'string' ],
621 - 'donor_phone' => [ 'type' => 'string' ],
622 - 'amount' => [ 'type' => 'number' ],
623 - 'fees_covered' => [ 'type' => 'number' ],
624 - 'refunded_amount' => [ 'type' => 'number' ],
625 - 'currency' => [ 'type' => 'string' ],
626 - 'donation_type' => [ 'type' => 'string' ],
627 - 'is_anonymous' => [ 'type' => 'boolean' ],
628 - 'donor_comment' => [ 'type' => 'string' ],
629 - 'payment_status' => [ 'type' => 'string' ],
630 - 'payment_mode' => [ 'type' => 'string' ],
631 - 'gateway' => [ 'type' => 'string' ],
632 - 'transaction_id' => [ 'type' => 'string' ],
633 - 'created_at' => [ 'type' => 'string' ],
634 - 'updated_at' => [ 'type' => 'string' ],
635 - 'logs' => [ 'type' => 'array' ],
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' ],
636 854 ],
637 855 ],
638 856 'execute_callback' => static function ( $input ) use ( $runtime ) {
639 857 return $runtime->get_donation( $input );
640 858 },
641 - 'meta' => self::build_meta( 1.0, true, false, true ),
859 + 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
642 860 ],
643 861
644 - $ns . 'get-donation-notes' => [
862 + $ns . 'get-donation-notes' => [
645 863 'label' => __( 'Get donation notes', 'suredonation' ),
646 864 'description' => __( 'Returns paginated notes for a donation. Notes are admin-added comments for internal tracking.', 'suredonation' ),
647 865 'category' => 'suredonation',
648 866 'permission_callback' => $perm_read,
@@ -673,12 +891,13 @@
673 891 'type' => 'array',
674 892 'items' => [
675 893 'type' => 'object',
676 894 'properties' => [
677 - 'id' => [ 'type' => 'string' ],
678 - 'content' => [ 'type' => 'string' ],
679 - 'author_id' => [ 'type' => 'integer' ],
680 - 'created_at' => [ 'type' => 'string' ],
895 + 'id' => [ 'type' => 'string' ],
896 + 'content' => [ 'type' => 'string' ],
897 + 'author_id' => [ 'type' => 'integer' ],
898 + 'author_name' => [ 'type' => 'string' ],
899 + 'created_at' => [ 'type' => 'string' ],
681 900 ],
682 901 ],
683 902 ],
684 903 'total' => [
@@ -693,12 +912,12 @@
693 912 ],
694 913 'execute_callback' => static function ( $input ) use ( $runtime ) {
695 914 return $runtime->get_donation_notes( $input );
696 915 },
697 - 'meta' => self::build_meta( 1.0, true, false, true ),
916 + 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
698 917 ],
699 918
700 - $ns . 'add-donation-note' => [
919 + $ns . 'add-donation-note' => [
701 920 'label' => __( 'Add donation note', 'suredonation' ),
702 921 'description' => __( 'Adds an internal note to a donation for admin tracking purposes.', 'suredonation' ),
703 922 'category' => 'suredonation',
704 923 'permission_callback' => $perm_edit,
@@ -728,10 +947,227 @@
728 947 ],
729 948 'execute_callback' => static function ( $input ) use ( $runtime ) {
730 949 return $runtime->add_donation_note( $input );
731 950 },
732 - 'meta' => self::build_meta( 2.0, false, false, false ),
951 + 'meta' => self::build_meta( 'write', 2.0, false, false, false ),
952 + 'gate' => self::GATE_UPDATE,
733 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 + ],
734 1170 ];
735 1171 }
736 1172
737 1173 /**
@@ -738,11 +1174,12 @@
738 1174 * Get donor ability configurations.
739 1175 *
740 1176 * @param Runtime $runtime Runtime instance.
741 1177 * @param callable $perm_read Read permission closure.
1178 + * @param callable $perm_edit Edit permission closure.
742 1179 * @return array<string, array<string, mixed>> Donor abilities.
743 1180 */
744 - private static function get_donor_abilities( $runtime, $perm_read ) {
1181 + private static function get_donor_abilities( $runtime, $perm_read, $perm_edit ) {
745 1182 $ns = SUREDONATION_ABILITY_API_NAMESPACE;
746 1183
747 1184 $donor_detail_schema = [
748 1185 'type' => 'object',
@@ -750,8 +1187,11 @@
750 1187 'id' => [ 'type' => 'integer' ],
751 1188 'name' => [ 'type' => 'string' ],
752 1189 'email' => [ 'type' => 'string' ],
753 1190 'phone' => [ 'type' => 'string' ],
1191 + 'company' => [ 'type' => 'string' ],
1192 + 'address' => [ 'type' => 'string' ],
1193 + 'stripe_customer_id' => [ 'type' => 'string' ],
754 1194 'user_id' => [ 'type' => 'integer' ],
755 1195 'donor_status' => [ 'type' => 'string' ],
756 1196 'total_donated' => [ 'type' => 'number' ],
757 1197 'donation_count' => [ 'type' => 'integer' ],
@@ -764,40 +1204,60 @@
764 1204 ],
765 1205 ];
766 1206
767 1207 return [
768 - $ns . 'list-donors' => [
1208 + $ns . 'list-donors' => [
769 1209 'label' => __( 'List donors', 'suredonation' ),
770 - 'description' => __( 'Returns a paginated list of donors with optional status filter and sorting by name, total donated, donation count, or date.', 'suredonation' ),
1210 + 'description' => __( 'Returns a paginated list of donors with optional search, status filter, campaign filter, date range, and sorting.', 'suredonation' ),
771 1211 'category' => 'suredonation',
772 1212 'permission_callback' => $perm_read,
773 1213 'input_schema' => [
774 1214 'type' => 'object',
775 1215 'properties' => [
776 - 'status' => [
1216 + 'search' => [
777 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',
778 1238 'enum' => [ 'all', 'active', 'inactive', 'blocked' ],
779 1239 'default' => 'all',
780 1240 'description' => __( 'Filter by donor status.', 'suredonation' ),
781 1241 ],
782 - 'sort_by' => [
1242 + 'sort_by' => [
783 1243 'type' => 'string',
784 - 'enum' => [ 'created_at', 'name', 'email', 'total_donated', 'donation_count', 'last_donation_date' ],
1244 + 'enum' => [ 'id', 'created_at', 'updated_at', 'name', 'email', 'total_donated', 'donation_count', 'last_donation_date' ],
785 1245 'default' => 'created_at',
786 1246 'description' => __( 'Column to sort by.', 'suredonation' ),
787 1247 ],
788 - 'order' => [
1248 + 'order' => [
789 1249 'type' => 'string',
790 1250 'enum' => [ 'ASC', 'DESC' ],
791 1251 'default' => 'DESC',
792 1252 'description' => __( 'Sort direction.', 'suredonation' ),
793 1253 ],
794 - 'page' => [
1254 + 'page' => [
795 1255 'type' => 'integer',
796 1256 'default' => 1,
797 1257 'description' => __( 'Page number (1-based).', 'suredonation' ),
798 1258 ],
799 - 'per_page' => [
1259 + 'per_page' => [
800 1260 'type' => 'integer',
801 1261 'default' => 20,
802 1262 'description' => __( 'Results per page (max 100).', 'suredonation' ),
803 1263 ],
@@ -814,8 +1274,11 @@
814 1274 'id' => [ 'type' => 'integer' ],
815 1275 'name' => [ 'type' => 'string' ],
816 1276 'email' => [ 'type' => 'string' ],
817 1277 'phone' => [ 'type' => 'string' ],
1278 + 'company' => [ 'type' => 'string' ],
1279 + 'address' => [ 'type' => 'string' ],
1280 + 'stripe_customer_id' => [ 'type' => 'string' ],
818 1281 'donor_status' => [ 'type' => 'string' ],
819 1282 'total_donated' => [ 'type' => 'number' ],
820 1283 'donation_count' => [ 'type' => 'integer' ],
821 1284 'largest_donation' => [ 'type' => 'number' ],
@@ -837,12 +1300,12 @@
837 1300 ],
838 1301 'execute_callback' => static function ( $input ) use ( $runtime ) {
839 1302 return $runtime->list_donors( $input );
840 1303 },
841 - 'meta' => self::build_meta( 1.0, true, false, true ),
1304 + 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
842 1305 ],
843 1306
844 - $ns . 'get-donor' => [
1307 + $ns . 'get-donor' => [
845 1308 'label' => __( 'Get donor', 'suredonation' ),
846 1309 'description' => __( 'Returns a single donor by ID with full stats including total donated, donation count, largest donation, and donation dates.', 'suredonation' ),
847 1310 'category' => 'suredonation',
848 1311 'permission_callback' => $perm_read,
@@ -859,12 +1322,12 @@
859 1322 'output_schema' => $donor_detail_schema,
860 1323 'execute_callback' => static function ( $input ) use ( $runtime ) {
861 1324 return $runtime->get_donor( $input );
862 1325 },
863 - 'meta' => self::build_meta( 1.0, true, false, true ),
1326 + 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
864 1327 ],
865 1328
866 - $ns . 'get-donor-by-email' => [
1329 + $ns . 'get-donor-by-email' => [
867 1330 'label' => __( 'Get donor by email', 'suredonation' ),
868 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' ),
869 1332 'category' => 'suredonation',
870 1333 'permission_callback' => $perm_read,
@@ -881,14 +1344,14 @@
881 1344 'output_schema' => $donor_detail_schema,
882 1345 'execute_callback' => static function ( $input ) use ( $runtime ) {
883 1346 return $runtime->get_donor_by_email( $input );
884 1347 },
885 - 'meta' => self::build_meta( 1.0, true, false, true ),
1348 + 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
886 1349 ],
887 1350
888 - $ns . 'get-top-donors' => [
1351 + $ns . 'get-top-donors' => [
889 1352 'label' => __( 'Get top donors', 'suredonation' ),
890 - 'description' => __( 'Returns top donors ranked by total donated amount. Useful for identifying major supporters and generating donor reports.', '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' ),
891 1354 'category' => 'suredonation',
892 1355 'permission_callback' => $perm_read,
893 1356 'input_schema' => [
894 1357 'type' => 'object',
@@ -920,10 +1383,132 @@
920 1383 ],
921 1384 'execute_callback' => static function ( $input ) use ( $runtime ) {
922 1385 return $runtime->get_top_donors( $input );
923 1386 },
924 - 'meta' => self::build_meta( 1.0, true, false, true ),
1387 + 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
925 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 + ],
926 1511 ];
927 1512 }
928 1513
929 1514 /**
@@ -928,17 +1513,19 @@
928 1513
929 1514 /**
930 1515 * Get form ability configurations.
931 1516 *
932 - * @param Runtime $runtime Runtime instance.
933 - * @param callable $perm_read Read permission closure.
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.
934 1521 * @return array<string, array<string, mixed>> Form abilities.
935 1522 */
936 - private static function get_form_abilities( $runtime, $perm_read ) {
1523 + private static function get_form_abilities( $runtime, $perm_read, $perm_edit, $perm_delete ) {
937 1524 $ns = SUREDONATION_ABILITY_API_NAMESPACE;
938 1525
939 1526 return [
940 - $ns . 'list-forms' => [
1527 + $ns . 'list-forms' => [
941 1528 'label' => __( 'List donation forms', 'suredonation' ),
942 1529 'description' => __( 'Returns donation forms with optional campaign filter and status filter. Forms are the front-end donation widgets linked to campaigns.', 'suredonation' ),
943 1530 'category' => 'suredonation',
944 1531 'permission_callback' => $perm_read,
@@ -960,14 +1547,27 @@
960 1547 'type' => 'integer',
961 1548 'default' => 20,
962 1549 'description' => __( 'Results per page (max 100).', 'suredonation' ),
963 1550 ],
1551 + 'page' => [
1552 + 'type' => 'integer',
1553 + 'default' => 1,
1554 + 'description' => __( 'Page number (1-based).', 'suredonation' ),
1555 + ],
964 1556 ],
965 1557 ],
966 1558 'output_schema' => [
967 1559 'type' => 'object',
968 1560 'properties' => [
969 - 'forms' => [
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' => [
970 1570 'type' => 'array',
971 1571 'items' => [
972 1572 'type' => 'object',
973 1573 'properties' => [
@@ -975,8 +1575,11 @@
975 1575 'title' => [ 'type' => 'string' ],
976 1576 'status' => [ 'type' => 'string' ],
977 1577 'campaign_id' => [ 'type' => 'integer' ],
978 1578 'campaign_name' => [ 'type' => 'string' ],
1579 + 'entries' => [ 'type' => 'integer' ],
1580 + 'revenue' => [ 'type' => 'number' ],
1581 + 'is_default' => [ 'type' => 'boolean' ],
979 1582 'created_at' => [ 'type' => 'string' ],
980 1583 'modified_at' => [ 'type' => 'string' ],
981 1584 'edit_url' => [ 'type' => 'string' ],
982 1585 ],
@@ -986,12 +1589,12 @@
986 1589 ],
987 1590 'execute_callback' => static function ( $input ) use ( $runtime ) {
988 1591 return $runtime->list_forms( $input );
989 1592 },
990 - 'meta' => self::build_meta( 1.0, true, false, true ),
1593 + 'meta' => self::build_meta( 'list', 1.0, true, false, true ),
991 1594 ],
992 1595
993 - $ns . 'get-form' => [
1596 + $ns . 'get-form' => [
994 1597 'label' => __( 'Get donation form', 'suredonation' ),
995 1598 'description' => __( 'Returns a single donation form by ID with campaign association, status, and edit URL.', 'suredonation' ),
996 1599 'category' => 'suredonation',
997 1600 'permission_callback' => $perm_read,
@@ -1012,8 +1615,11 @@
1012 1615 'title' => [ 'type' => 'string' ],
1013 1616 'status' => [ 'type' => 'string' ],
1014 1617 'campaign_id' => [ 'type' => 'integer' ],
1015 1618 'campaign_name' => [ 'type' => 'string' ],
1619 + 'entries' => [ 'type' => 'integer' ],
1620 + 'revenue' => [ 'type' => 'number' ],
1621 + 'is_default' => [ 'type' => 'boolean' ],
1016 1622 'created_at' => [ 'type' => 'string' ],
1017 1623 'modified_at' => [ 'type' => 'string' ],
1018 1624 'edit_url' => [ 'type' => 'string' ],
1019 1625 ],
@@ -1020,10 +1626,157 @@
1020 1626 ],
1021 1627 'execute_callback' => static function ( $input ) use ( $runtime ) {
1022 1628 return $runtime->get_form( $input );
1023 1629 },
1024 - 'meta' => self::build_meta( 1.0, true, false, true ),
1630 + 'meta' => self::build_meta( 'read', 1.0, true, false, true ),
1025 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 + ],
1026 1779 ];
1027 1780 }
1028 1781
1029 1782 /**
@@ -1036,32 +1789,48 @@
1036 1789 private static function get_analytics_abilities( $runtime, $perm_read ) {
1037 1790 $ns = SUREDONATION_ABILITY_API_NAMESPACE;
1038 1791
1039 1792 return [
1040 - $ns . 'get-donation-trends' => [
1793 + $ns . 'get-donation-trends' => [
1041 1794 'label' => __( 'Get donation trends', 'suredonation' ),
1042 - 'description' => __( 'Returns donation trend data grouped by day, week, or month. Supports date range filtering. Useful for charts and analytics.', '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' ),
1043 1796 'category' => 'suredonation',
1044 1797 'permission_callback' => $perm_read,
1045 1798 'input_schema' => [
1046 1799 'type' => 'object',
1047 1800 'properties' => [
1048 - 'after' => [
1801 + 'after' => [
1049 1802 'type' => 'string',
1050 1803 'default' => '',
1051 - 'description' => __( 'Start date (YYYY-MM-DD). Empty for no lower bound.', 'suredonation' ),
1804 + 'description' => __( 'Start date (YYYY-MM-DD). Empty defaults to 30 days ago.', 'suredonation' ),
1052 1805 ],
1053 - 'before' => [
1806 + 'before' => [
1054 1807 'type' => 'string',
1055 1808 'default' => '',
1056 - 'description' => __( 'End date (YYYY-MM-DD). Empty for no upper bound.', 'suredonation' ),
1809 + 'description' => __( 'End date (YYYY-MM-DD). Empty defaults to today.', 'suredonation' ),
1057 1810 ],
1058 - 'group' => [
1811 + 'group' => [
1059 1812 'type' => 'string',
1060 1813 'enum' => [ 'day', 'week', 'month' ],
1061 1814 'default' => 'day',
1062 1815 'description' => __( 'Group results by time period.', 'suredonation' ),
1063 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 + ],
1064 1833 ],
1065 1834 ],
1066 1835 'output_schema' => [
1067 1836 'type' => 'object',
@@ -1077,14 +1846,231 @@
1077 1846 ],
1078 1847 ],
1079 1848 ],
1080 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' ],
1081 1859 ],
1082 1860 ],
1083 1861 'execute_callback' => static function ( $input ) use ( $runtime ) {
1084 1862 return $runtime->get_donation_trends( $input );
1085 1863 },
1086 - 'meta' => self::build_meta( 1.0, true, false, true ),
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 ),
1087 2073 ],
1088 2074 ];
1089 2075 }
1090 2076 }