diff --git a/.env.dist.testing b/.env.dist.testing index ca3522d..f61980a 100644 --- a/.env.dist.testing +++ b/.env.dist.testing @@ -1,4 +1,5 @@ CONVERTKIT_API_BROADCAST_ID="8697158" +CONVERTKIT_API_CUSTOM_FIELD_ID="264073" CONVERTKIT_API_FORM_ID="2765139" CONVERTKIT_API_FORM_ID_2="2780977" CONVERTKIT_API_LEGACY_FORM_URL="https://app.convertkit.com/landing_pages/470099" diff --git a/.env.example b/.env.example index 2628529..a995c61 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,7 @@ CONVERTKIT_OAUTH_CLIENT_ID= CONVERTKIT_OAUTH_CLIENT_SECRET= CONVERTKIT_OAUTH_REDIRECT_URI="https://convertkit-github.local/wp-admin/options-general.php?page=_wp_convertkit_settings" CONVERTKIT_API_BROADCAST_ID="8697158" +CONVERTKIT_API_CUSTOM_FIELD_ID="264073" CONVERTKIT_API_FORM_ID="2765139" CONVERTKIT_API_FORM_ID_2="2780977" CONVERTKIT_API_LEGACY_FORM_URL="https://app.convertkit.com/landing_pages/470099" diff --git a/phpcs.xml b/phpcs.xml index 9de2bbd..3b059b2 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -72,7 +72,7 @@ - + diff --git a/src/ConvertKit_API.php b/src/ConvertKit_API.php index 9a8e1b9..a44401b 100644 --- a/src/ConvertKit_API.php +++ b/src/ConvertKit_API.php @@ -348,13 +348,13 @@ public function get_resource(string $url) /** * Performs an API request using Guzzle. * - * @param string $endpoint API Endpoint. - * @param string $method Request method. - * @param array>> $args Request arguments. + * @param string $endpoint API Endpoint. + * @param string $method Request method. + * @param array>> $args Request arguments. * * @throws \Exception If JSON encoding arguments failed. * - * @return mixed|object + * @return false|mixed */ public function request(string $endpoint, string $method, array $args = []) { diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php index 4511a73..efc67ab 100644 --- a/src/ConvertKit_API_Traits.php +++ b/src/ConvertKit_API_Traits.php @@ -945,6 +945,74 @@ public function create_subscribers(array $subscribers, string $callback_url = '' ); } + /** + * Filter subscribers based on engagement. + * + * @param array> $all Array of filter conditions where ALL must be met (AND logic). Each condition can have. + * - 'type' (string). + * - 'count_greater_than' (int|null). + * - 'count_less_than' (int|null). + * - 'after' (\DateTime|null). + * - 'before' (\DateTime|null). + * - 'any' (array|null). + * @param boolean $include_total_count To include the total count of records in the response, use true. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. + * + * @since 2.4.0 + * + * @see https://developers.kit.com/api-reference/subscribers/filter-subscribers-based-on-engagement + * + * @return mixed + */ + public function filter_subscribers( + array $all = [], + bool $include_total_count = false, + string $after_cursor = '', + string $before_cursor = '', + int $per_page = 100 + ) { + $options = []; + + foreach ($all as $condition) { + $option = []; + + if (array_key_exists('count_greater_than', $condition) && $condition['count_greater_than'] !== null) { + $option['count_greater_than'] = $condition['count_greater_than']; + } + + if (array_key_exists('count_less_than', $condition) && $condition['count_less_than'] !== null) { + $option['count_less_than'] = $condition['count_less_than']; + } + + if (array_key_exists('after', $condition) && $condition['after'] instanceof \DateTime) { + $option['after'] = $condition['after']->format('Y-m-d'); + } + + if (array_key_exists('before', $condition) && $condition['before'] instanceof \DateTime) { + $option['before'] = $condition['before']->format('Y-m-d'); + } + + if (array_key_exists('any', $condition) && !empty($condition['any'])) { + $option['any'] = (array) $condition['any']; + } + + $options[] = $option; + }//end foreach + + return $this->post( + 'subscribers/filter', + $this->build_total_count_and_pagination_params( + ['all' => $options], + $include_total_count, + $after_cursor, + $before_cursor, + $per_page + ) + ); + } + /** * Get the ConvertKit subscriber ID associated with email address if it exists. * Return false if subscriber not found. @@ -1468,6 +1536,8 @@ public function create_webhook(string $url, string $event, string $parameter = ' case 'subscriber.subscriber_bounce': case 'subscriber.subscriber_complain': case 'purchase.purchase_create': + case 'custom_field.field_created': + case 'custom_field.field_deleted': $eventData = ['name' => $event]; break; @@ -1508,6 +1578,13 @@ public function create_webhook(string $url, string $event, string $parameter = ' ]; break; + case 'custom_field.field_value_updated': + $eventData = [ + 'name' => $event, + 'custom_field_id' => $parameter, + ]; + break; + default: throw new \InvalidArgumentException(sprintf('The event %s is not supported', $event)); }//end switch @@ -1617,6 +1694,36 @@ public function create_custom_fields(array $labels, string $callback_url = '') ); } + /** + * Bulk update subscriber custom field values + * + * @param array> $custom_field_values Array of custom field values to update. + * - 'subscriber_id' (int) Subscriber ID. + * - 'subscriber_custom_field_id' (int) Custom Field ID. + * - 'value' (string|integer) Value to update. + * @param string $callback_url URL to notify for large batch size when async processing complete. + * + * @since 2.4.0 + * + * @see https://developers.kit.com/api-reference/custom-fields/bulk-update-subscriber-custom-field-values + * + * @return mixed|object + */ + public function update_subscriber_custom_field_values(array $custom_field_values, string $callback_url = '') + { + // Build parameters. + $options = ['custom_field_values' => $custom_field_values]; + if (!empty($callback_url)) { + $options['callback_url'] = $callback_url; + } + + // Send request. + return $this->post( + 'bulk/custom_fields/subscribers', + $options + ); + } + /** * Update a custom field. * @@ -1854,15 +1961,15 @@ public function strip_html_head_body_tags(string $markup) /** * Adds total count and pagination parameters to the given array of existing API parameters. * - * @param array $params API parameters. - * @param boolean $include_total_count Return total count of records. - * @param string $after_cursor Return results after the given pagination cursor. - * @param string $before_cursor Return results before the given pagination cursor. - * @param integer $per_page Number of results to return. + * @param array>> $params API parameters. + * @param boolean $include_total_count Return total count of records. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. * * @since 2.0.0 * - * @return array + * @return array>> */ private function build_total_count_and_pagination_params( array $params = [], @@ -1888,8 +1995,8 @@ private function build_total_count_and_pagination_params( /** * Performs a GET request to the API. * - * @param string $endpoint API Endpoint. - * @param array|string> $args Request arguments. + * @param string $endpoint API Endpoint. + * @param array|list>> $args Request arguments. * * @return false|mixed */ @@ -1901,8 +2008,8 @@ public function get(string $endpoint, array $args = []) /** * Performs a POST request to the API. * - * @param string $endpoint API Endpoint. - * @param array>> $args Request arguments. + * @param string $endpoint API Endpoint. + * @param array|boolean|integer|float|string>> $args Request arguments. * * @return false|mixed */ @@ -1940,9 +2047,9 @@ public function delete(string $endpoint, array $args = []) /** * Performs an API request. * - * @param string $endpoint API Endpoint. - * @param string $method Request method. - * @param array>> $args Request arguments. + * @param string $endpoint API Endpoint. + * @param string $method Request method. + * @param array>> $args Request arguments. * * @throws \Exception If JSON encoding arguments failed. * diff --git a/tests/ConvertKitAPIKeyTest.php b/tests/ConvertKitAPIKeyTest.php index 43dca9e..28ec468 100644 --- a/tests/ConvertKitAPIKeyTest.php +++ b/tests/ConvertKitAPIKeyTest.php @@ -475,6 +475,33 @@ public function testCreateCustomFields() $result = $this->api->create_custom_fields($labels); } + /** + * Test that update_subscriber_custom_field_values() throws a ClientException + * as this is only supported using OAuth. + * + * @since 2.4.0 + * + * @return void + */ + public function testUpdateSubscriberCustomFieldValues() + { + $this->expectException(ClientException::class); + $result = $this->api->update_subscriber_custom_field_values( + [ + [ + 'subscriber_id' => 1, + 'subscriber_custom_field_id' => (int) $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'], + 'value' => '100', + ], + [ + 'subscriber_id' => 2, + 'subscriber_custom_field_id' => (int) $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'], + 'value' => '200', + ], + ] + ); + } + /** * Test that get_purchases() throws a ClientException * as this is only supported using OAuth. diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index 70bbf12..6c48680 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -3679,6 +3679,166 @@ public function testCreateSubscribersWithInvalidEmailAddresses() $this->assertCount(2, $result->failures); } + /** + * Test that filter_subscribers() returns the expected data. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribers() + { + $result = $this->api->filter_subscribers( + [ + [ + 'type' => 'opens', + 'count_greater_than' => 10, + 'count_less_than' => 100, + 'after' => new \DateTime('2024-01-01'), + 'before' => new \DateTime('2027-01-01'), + ] + ] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that filter_subscribers() returns the expected data + * when multiple all conditions are specified. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribersWithMultipleConditions() + { + $result = $this->api->filter_subscribers( + [ + [ + 'type' => 'opens', + 'count_greater_than' => 10, + 'count_less_than' => 100, + 'after' => new \DateTime('2024-01-01'), + 'before' => new \DateTime('2027-01-01'), + ], + [ + 'type' => 'clicks', + 'count_greater_than' => 1, + 'count_less_than' => 100, + 'after' => new \DateTime('2024-01-01'), + 'before' => new \DateTime('2027-01-01'), + ] + ] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that filter_subscribers() returns the expected data + * when multiple any conditions are specified. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribersWithNoParameters() + { + $result = $this->api->filter_subscribers(); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that filter_subscribers() returns the expected data + * when the total count is included. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribersWithTotalCount() + { + $result = $this->api->filter_subscribers( + include_total_count: true + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that filter_subscribers() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribersPagination() + { + $result = $this->api->filter_subscribers( + per_page: 1 + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->filter_subscribers( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->filter_subscribers( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + } + /** * Test that get_subscriber_id() returns the expected data. * @@ -4693,17 +4853,19 @@ public function testCreateWebhookWithEventParameter() $url = 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'); $result = $this->api->create_webhook( url: $url, - event: 'subscriber.form_subscribe', - parameter: $_ENV['CONVERTKIT_API_FORM_ID'] + event: 'custom_field.field_value_updated', + parameter: $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'] ); // Confirm webhook created with correct data. $this->assertArrayHasKey('webhook', get_object_vars($result)); $this->assertArrayHasKey('id', get_object_vars($result->webhook)); + $this->assertArrayHasKey('account_id', get_object_vars($result->webhook)); + $this->assertArrayHasKey('event', get_object_vars($result->webhook)); $this->assertArrayHasKey('target_url', get_object_vars($result->webhook)); $this->assertEquals($result->webhook->target_url, $url); - $this->assertEquals($result->webhook->event->name, 'form_subscribe'); - $this->assertEquals($result->webhook->event->form_id, $_ENV['CONVERTKIT_API_FORM_ID']); + $this->assertEquals($result->webhook->event->name, 'field_value_updated'); + $this->assertEquals($result->webhook->event->custom_field_id, $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID']); // Delete the webhook. $result = $this->api->delete_webhook($result->webhook->id); @@ -4899,6 +5061,55 @@ public function testCreateCustomFields() $this->assertIsArray($result->custom_fields); } + /** + * Test that update_subscriber_custom_field_values() works. + * + * @since 2.4.0 + * + * @return void + */ + public function testUpdateSubscriberCustomFieldValues() + { + // Create subscribers. + $subscribers = [ + [ + 'email_address' => str_replace('@kit.com', '-1@kit.com', $this->generateEmailAddress()), + ], + [ + 'email_address' => str_replace('@kit.com', '-2@kit.com', $this->generateEmailAddress()), + ], + ]; + $result = $this->api->create_subscribers($subscribers); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + foreach ($result->subscribers as $i => $subscriber) { + $this->subscriber_ids[] = $subscriber->id; + } + + // Bulk update subscriber custom field values. + $result = $this->api->update_subscriber_custom_field_values( + [ + [ + 'subscriber_id' => $this->subscriber_ids[0], + 'subscriber_custom_field_id' => (int) $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'], + 'value' => '100', + ], + [ + 'subscriber_id' => $this->subscriber_ids[1], + 'subscriber_custom_field_id' => (int) $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'], + 'value' => '200', + ], + ] + ); + + // Assert no failures. + $this->assertCount(0, $result->failures); + + // Confirm result is an array comprising of each custom field value that was updated. + $this->assertIsArray($result->custom_field_values); + $this->assertCount(2, $result->custom_field_values); + } + /** * Test that update_custom_field() works. *