diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d389cee..1c322ab 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,7 @@ jobs: # Defines PHP Versions matrix to run tests on strategy: fail-fast: false + max-parallel: 1 matrix: php-versions: [ '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php index 7462457..4ca2f85 100644 --- a/src/ConvertKit_API_Traits.php +++ b/src/ConvertKit_API_Traits.php @@ -662,6 +662,196 @@ public function get_sequence_subscriptions( ); } + /** + * List sequence emails + * + * @param integer $sequence_id Sequence ID. + * @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. + * + * @see https://developers.kit.com/api-reference/sequence-emails/list-sequence-emails + * + * @return false|mixed + */ + public function get_sequence_emails( + int $sequence_id, + bool $include_total_count = false, + string $after_cursor = '', + string $before_cursor = '', + int $per_page = 100 + ) { + return $this->get( + sprintf('sequences/%s/emails', $sequence_id), + $this->build_total_count_and_pagination_params( + [], + $include_total_count, + $after_cursor, + $before_cursor, + $per_page + ) + ); + } + + /** + * Create a sequence email + * + * @param integer $sequence_id Sequence ID. + * @param string $subject Subject line of the email. + * @param integer $delay_value Number of days or hours to wait before sending this email after the previous one. + * @param string $delay_unit Unit for the send delay. Use `days` for schedule-aware delivery, `hours` for a fixed hourly delay. + * @param string|null $preview_text Preview text shown in email clients before the email is opened. + * @param string|null $content HTML body content of the email. + * @param integer|null $email_template_id ID of the email template to use for layout and styling. + * @param boolean $published Whether the email is active and will be sent to subscribers. + * @param array|null $send_days Days of the week this email may be sent. Defaults to all 7 days (inherits the sequence schedule). Pass a subset to restrict delivery, or null to reset to all days. + * @param integer|null $position Zero-based position of the email in the sequence. Assigned automatically after the last email if omitted. + * + * @see https://developers.kit.com/api-reference/sequence-emails/create-a-sequence-email + * + * @return mixed|object + */ + public function create_sequence_email( + int $sequence_id, + string $subject, + int $delay_value, + string $delay_unit, + string|null $preview_text = null, + string|null $content = null, + int|null $email_template_id = null, + bool $published = false, + array|null $send_days = null, + int|null $position = null, + ) { + $options = [ + 'subject' => $subject, + 'delay_value' => $delay_value, + 'delay_unit' => $delay_unit, + 'published' => $published, + 'send_days' => $send_days, + ]; + + if (!empty($preview_text)) { + $options['preview_text'] = $preview_text; + } + if (!empty($content)) { + $options['content'] = $content; + } + if (!empty($email_template_id)) { + $options['email_template_id'] = $email_template_id; + } + if (!empty($position)) { + $options['position'] = $position; + } + + // Send request. + return $this->post( + sprintf('sequences/%s/emails', $sequence_id), + $options + ); + } + + /** + * Get a sequence email. + * + * @param integer $sequence_id Sequence ID. + * @param integer $email_id Email ID. + * + * @see https://developers.kit.com/api-reference/sequence-emails/get-a-sequence-email + * + * @return mixed|object + */ + public function get_sequence_email(int $sequence_id, int $email_id) + { + return $this->get(sprintf('sequences/%s/emails/%s', $sequence_id, $email_id)); + } + + /** + * Updates a sequence + * + * @param integer $sequence_id Sequence ID. + * @param integer $email_id Sequence Email ID. + * @param string|null $subject Subject line of the email. + * @param integer|null $delay_value Number of days or hours to wait before sending this email after the previous one. + * @param string|null $delay_unit Unit for the send delay. Use `days` for schedule-aware delivery, `hours` for a fixed hourly delay. + * @param string|null $preview_text Preview text shown in email clients before the email is opened. + * @param string|null $content HTML body content of the email. + * @param integer|null $email_template_id ID of the email template to use for layout and styling. + * @param boolean|null $published Whether the email is active and will be sent to subscribers. + * @param array|null $send_days Days of the week this email may be sent. Defaults to all 7 days (inherits the sequence schedule). Pass a subset to restrict delivery, or null to reset to all days. + * @param integer|null $position Zero-based position of the email in the sequence. Assigned automatically after the last email if omitted. + * + * @see https://developers.kit.com/api-reference/sequences/create-a-sequence + * + * @return mixed|object + */ + public function update_sequence_email( + int $sequence_id, + int $email_id, + string|null $subject = null, + int|null $delay_value = null, + string|null $delay_unit = null, + string|null $preview_text = null, + string|null $content = null, + int|null $email_template_id = null, + bool|null $published = null, + array|null $send_days = null, + int|null $position = null, + ) { + // Build parameters. + $options = ['send_days' => $send_days]; + + if (!is_null($subject)) { + $options['subject'] = $subject; + } + if (!is_null($delay_value)) { + $options['delay_value'] = $delay_value; + } + if (!is_null($delay_unit)) { + $options['delay_unit'] = $delay_unit; + } + if (!is_null($preview_text)) { + $options['preview_text'] = $preview_text; + } + if (!is_null($content)) { + $options['content'] = $content; + } + if (!is_null($email_template_id)) { + $options['email_template_id'] = $email_template_id; + } + if (!is_null($published)) { + $options['published'] = $published; + } + if (!is_null($send_days)) { + $options['send_days'] = $send_days; + } + if (!is_null($position)) { + $options['position'] = $position; + } + + // Send request. + return $this->put( + sprintf('sequences/%s/emails/%s', $sequence_id, $email_id), + $options + ); + } + + /** + * Deletes a sequence email. + * + * @param integer $sequence_id Sequence ID. + * @param integer $email_id Email ID. + * + * @see https://developers.kit.com/api-reference/sequence-emails/delete-a-sequence-email + * + * @return mixed|object + */ + public function delete_sequence_email(int $sequence_id, int $email_id) + { + return $this->delete(sprintf('sequences/%s/emails/%s', $sequence_id, $email_id)); + } + /** * List snippets * diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index 3fbdded..2620edb 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -1601,6 +1601,302 @@ public function testGetSequenceSubscriptionsWithInvalidPagination() ); } + /** + * Test that get_sequence_emails() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmails() + { + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'] + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Check first sequence in resultset has expected data. + $email = get_object_vars($result->emails[0]); + $this->assertArrayHasKey('id', $email); + $this->assertArrayHasKey('sequence_id', $email); + $this->assertArrayHasKey('subject', $email); + $this->assertArrayHasKey('preview_text', $email); + $this->assertArrayHasKey('email_address', $email); + $this->assertArrayHasKey('email_template_id', $email); + $this->assertArrayHasKey('published', $email); + $this->assertArrayHasKey('position', $email); + $this->assertArrayHasKey('delay_value', $email); + $this->assertArrayHasKey('delay_unit', $email); + $this->assertArrayHasKey('send_days', $email); + } + + /** + * Test that get_sequence_emails() returns the expected data + * when the total count is included. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailsWithTotalCount() + { + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + include_total_count: true + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'emails'); + $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 get_sequence_emails() returns the expected data when + * pagination parameters and per_page limits are specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailsPagination() + { + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1 + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Assert a single email was returned. + $this->assertCount(1, $result->emails); + + // 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->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Assert a single email was returned. + $this->assertCount(1, $result->emails); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Assert a single email was returned. + $this->assertCount(1, $result->emails); + } + + /** + * Test that get_sequence_emails() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailsWithInvalidSequenceID() + { + $this->expectException(ClientException::class); + $result = $this->api->get_sequence_emails( + sequence_id: 12345 + ); + } + + /** + * Test that create_sequence_email(), get_sequence_email(), update_sequence_email() + * and delete_sequence_email() works. + * + * We do all tests in a single function, so we don't end up with unnecessary + * Sequence Emails remaining on the Kit account when running tests, which might impact + * other tests that expect (or do not expect) specific Sequence Emails. + * + * @since 2.5.0 + * + * @return void + */ + public function testCreateGetUpdateAndDeleteSequenceEmail() + { + // Create a sequence email. + $result = $this->api->create_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + subject: 'Test Sequence Email', + delay_value: 1, + delay_unit: 'days', + preview_text: 'Test Preview Text', + content: 'Test Content', + email_template_id: (int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], + published: false, + send_days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], + position: 0 + ); + $sequenceEmailID = $result->email->id; + + // Confirm the Sequence Email saved. + $result = get_object_vars($result->email); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Test Sequence Email', $result['subject']); + $this->assertEquals(1, $result['delay_value']); + $this->assertEquals('days', $result['delay_unit']); + $this->assertEquals('Test Preview Text', $result['preview_text']); + $this->assertEquals('Test Content', $result['content']); + $this->assertEquals((int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], $result['email_template_id']); + $this->assertEquals(false, $result['published']); + $this->assertEquals(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], $result['send_days']); + $this->assertEquals(2, $result['position']); + + // Get the sequence email. + $result = $this->api->get_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + email_id: $sequenceEmailID + ); + + // Update the existing sequence email. + $result = $this->api->update_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + email_id: $sequenceEmailID, + subject: 'Edited Test Sequence Email', + preview_text: 'Edited Test Preview Text', + content: 'Edited Test Content', + delay_value: 2, + delay_unit: 'hours', + email_template_id: (int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], + published: true, + send_days: ['saturday', 'sunday'], + position: 1, + ); + + // Confirm the changes saved. + $result = get_object_vars($result->email); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Edited Test Sequence Email', $result['subject']); + $this->assertEquals(2, $result['delay_value']); + $this->assertEquals('hours', $result['delay_unit']); + $this->assertEquals('Edited Test Preview Text', $result['preview_text']); + $this->assertEquals('Edited Test Content', $result['content']); + $this->assertEquals((int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], $result['email_template_id']); + $this->assertEquals(true, $result['published']); + $this->assertEquals(['saturday', 'sunday'], $result['send_days']); + $this->assertEquals(1, $result['position']); + + // Delete Sequence Email. + $this->api->delete_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], $sequenceEmailID); + $this->assertEquals(204, $this->api->getResponseInterface()->getStatusCode()); + } + + /** + * Test that get_sequence_email() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailWithInvalidSequenceID() + { + $this->expectException(ClientException::class); + $this->api->get_sequence_email(12345, 12345); + } + + /** + * Test that get_sequence_email() throws a ClientException when an invalid + * email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailWithInvalidEmailID() + { + $this->expectException(ClientException::class); + $this->api->get_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], 12345); + } + + /** + * Test that update_sequence_email() throws a ClientException when an invalid + * sequence email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSequenceEmailWithInvalidSequenceID() + { + $this->expectException(ClientException::class); + $this->api->update_sequence_email(12345, 12345); + } + + /** + * Test that update_sequence_email() throws a ClientException when an invalid + * email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSequenceEmailWithInvalidEmailID() + { + $this->expectException(ClientException::class); + $this->api->update_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], 12345); + } + + /** + * Test that delete_sequence_email() throws a ClientException when an invalid + * sequence email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testDeleteSequenceEmailWithInvalidSequenceID() + { + $this->expectException(ClientException::class); + $this->api->delete_sequence_email(12345, 12345); + } + + /** + * Test that delete_sequence_email() throws a ClientException when an invalid + * email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testDeleteSequenceEmailWithInvalidEmailID() + { + $this->expectException(ClientException::class); + $this->api->delete_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], 12345); + } + /** * Test that get_snippets() returns the expected data. *