<?php
namespace Give\API\REST\V3\Routes\Donations;
use Exception;
use Give\API\REST\V3\Routes\Donations\DataTransferObjects\DonationCreateData;
use Give\API\REST\V3\Routes\Donations\Exceptions\DonationValidationException;
use Give\API\REST\V3\Routes\Donations\Fields\DonationFields;
use Give\API\REST\V3\Routes\Donations\ValueObjects\DonationAnonymousMode;
use Give\API\REST\V3\Routes\Donations\ValueObjects\DonationRoute;
use Give\API\REST\V3\Support\CURIE;
use Give\API\REST\V3\Support\Item;
use Give\API\REST\V3\Support\Schema\SchemaTypes;
use Give\Donations\Models\Donation;
use Give\Donations\ValueObjects\DonationMode;
use Give\Donations\ValueObjects\DonationStatus;
use Give\Donations\ValueObjects\DonationType;
use Give\API\REST\V3\Routes\Donations\ViewModels\DonationViewModel;
use Give\Framework\PaymentGateways\CommandHandlers\PaymentRefundedHandler;
use Give\Framework\PaymentGateways\Commands\PaymentRefunded;
use Give\Framework\PaymentGateways\Contracts\PaymentGatewayRefundable;
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.6.0
*/
class DonationController extends WP_REST_Controller
{
/**
* @var string
*/
protected $namespace;
/**
* @var string
*/
protected $rest_base;
/**
* @since 4.6.0
*/
public function __construct()
{
$this->namespace = DonationRoute::NAMESPACE;
$this->rest_base = DonationRoute::BASE;
}
/**
*
* @since 4.14.0 replaced permissionsCheck with get_item_permissions_check and get_items_permissions_check
* @since 4.9.0 Move schema key to the route level instead of defining it for each endpoint (which is incorrect)
* @since 4.6.0
*/
public function register_routes()
{
register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_item'],
'permission_callback' => [$this, 'get_item_permissions_check'],
'args' => [
'_embed' => [
'description' => __(
'Whether to embed related resources in the response. It can be true when we want to embed all available resources, or a string like "givewp:donor" when we wish to embed only a specific one.',
'give'
),
'type' => [
'string',
'boolean',
],
'default' => false,
],
'id' => [
'type' => 'integer',
'required' => true,
],
'includeSensitiveData' => [
'type' => 'boolean',
'default' => false,
],
'anonymousDonations' => [
'type' => 'string',
'default' => 'exclude',
'enum' => [
'exclude',
'include',
'redact',
],
],
],
],
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$this, 'update_item'],
'permission_callback' => [$this, 'update_item_permissions_check'],
'args' => rest_get_endpoint_args_for_schema($this->get_item_schema(), WP_REST_Server::EDITABLE),
],
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [$this, 'delete_item'],
'permission_callback' => [$this, 'delete_item_permissions_check'],
'args' => [
'id' => [
'type' => 'integer',
'required' => true,
],
'force' => [
'type' => 'boolean',
'default' => false,
'description' => 'Whether to permanently delete (force=true) or move to trash (force=false, default).',
],
],
],
'schema' => [$this, 'get_public_item_schema'],
]);
register_rest_route($this->namespace, '/' . $this->rest_base, [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_items'],
'permission_callback' => [$this, 'get_items_permissions_check'],
'args' => $this->get_collection_params(),
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'create_item'],
'permission_callback' => [$this, 'create_item_permissions_check'],
'args' => rest_get_endpoint_args_for_schema($this->get_item_schema(), WP_REST_Server::CREATABLE),
],
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [$this, 'delete_items'],
'permission_callback' => [$this, 'delete_items_permissions_check'],
'args' => [
'ids' => [
'description' => __('Array of donation IDs to delete', 'give'),
'type' => 'array',
'items' => [
'type' => 'integer',
],
'required' => true,
],
'force' => [
'type' => 'boolean',
'default' => false,
'description' => 'Whether to permanently delete (force=true) or move to trash (force=false, default).',
],
],
],
'schema' => [$this, 'get_public_item_schema'],
]);
register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/refund', [
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$this, 'refund_item'],
'permission_callback' => [$this, 'refund_item_permissions_check'],
'args' => [
'id' => [
'type' => 'integer',
'required' => true,
],
],
],
'schema' => [$this, 'get_public_item_schema'],
]);
}
/**
* @since 4.6.0
*/
public function get_items($request)
{
$includeSensitiveData = $request->get_param('includeSensitiveData');
$donationAnonymousMode = new DonationAnonymousMode($request->get_param('anonymousDonations'));
$page = $request->get_param('page');
$perPage = $request->get_param('per_page');
$sortColumn = $this->getSortColumn($request->get_param('sort'));
$sortDirection = $request->get_param('direction');
$mode = $request->get_param('mode');
$status = $request->get_param('status');
$query = Donation::query();
if ($campaignId = $request->get_param('campaignId')) {
// Filter by CampaignId
$query->where('give_donationmeta_attach_meta_campaignId.meta_value', $campaignId);
}
if ($donorId = $request->get_param('donorId')) {
$query->where('give_donationmeta_attach_meta_donorId.meta_value', $donorId);
}
if ($subscriptionId = $request->get_param('subscriptionId')) {
$query->where('give_donationmeta_attach_meta_subscriptionId.meta_value', $subscriptionId);
}
if ($donationAnonymousMode->isExcluded()) {
// Exclude anonymous donations from results
$query->where('give_donationmeta_attach_meta_anonymous.meta_value', 0);
}
// Include only current payment "mode"
$query->where('give_donationmeta_attach_meta_mode.meta_value', $mode);
// Filter by status if not 'any'
if (!in_array('any', (array)$status, true)) {
$query->whereIn('post_status', (array)$status);
}
$query
->limit($perPage)
->offset(($page - 1) * $perPage)
->orderBy($sortColumn, $sortDirection);
$donations = $query->getAll() ?? [];
$donations = array_map(function ($donation) use ($includeSensitiveData, $donationAnonymousMode, $request) {
$item = (new DonationViewModel($donation))
->anonymousMode($donationAnonymousMode)
->includeSensitiveData($includeSensitiveData)
->exports();
return $this->prepare_response_for_collection(
$this->prepare_item_for_response($item, $request)
);
}, $donations);
$totalDonations = empty($donations) ? 0 : Donation::query()->count();
$totalPages = (int)ceil($totalDonations / $perPage);
$response = rest_ensure_response($donations);
$response->header('X-WP-Total', $totalDonations);
$response->header('X-WP-TotalPages', $totalPages);
$base = add_query_arg(
map_deep($request->get_query_params(), function ($value) {
if (is_bool($value)) {
$value = $value ? 'true' : 'false';
}
return urlencode($value);
}),
rest_url(DonationRoute::BASE)
);
if ($page > 1) {
$prevPage = $page - 1;
if ($prevPage > $totalPages) {
$prevPage = $totalPages;
}
$response->link_header('prev', add_query_arg('page', $prevPage, $base));
}
if ($totalPages > $page) {
$nextPage = $page + 1;
$response->link_header('next', add_query_arg('page', $nextPage, $base));
}
return $response;
}
/**
* @since 4.6.0
*/
public function get_item($request)
{
$donation = Donation::find($request->get_param('id'));
$includeSensitiveData = $request->get_param('includeSensitiveData');
$donationAnonymousMode = new DonationAnonymousMode($request->get_param('anonymousDonations'));
if (!$donation || ($donation->anonymous && $donationAnonymousMode->isExcluded())) {
return new WP_Error('donation_not_found', __('Donation not found', 'give'), ['status' => 404]);
}
$item = (new DonationViewModel($donation))
->anonymousMode($donationAnonymousMode)
->includeSensitiveData($includeSensitiveData)
->exports();
$response = $this->prepare_item_for_response($item, $request);
return rest_ensure_response($response);
}
/**
* Create a single donation.
*
* @since 4.8.0
*/
public function create_item($request): WP_REST_Response
{
try {
$data = DonationCreateData::fromRequest($request);
$donation = $data->isRenewal() ? $data->createRenewal() : $data->createDonation();
$item = (new DonationViewModel($donation))
->includeSensitiveData(true)
->anonymousMode(new DonationAnonymousMode('include'))
->exports();
$response = $this->prepare_item_for_response($item, $request);
$response->set_status(201);
return rest_ensure_response($response);
} catch (DonationValidationException $e) {
return new WP_REST_Response([
'message' => $e->getMessage(),
'error' => $e->getErrorCode()
], $e->getStatusCode());
} catch (\Exception $e) {
return new WP_REST_Response([
'message' => __('Failed to create donation', 'give'),
'error' => $e->getMessage()
], 400);
}
}
/**
* Update a single donation.
*
* @since 4.7.0 Add support for updating custom fields
* @since 4.6.0
*
* @return WP_REST_Response|WP_Error
*/
public function update_item($request)
{
$donation = Donation::find($request->get_param('id'));
if (!$donation) {
return new WP_REST_Response(__('Donation not found', 'give'), 404);
}
$nonEditableFields = [
'id',
'updatedAt',
'purchaseKey',
'donorIp',
'type',
'mode',
'gatewayTransactionId',
];
foreach ($request->get_params() as $key => $value) {
if (!in_array($key, $nonEditableFields, true)) {
if (in_array($key, $donation::propertyKeys(), true)) {
try {
$processedValue = DonationFields::processValue($key, $value);
if ($donation->isPropertyTypeValid($key, $processedValue)) {
$donation->$key = $processedValue;
}
} catch (Exception $e) {
continue;
}
}
}
}
if ($donation->isDirty()) {
$donation->save();
}
$item = (new DonationViewModel($donation))
->includeSensitiveData(true)
->anonymousMode(new DonationAnonymousMode('include'))
->exports();
$fieldsUpdate = $this->update_additional_fields_for_object($item, $request);
if (is_wp_error($fieldsUpdate)) {
return $fieldsUpdate;
}
$response = $this->prepare_item_for_response($item, $request);
return rest_ensure_response($response);
}
/**
* Refund a single donation.
*
* @since 4.6.0
*/
public function refund_item($request)
{
$donation = Donation::find($request->get_param('id'));
if (!$donation) {
return new WP_REST_Response(__('Donation not found', 'give'), 404);
}
$gateway = $donation->gateway();
if (!$gateway->supportsRefund()) {
return new WP_REST_Response(__('Refunds are not supported for this gateway', 'give'), 400);
}
try {
/** @var PaymentGatewayRefundable $gateway */
$command = $gateway->refundDonation($donation);
if ($command instanceof PaymentRefunded) {
$handler = new PaymentRefundedHandler($command);
$handler->handle($donation);
}
$response = $this->prepare_item_for_response($donation->toArray(), $request);
return rest_ensure_response($response);
} catch (\Exception $exception) {
return new WP_REST_Response(__('Failed to refund donation', 'give'), 500);
}
}
/**
* Delete a single donation.
*
* @since 4.6.0
*/
public function delete_item($request): WP_REST_Response
{
$donation = Donation::find($request->get_param('id'));
$force = $request->get_param('force');
if (!$donation) {
return new WP_REST_Response(['message' => __('Donation not found', 'give')], 404);
}
$item = (new DonationViewModel($donation))
->includeSensitiveData(true)
->anonymousMode(new DonationAnonymousMode('include'))
->exports();
if ($force) {
// Permanently delete the donation
$deleted = $donation->delete();
if (!$deleted) {
return new WP_REST_Response(['message' => __('Failed to delete donation', 'give')], 500);
}
} else {
// Move the donation to trash (soft delete)
$trashed = $donation->trash();
if (!$trashed) {
return new WP_REST_Response(['message' => __('Failed to trash donation', 'give')], 500);
}
}
return new WP_REST_Response(['deleted' => true, 'previous' => $item], 200);
}
/**
* Delete multiple donations.
*
* @since 4.6.0
*/
public function delete_items($request): WP_REST_Response
{
$ids = $request->get_param('ids');
$force = $request->get_param('force');
$deleted = [];
$errors = [];
foreach ($ids as $id) {
$donation = Donation::find($id);
if (!$donation) {
$errors[] = ['id' => $id, 'message' => __('Donation not found', 'give')];
continue;
}
$item = (new DonationViewModel($donation))
->includeSensitiveData(true)
->anonymousMode(new DonationAnonymousMode('include'))
->exports();
if ($force) {
if ($donation->delete()) {
$deleted[] = ['id' => $id, 'previous' => $item];
} else {
$errors[] = ['id' => $id, 'message' => __('Failed to delete donation', 'give')];
}
} else {
$trashed = $donation->trash();
if ($trashed) {
$deleted[] = ['id' => $id, 'previous' => $item];
} else {
$errors[] = ['id' => $id, 'message' => __('Failed to trash donation', 'give')];
}
}
}
return new WP_REST_Response([
'deleted' => $deleted,
'errors' => $errors,
'total_requested' => count($ids),
'total_deleted' => count($deleted),
'total_errors' => count($errors),
], 200);
}
/**
* @since 4.13.0 updated the amount sort columns to CAST as DECIMAL
* @since 4.6.0
*/
public function getSortColumn(string $sortColumn): string
{
$sortColumnsMap = [
'id' => 'ID',
'createdAt' => 'post_date',
'updatedAt' => 'post_modified',
'status' => 'post_status',
'amount' => 'CAST(give_donationmeta_attach_meta_amount.meta_value AS DECIMAL(10, 2))',
'feeAmountRecovered' => 'CAST(give_donationmeta_attach_meta_feeAmountRecovered.meta_value AS DECIMAL(10, 2))',
'donorId' => 'give_donationmeta_attach_meta_donorId.meta_value',
'firstName' => 'give_donationmeta_attach_meta_firstName.meta_value',
'lastName' => 'give_donationmeta_attach_meta_lastName.meta_value',
];
return $sortColumnsMap[$sortColumn];
}
/**
* @since 4.6.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']);
$params += [
'sort' => [
'type' => 'string',
'default' => 'id',
'enum' => [
'id',
'createdAt',
'updatedAt',
'status',
'amount',
'feeAmountRecovered',
'donorId',
'firstName',
'lastName',
],
],
'direction' => [
'type' => 'string',
'default' => 'DESC',
'enum' => ['ASC', 'DESC'],
],
'mode' => [
'type' => 'string',
'default' => 'live',
'enum' => ['live', 'test'],
],
'status' => [
'type' => 'array',
'items' => [
'type' => 'string',
'enum' => [
'any',
'publish',
'give_subscription',
'pending',
'processing',
'refunded',
'revoked',
'failed',
'cancelled',
'abandoned',
'preapproval',
],
],
'default' => ['any'],
],
'campaignId' => [
'type' => 'integer',
'default' => 0,
],
'donorId' => [
'type' => 'integer',
'default' => 0,
],
'subscriptionId' => [
'type' => 'integer',
'default' => 0,
],
'includeSensitiveData' => [
'type' => 'boolean',
'default' => false,
],
'anonymousDonations' => [
'type' => 'string',
'default' => 'exclude',
'enum' => [
'exclude',
'include',
'redact',
],
],
'force' => [
'type' => 'boolean',
'default' => false,
'description' => 'Whether to permanently delete (force=true) or move to trash (force=false, default).',
],
];
return $params;
}
/**
* @since 4.13.0 updated embeddable links
* @since 4.7.0 Add support for adding custom fields to the response
* @since 4.6.0
* @throws Exception
*/
public function prepare_item_for_response($item, $request): WP_REST_Response
{
$donationId = $request->get_param('id') ?? $item['id'] ?? null;
if ($donationId && $donation = Donation::find($donationId)) {
$self_url = rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $donationId));
$links = [
'self' => ['href' => $self_url]
];
if (!empty($item['donorId'])) {
$donor_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'donors', $item['donorId']));
$donor_url = add_query_arg([
'mode' => $request->get_param('mode'),
'includeSensitiveData' => $request->get_param('includeSensitiveData'),
'anonymousDonors' => $request->get_param('anonymousDonations'),
], $donor_url);
$links[CURIE::relationUrl('donor')] = [
'href' => $donor_url,
'embeddable' => true,
];
}
if (!empty($item['campaignId'])) {
$campaign_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'campaigns', $item['campaignId']));
$campaign_url = add_query_arg([
'mode' => $request->get_param('mode'),
], $campaign_url);
$links[CURIE::relationUrl('campaign')] = [
'href' => $campaign_url,
'embeddable' => true,
];
}
if (!empty($item['formId'])) {
$form_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'forms', $item['formId']));
$form_url = add_query_arg([
'mode' => $request->get_param('mode'),
], $form_url);
$links[CURIE::relationUrl('form')] = [
'href' => $form_url,
'embeddable' => true,
];
}
// Add subscription link when subscriptionId is greater than 0
if (isset($item['subscriptionId']) && $item['subscriptionId'] > 0) {
$subscription_url = rest_url(sprintf('%s/%s/%d', $this->namespace, 'subscriptions', $item['subscriptionId']));
$links[CURIE::relationUrl('subscription')] = [
'href' => $subscription_url,
'embeddable' => true,
];
}
} else {
$links = [];
}
$responseItem = Item::formatDatesForResponse(
$item,
['createdAt', 'updatedAt']
);
$response = new WP_REST_Response($responseItem);
if (!empty($links)) {
$response->add_links($links);
}
$response->data = $this->add_additional_fields_to_object($response->data, $request);
return $response;
}
/**
* @since 4.14.0
*/
public function get_item_permissions_check($request)
{
return $this->validationForGetMethods($request);
}
/**
* @since 4.14.0
*/
public function get_items_permissions_check($request)
{
return $this->validationForGetMethods($request);
}
/**
* @since 4.14.0 update method name to validationForGetMethods, replace logic with UserPermissions facade and add canViewDonations check
* @since 4.6.0
*/
public function validationForGetMethods(WP_REST_Request $request)
{
$includeSensitiveData = $request->get_param('includeSensitiveData');
$includeAnonymousDonations = $request->get_param('anonymousDonations');
$canViewDonations = UserPermissions::donations()->canView();
if ($includeSensitiveData && !$canViewDonations) {
return new WP_Error(
'rest_forbidden',
__('You do not have permission to include sensitive data.', 'give'),
['status' => $this->authorizationStatusCode()]
);
}
if ($includeAnonymousDonations !== null) {
$anonymousMode = new DonationAnonymousMode($includeAnonymousDonations);
if ($anonymousMode->isIncluded() && !$canViewDonations) {
return new WP_Error(
'rest_forbidden',
__('You do not have permission to include anonymous donations.', 'give'),
['status' => $this->authorizationStatusCode()]
);
}
}
return true;
}
/**
* @since 4.6.0
*/
public function update_item_permissions_check($request)
{
if ($this->canEditDonations()) {
return true;
}
return new WP_Error(
'rest_forbidden',
__('You do not have permission to update donations.', 'give'),
['status' => $this->authorizationStatusCode()]
);
}
/**
* @since 4.6.0
*/
public function create_item_permissions_check($request)
{
if ($this->canEditDonations()) {
return true;
}
return new WP_Error(
'rest_forbidden',
__('You do not have permission to create donations.', 'give'),
['status' => $this->authorizationStatusCode()]
);
}
/**
* @since 4.8.0
*/
public function delete_item_permissions_check($request)
{
if ($this->canDeleteDonations()) {
return true;
}
return new WP_Error(
'rest_forbidden',
__('You do not have permission to delete donations.', 'give'),
['status' => $this->authorizationStatusCode()]
);
}
/**
* @since 4.6.0
*/
public function delete_items_permissions_check($request)
{
if ($this->canDeleteDonations()) {
return true;
}
return new WP_Error(
'rest_forbidden',
__('You do not have permission to delete donations.', 'give'),
['status' => $this->authorizationStatusCode()]
);
}
/**
* @since 4.6.0
*/
public function refund_item_permissions_check($request)
{
if ($this->canRefundDonations()) {
return true;
}
return new WP_Error(
'rest_forbidden',
__('You do not have permission to refund donations.', 'give'),
['status' => $this->authorizationStatusCode()]
);
}
/**
* Check if current user can edit donations.
*
* @since 4.14.0 replace logic with UserPermissions facade
* @since 4.6.0
*/
private function canEditDonations(): bool
{
return UserPermissions::donations()->canEdit();
}
/**
* Check if current user can delete donations.
*
* @since 4.14.0 replace logic with UserPermissions facade
* @since 4.6.0
*/
private function canDeleteDonations(): bool
{
return UserPermissions::donations()->canDelete();
}
/**
* Check if current user can refund donations.
*
* @since 4.14.0 replace logic with UserPermissions facade
* @since 4.6.0
*/
private function canRefundDonations(): bool
{
return UserPermissions::donations()->canEdit();
}
/**
* @since 4.6.0
*/
public function authorizationStatusCode(): int
{
return is_user_logged_in() ? 403 : 401;
}
/**
* @since 4.13.0 Updated schema to match actual response, add schema description
* @since 4.8.0 Change default status to complete
* @since 4.7.0 Change title to givewp/donation and add custom fields schema
* @since 4.6.1 Change type of billing address properties to accept null values
* @since 4.6.0
*/
public function get_item_schema(): array
{
$schema = [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'givewp/donation',
'description' => esc_html__('Donation routes for CRUD operations', 'give'),
'type' => 'object',
'properties' => [
'id' => [
'type' => 'integer',
'description' => esc_html__('Donation ID', 'give'),
'readonly' => true,
],
'donorId' => [
'type' => 'integer',
'description' => esc_html__('Donor ID', 'give'),
],
'firstName' => [
'type' => 'string',
'description' => esc_html__('Donor first name', 'give'),
'format' => 'text-field',
],
'lastName' => [
'type' => ['string', 'null'],
'description' => esc_html__('Donor last name', 'give'),
'format' => 'text-field',
],
'honorific' => [
'type' => ['string', 'null'],
'description' => esc_html__('Donor honorific/prefix', 'give'),
'enum' => $this->get_honorific_prefixes(),
],
'email' => [
'type' => 'string',
'description' => esc_html__('Donor email', 'give'),
'format' => 'email',
],
'phone' => [
'type' => ['string', 'null'],
'description' => esc_html__('Donor phone', 'give'),
'format' => 'text-field',
],
'company' => [
'type' => ['string', 'null'],
'description' => esc_html__('Donor company', 'give'),
'format' => 'text-field',
],
'amount' => SchemaTypes::money()->description(esc_html__('Donation amount', 'give'))->toArray(),
'feeAmountRecovered' => SchemaTypes::money()->nullable()->description(esc_html__('Fee amount recovered', 'give'))->toArray(),
'eventTicketsAmount' => SchemaTypes::money()->nullable()->readonly()->description(esc_html__('Event tickets amount', 'give'))->toArray(),
'status' => [
'type' => 'string',
'description' => esc_html__('Donation status', 'give'),
'enum' => array_values(DonationStatus::toArray()),
'default' => DonationStatus::COMPLETE,
],
'type' => [
'type' => 'string',
'description' => esc_html__('Donation type', 'give'),
'enum' => array_values(DonationType::toArray()),
'default' => DonationType::SINGLE,
'required' => true,
],
'gatewayId' => [
'type' => 'string',
'description' => esc_html__('Payment gateway ID', 'give'),
'format' => 'text-field',
],
'mode' => [
'type' => 'string',
'description' => esc_html__('Donation mode (live or test)', 'give'),
'enum' => array_values(DonationMode::toArray()),
],
'anonymous' => [
'type' => 'boolean',
'description' => esc_html__('Whether the donation is anonymous', 'give'),
'default' => false,
],
'campaignId' => [
'type' => 'integer',
'description' => esc_html__('Campaign ID', 'give'),
],
'formId' => [
'type' => 'integer',
'description' => esc_html__('Form ID', 'give'),
],
'formTitle' => [
'type' => 'string',
'description' => esc_html__('Form title', 'give'),
'format' => 'text-field',
],
'subscriptionId' => [
'type' => ['integer', 'null'],
'description' => esc_html__('Subscription ID', 'give'),
],
'levelId' => [
'type' => ['string', 'null'],
'description' => esc_html__('Level ID', 'give'),
'format' => 'text-field',
],
'gatewayTransactionId' => [
'type' => ['string', 'null'],
'description' => esc_html__('Gateway transaction ID', 'give'),
'format' => 'text-field',
],
'exchangeRate' => [
'type' => 'string',
'description' => esc_html__('Exchange rate', 'give'),
'format' => 'text-field',
'default' => '1',
],
'comment' => [
'type' => ['string', 'null'],
'description' => esc_html__('Donation comment', 'give'),
'format' => 'text-field',
],
'billingAddress' => [
'type' => ['object', 'null'],
'description' => esc_html__('Billing address', 'give'),
'properties' => [
'address1' => ['type' => ['string', 'null'], 'format' => 'text-field'],
'address2' => ['type' => ['string', 'null'], 'format' => 'text-field'],
'city' => ['type' => ['string', 'null'], 'format' => 'text-field'],
'state' => ['type' => ['string', 'null'], 'format' => 'text-field'],
'country' => ['type' => ['string', 'null'], 'format' => 'text-field'],
'zip' => ['type' => ['string', 'null'], 'format' => 'text-field'],
],
],
'donorIp' => [
'type' => ['string', 'null'],
'description' => esc_html__('Donor IP address (sensitive data)', 'give'),
'format' => 'text-field',
],
'purchaseKey' => [
'type' => ['string', 'null'],
'description' => esc_html__('Purchase key (sensitive data)', 'give'),
'format' => 'text-field',
],
'createdAt' => [
'type' => ['string', 'null'],
'description' => esc_html__('Created at Date and Time string', 'give'),
'format' => 'date-time',
],
'updatedAt' => [
'type' => ['string', 'null'],
'description' => esc_html__('Created at Date and Time string', 'give'),
'format' => 'date-time',
],
'updateRenewalDate' => [
'type' => 'boolean',
'description' => esc_html__('Whether to update the subscription renewal date with the createdAt date when creating subscription or renewal donations', 'give'),
'default' => false,
],
'customFields' => [
'type' => 'array',
'readonly' => true,
'description' => esc_html__('Custom fields (sensitive data)', 'give'),
'items' => [
'type' => 'object',
'properties' => [
'label' => [
'type' => 'string',
'description' => esc_html__('Field label', 'give'),
'format' => 'text-field',
],
'value' => [
'type' => 'string',
'description' => esc_html__('Field value', 'give'),
'format' => 'text-field',
],
],
],
],
'gateway' => [
'type' => 'object',
'readonly' => true,
'properties' => [
'id' => [
'type' => 'string',
'description' => esc_html__('Gateway ID', 'give'),
],
'name' => [
'type' => 'string',
'description' => esc_html__('Gateway name', 'give'),
],
'label' => [
'type' => 'string',
'description' => esc_html__('Payment method label', 'give'),
],
'transactionUrl' => [
'type' => 'string',
'description' => esc_html__('Gateway transaction URL', 'give'),
'format' => 'uri',
],
],
],
'eventTickets' => [
'type' => ['array', 'null'],
'readonly' => true,
'description' => esc_html__('Event tickets', 'give'),
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'integer',
'description' => esc_html__('Event ticket ID', 'give'),
],
'eventId' => [
'type' => 'integer',
'description' => esc_html__('Event ID', 'give'),
],
'ticketTypeId' => [
'type' => 'integer',
'description' => esc_html__('Ticket type ID', 'give'),
],
'donationId' => [
'type' => 'integer',
'description' => esc_html__('Donation ID', 'give'),
],
'amount' => SchemaTypes::money()->description(esc_html__('Event ticket amount', 'give'))->toArray(),
'createdAt' => [
'type' => 'string',
'description' => esc_html__('Created at Date and Time string', 'give'),
'format' => 'date-time',
],
'updatedAt' => [
'type' => 'string',
'description' => esc_html__('Updated at Date and Time string', 'give'),
'format' => 'date-time',
],
'event' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'integer',
'description' => esc_html__('Event ID', 'give'),
],
'title' => [
'type' => 'string',
'description' => esc_html__('Event title', 'give'),
],
'description' => [
'type' => 'string',
'description' => esc_html__('Event description', 'give'),
],
'startDateTime' => [
'type' => 'string',
'description' => esc_html__('Event start date and time', 'give'),
'format' => 'date-time',
],
'endDateTime' => [
'type' => 'string',
'description' => esc_html__('Event end date and time', 'give'),
'format' => 'date-time',
],
'ticketCloseDateTime' => [
'type' => 'string',
'description' => esc_html__('Event ticket close date and time', 'give'),
'format' => 'date-time',
],
'createdAt' => [
'type' => 'string',
'description' => esc_html__('Event creation date and time', 'give'),
'format' => 'date-time',
],
'updatedAt' => [
'type' => 'string',
'description' => esc_html__('Event last update date and time', 'give'),
'format' => 'date-time',
],
],
],
'ticketType' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'integer',
'description' => esc_html__('Ticket type ID', 'give'),
],
'eventId' => [
'type' => 'integer',
'description' => esc_html__('Event ID', 'give'),
],
'title' => [
'type' => 'string',
'description' => esc_html__('Ticket type title', 'give'),
],
'description' => [
'type' => 'string',
'description' => esc_html__('Ticket type description', 'give'),
],
'price' => SchemaTypes::money()->description(esc_html__('Ticket type price', 'give'))->toArray(),
'capacity' => [
'type' => 'integer',
'description' => esc_html__('Ticket type capacity', 'give'),
],
'createdAt' => [
'type' => 'string',
'description' => esc_html__('Ticket type creation date and time', 'give'),
'format' => 'date-time',
],
'updatedAt' => [
'type' => 'string',
'description' => esc_html__('Ticket type last update date and time', 'give'),
'format' => 'date-time',
],
],
],
],
],
],
'anyOf' => [
[
// 1) type = renewal -> require subscriptionId
[
'properties' => [
'type' => [
'enum' => ['renewal'],
],
],
'required' => ['subscriptionId'],
],
// 2) type = single -> require donorId, amount, gatewayId, mode, formId, firstName, email
[
'properties' => [
'type' => [
'enum' => ['single'],
],
],
'required' => ['donorId', 'amount', 'gatewayId', 'mode', 'formId', 'firstName', 'email'],
],
// 3) type = subscription -> require donorId, amount, gatewayId, mode, formId, firstName, email, subscriptionId
[
'properties' => [
'type' => [
'enum' => ['subscription'],
],
],
'required' => ['donorId', 'amount', 'gatewayId', 'mode', 'formId', 'firstName', 'email', 'subscriptionId'],
],
],
],
],
];
return $this->add_additional_fields_schema($schema);
}
/**
* Gets all available honorific prefixes.
*
* Fetches the user-configured honorific prefixes from settings and merges them
* with a hardcoded 'anonymous' prefix. The 'anonymous' prefix is required
* when requests with anonymousDonations=redact are present.
*
* @return array<string> An array of honorific prefixes.
*/
private function get_honorific_prefixes(): array {
$prefixes = (array) give_get_option( 'title_prefixes', array_values( give_get_default_title_prefixes() ) );
return array_merge( $prefixes, ['anonymous', null] );
}
}