• File: DonorNotesController.php
  • Full Path: /home/bravrvjk/itiministry.org/wp-content/plugins/give/src/API/REST/V3/Routes/Donors/ValueObjects/DonorNotesController.php
  • Date Modified: 01/28/2026 8:00 PM
  • File size: 16.36 KB
  • MIME-type: text/x-php
  • Charset: utf-8
<?php

namespace Give\API\REST\V3\Routes\Donors;

use Exception;
use Give\API\REST\V3\Routes\Donors\ValueObjects\DonorRoute;
use Give\API\REST\V3\Support\Headers;
use Give\API\REST\V3\Support\Item;
use Give\Donors\Models\Donor;
use Give\Donors\Models\DonorNote;
use Give\Donors\ValueObjects\DonorNoteType;
use Give\Framework\Permissions\Facades\UserPermissions;
use WP_Error;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * @since 4.4.0
 */
class DonorNotesController extends WP_REST_Controller
{
    /**
     * @since 4.4.0
     */
    public function __construct()
    {
        $this->namespace = DonorRoute::NAMESPACE;
        $this->rest_base = DonorRoute::BASE;
    }

    /**
     * @since 4.9.0 Move schema key to the route level instead of defining it for each endpoint (which is incorrect)
     * @since 4.4.0
     */
    public function register_routes()
    {
        register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<donorId>[\d]+)/notes', [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'get_items'],
                'permission_callback' => [$this, 'get_items_permissions_check'],
                'args' => array_merge([
                    'donorId' => [
                        'description' => __('The ID of the donor this note belongs to.', 'give'),
                        'type' => 'integer',
                        'required' => true,
                    ]
                ], $this->get_collection_params()),
            ],
            [
                'methods' => WP_REST_Server::CREATABLE,
                'callback' => [$this, 'create_item'],
                'permission_callback' => [$this, 'create_item_permissions_check'],
                'args' => $this->get_endpoint_args_for_item_schema(WP_REST_Server::CREATABLE),
            ],
            'schema' => [$this, 'get_public_item_schema'],
        ]);

        register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<donorId>[\d]+)/notes/(?P<id>[\d]+)', [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'get_item'],
                'permission_callback' => [$this, 'get_item_permissions_check'],
                'args' => $this->get_endpoint_args_for_item_schema(WP_REST_Server::READABLE),
            ],
            [
                'methods' => WP_REST_Server::EDITABLE,
                'callback' => [$this, 'update_item'],
                'permission_callback' => [$this, 'update_item_permissions_check'],
                'args' => $this->get_endpoint_args_for_item_schema(WP_REST_Server::EDITABLE),
            ],
            [
                'methods' => WP_REST_Server::DELETABLE,
                'callback' => [$this, 'delete_item'],
                'permission_callback' => [$this, 'delete_item_permissions_check'],
                'args' => $this->get_endpoint_args_for_item_schema(WP_REST_Server::DELETABLE),
            ],
            'schema' => [$this, 'get_public_item_schema'],
        ]);
    }

    /**
     * Get a collection of donor notes.
     *
     * @since 4.14.0 Use Headers::addPagination() helper for pagination headers
     * @since 4.4.0
     *
     * @param WP_REST_Request $request Full data about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_items($request)
    {
        $donor = Donor::find($request->get_param('donorId'));
        if (!$donor) {
            return new WP_Error('donor_not_found', __('Donor not found', 'give'), ['status' => 404]);
        }

        $page = $request->get_param('page');
        $perPage = $request->get_param('per_page');

        $query = DonorNote::query()
            ->where('comment_parent', $donor->id)
            ->limit($perPage)
            ->offset(($page - 1) * $perPage)
            ->orderBy('createdAt', 'DESC');

        $notes = $query->getAll() ?? [];
        $notes = array_map(function ($note) use ($request) {
            $item = $this->prepare_item_for_response($note, $request);
            return $this->prepare_response_for_collection($item);
        }, $notes);

        $totalNotes = DonorNote::query()->where('comment_parent', $donor->id)->count();
        $routeBase = sprintf('%s/%s/%d/notes', $this->namespace, $this->rest_base, $donor->id);

        $response = rest_ensure_response($notes);
        $response = Headers::addPagination($response, $request, $totalNotes, $perPage, $routeBase);

        return $response;
    }

    /**
     * Create a donor note.
     *
     * @since 4.7.0 Add support updating custom fields
     * @since 4.4.0
     *
     * @param WP_REST_Request $request Full data about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     *
     * @throws Exception
     */
    public function create_item($request)
    {
        $donor = Donor::find($request->get_param('donorId'));
        if (!$donor) {
            return new WP_Error('donor_not_found', __('Donor not found', 'give'), ['status' => 404]);
        }

        $note = DonorNote::create([
            'donorId' => $donor->id,
            'content' => $request->get_param('content'),
            'type' => new DonorNoteType($request->get_param('type')),
        ]);

        $response = $this->prepare_item_for_response($note, $request);
        $response->set_status(201);

        return rest_ensure_response($response);
    }

    /**
     * Get a single donor note.
     *
     * @since 4.4.0
     *
     * @param WP_REST_Request $request Full data about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_item($request)
    {
        $donor = Donor::find($request->get_param('donorId'));
        if (!$donor) {
            return new WP_Error('donor_not_found', __('Donor not found', 'give'), ['status' => 404]);
        }

        $note = DonorNote::find($request->get_param('id'));
        if (!$note || $note->donorId !== $donor->id) {
            return new WP_Error('note_not_found', __('Note not found', 'give'), ['status' => 404]);
        }

        $response = $this->prepare_item_for_response($note, $request);
        return rest_ensure_response($response);
    }

    /**
     * Update a donor note.
     *
     * @since 4.7.0 Add support for updating custom fields
     * @since 4.4.0
     *
     * @param WP_REST_Request $request Full data about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     *
     * @throws Exception
     */
    public function update_item($request)
    {
        $donor = Donor::find($request->get_param('donorId'));
        if (!$donor) {
            return new WP_Error('donor_not_found', __('Donor not found', 'give'), ['status' => 404]);
        }

        $note = DonorNote::find($request->get_param('id'));
        if (!$note || $note->donorId !== $donor->id) {
            return new WP_Error('note_not_found', __('Note not found', 'give'), ['status' => 404]);
        }

        if ($request->has_param('content')) {
            $note->content = $request->get_param('content');
        }

        if ($request->has_param('type')) {
            $note->type = new DonorNoteType($request->get_param('type'));
        }

        if ($note->isDirty()) {
            $note->save();
        }

        $fieldsUpdate = $this->update_additional_fields_for_object($note, $request);
        if (is_wp_error($fieldsUpdate)) {
            return $fieldsUpdate;
        }

        $response = $this->prepare_item_for_response($note, $request);
        return rest_ensure_response($response);
    }

    /**
     * Delete a donor note.
     *
     * @since 4.4.0
     *
     * @param WP_REST_Request $request Full data about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     *
     * @throws Exception
     */
    public function delete_item($request)
    {
        $donor = Donor::find($request->get_param('donorId'));
        if (!$donor) {
            return new WP_Error('donor_not_found', __('Donor not found', 'give'), ['status' => 404]);
        }

        $note = DonorNote::find($request->get_param('id'));
        if (!$note || $note->donorId !== $donor->id) {
            return new WP_Error('note_not_found', __('Note not found', 'give'), ['status' => 404]);
        }

        // Store the note data before deletion for the response
        $noteData = $note->toArray();

        $note->delete();

        $response = new WP_REST_Response($noteData);

        return $response;
    }

    /**
     * @since 4.14.0 update permission capability to use facade
     * @since 4.4.0
     */
    public function get_items_permissions_check($request): bool
    {
        return UserPermissions::donors()->canView();
    }

    /**
     * @since 4.14.0 replace logic with UserPermissions facade
     * @since 4.4.0
     */
    public function create_item_permissions_check($request): bool
    {
        return UserPermissions::donors()->canCreate();
    }

    /**
     * @since 4.14.0 replace logic with UserPermissions facade
     * @since 4.4.0
     */
    public function get_item_permissions_check($request): bool
    {
        return UserPermissions::donors()->canView();
    }

    /**
     * @since 4.14.0 replace logic with UserPermissions facade
     * @since 4.4.0
     */
    public function update_item_permissions_check($request): bool
    {
        return UserPermissions::donors()->canEdit();
    }

    /**
     * @since 4.14.0 replace logic with UserPermissions facade
     * @since 4.4.0
     */
    public function delete_item_permissions_check($request): bool
    {
        return UserPermissions::donors()->canDelete();
    }

    /**
     * @since 4.14.0 Format dates as strings using Item::formatDatesForResponse
     * @since 4.7.0 Add support for adding custom fields to the response
     * @since 4.4.0
     */
    public function prepare_item_for_response($note, $request): WP_REST_Response
    {
        $self_url = rest_url(sprintf(
            '%s/%s/%d/notes/%d',
            $this->namespace,
            $this->rest_base,
            $note->donorId,
            $note->id
        ));

        $links = [
            'self' => ['href' => $self_url],
        ];

        $item = $note->toArray();
        $response = new WP_REST_Response(Item::formatDatesForResponse($item, ['createdAt', 'updatedAt']));
        $response->add_links($links);
        $response->data = $this->add_additional_fields_to_object($response->data, $request);

        return $response;
    }

    /**
     * @since 4.4.0
     */
    public function get_collection_params(): array
    {
        $params = parent::get_collection_params();

        $params['page']['default'] = 1;
        $params['per_page']['default'] = 30;

        // Remove default parameters not being used
        unset($params['context']);
        unset($params['search']);

        return $params;
    }

    /**
     * Get the donor note schema, conforming to JSON Schema.
     *
     * @since 4.14.0 Add date format examples
     * @since 4.13.0 add schema description
     * @since 4.9.0 Set proper JSON Schema version
     * @since 4.7.0 Change title to givewp/donor-note and add custom fields schema
     * @since 4.4.0
     *
     * @return array
     */
    public function get_item_schema(): array
    {
        $schema = [
            '$schema' => 'http://json-schema.org/draft-04/schema#',
            'title' => 'givewp/donor-note',
            'description' => esc_html__('Donor Note routes for CRUD operations', 'give'),
            'type' => 'object',
            'properties' => [
                'id' => [
                    'description' => __('Unique identifier for the note.', 'give'),
                    'type' => 'integer',
                    'readonly' => true,
                ],
                'donorId' => [
                    'description' => __('The ID of the donor this note belongs to.', 'give'),
                    'type' => 'integer',
                    'required' => true,
                ],
                'content' => [
                    'description' => __('The content of the note.', 'give'),
                    'type' => 'string',
                    'required' => true,
                    'minLength' => 1,
                ],
                'type' => [
                    'description' => __('The type of the note.', 'give'),
                    'type' => 'string',
                    'enum' => ['admin', 'donor'],
                    'default' => 'admin',
                ],
                'createdAt' => [
                    'description' => sprintf(
                        /* translators: %s: WordPress documentation URL */
                        esc_html__('The date the note was created in ISO 8601 format. Follows WordPress REST API date format standards. See %s for more information.', 'give'),
                        '<a href="https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#format" target="_blank">WordPress REST API Date and Time</a>'
                    ),
                    'type' => ['string', 'null'],
                    'format' => 'date-time',
                    'example' => '2025-09-02T20:27:02',
                    'readonly' => true,
                ],
                'updatedAt' => [
                    'description' => sprintf(
                        /* translators: %s: WordPress documentation URL */
                        esc_html__('The date the note was last updated in ISO 8601 format. Follows WordPress REST API date format standards. See %s for more information.', 'give'),
                        '<a href="https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#format" target="_blank">WordPress REST API Date and Time</a>'
                    ),
                    'type' => ['string', 'null'],
                    'format' => 'date-time',
                    'example' => '2025-09-02T20:27:02',
                    'readonly' => true,
                ],
            ],
        ];

        return $this->add_additional_fields_schema($schema);
    }

    /**
     * Get the donor note schema for public display.
     *
     * @since 4.4.0
     *
     * @return array
     */
    public function get_public_item_schema(): array
    {
        $schema = $this->get_item_schema();

        // Add additional properties for public display
        $schema['properties']['_links'] = [
            'description' => __('HATEOAS links for the note.', 'give'),
            'type' => 'object',
            'readonly' => true,
        ];

        return $schema;
    }

    /**
     * @since 4.6.0 Add format to content argument
     * @since 4.4.0
     */
    public function get_endpoint_args_for_item_schema($method = WP_REST_Server::CREATABLE): array
    {
        $args = parent::get_endpoint_args_for_item_schema($method);
        $schema = $this->get_item_schema();

        // Common argument for all endpoints
        $args['donorId'] = $schema['properties']['donorId'];
        $args['donorId']['in'] = 'path';

        // Arguments for single item endpoints (not for POST)
        if (in_array($method, [WP_REST_Server::READABLE, WP_REST_Server::EDITABLE, WP_REST_Server::DELETABLE], true)) {
            $args['id'] = [
                'description' => __('The note ID.', 'give'),
                'type' => 'integer',
                'required' => true,
                'in' => 'path',
            ];
        } else {
            // Remove id if present (for POST)
            unset($args['id']);
        }

        // Arguments for create/update endpoints
        if (in_array($method, [WP_REST_Server::CREATABLE, WP_REST_Server::EDITABLE], true)) {
            $args['content'] = [
                'description' => __('The content of the note.', 'give'),
                'type' => 'string',
                'required' => $method === WP_REST_Server::CREATABLE,
                'minLength' => 1,
                'format' => 'text-field',
            ];

            $args['type'] = [
                'description' => __('The type of the note.', 'give'),
                'type' => 'string',
                'required' => $method === WP_REST_Server::CREATABLE,
                'enum' => ['admin', 'donor'],
                'default' => 'admin',
            ];
        } else {
            unset($args['content']);
            unset($args['type']);
        }

        return $args;
    }
}