diff --git a/lib/convertkit.rb b/lib/convertkit.rb index c50757d..dca157b 100644 --- a/lib/convertkit.rb +++ b/lib/convertkit.rb @@ -19,8 +19,12 @@ require 'convertkit/resources/sequence_response' require 'convertkit/resources/sequences' require 'convertkit/resources/subscriber_response' +require 'convertkit/resources/subscriber_bulk_add_tag_response' require 'convertkit/resources/subscriber_bulk_create_response' require 'convertkit/resources/subscriber_bulk_create_failure_response' +require 'convertkit/resources/subscriber_bulk_remove_tag_response' +require 'convertkit/resources/subscriber_bulk_tag_failure_response' +require 'convertkit/resources/subscriber_tag_response' require 'convertkit/resources/subscribers' require 'convertkit/resources/subscribers_response' require 'convertkit/resources/subscription_response' diff --git a/lib/convertkit/client.rb b/lib/convertkit/client.rb index 550bcaa..222a0e6 100644 --- a/lib/convertkit/client.rb +++ b/lib/convertkit/client.rb @@ -69,4 +69,12 @@ def handle_response(response, raw_response= false) end end end + + # This client behaves like the standard Client, but uses an API Key rather than OAuth access_token. + # Certain API resources can only be accessed with an OAuth access_token + class DeveloperClient < Client + def initialize(api_key) + @connection = ConvertKit::Connection.new(API_URL, api_key: api_key) + end + end end diff --git a/lib/convertkit/connection.rb b/lib/convertkit/connection.rb index 367e9b0..78cfba2 100644 --- a/lib/convertkit/connection.rb +++ b/lib/convertkit/connection.rb @@ -10,8 +10,10 @@ class Connection def initialize(url, options = {}) @url = url - @connection = Faraday.new(url: @url, headers: {'content-type' => MIME_TYPE }) do |builder| - builder.request :authorization, 'Bearer', options[:auth_token] if options[:auth_token] + @connection = Faraday.new(url: @url, headers: default_headers(options)) do |builder| + if options[:auth_token] + builder.request :authorization, 'Bearer', options[:auth_token] + end end end @@ -32,6 +34,12 @@ def initialize(url, options = {}) private + def default_headers(options) + headers = { 'Content-Type' => MIME_TYPE } + headers['X-Kit-Api-Key'] = options[:api_key] if options[:api_key] + headers + end + def process_request(method, params) return params if method == :get return params if params.empty? diff --git a/lib/convertkit/resources/subscriber_bulk_add_tag_response.rb b/lib/convertkit/resources/subscriber_bulk_add_tag_response.rb new file mode 100644 index 0000000..5b6cbd4 --- /dev/null +++ b/lib/convertkit/resources/subscriber_bulk_add_tag_response.rb @@ -0,0 +1,17 @@ +module ConvertKit + module Resources + class SubscriberBulkAddTagResponse + attr_accessor :subscribers, :failures + + def initialize(response) + @subscribers = response['subscribers'].map do |subscriber| + ConvertKit::Resources::TaggedSubscriberResponse.new(subscriber) + end + + @failures = response['failures'].map do |failure| + ConvertKit::Resources::SubscriberBulkTagFailureResponse.new(failure) + end + end + end + end +end diff --git a/lib/convertkit/resources/subscriber_bulk_remove_tag_response.rb b/lib/convertkit/resources/subscriber_bulk_remove_tag_response.rb new file mode 100644 index 0000000..0f8612e --- /dev/null +++ b/lib/convertkit/resources/subscriber_bulk_remove_tag_response.rb @@ -0,0 +1,13 @@ +module ConvertKit + module Resources + class SubscriberBulkRemoveTagResponse + attr_accessor :failures + + def initialize(response) + @failures = response['failures'].map do |failure| + ConvertKit::Resources::SubscriberBulkTagFailureResponse.new(failure) + end + end + end + end +end diff --git a/lib/convertkit/resources/subscriber_bulk_tag_failure_response.rb b/lib/convertkit/resources/subscriber_bulk_tag_failure_response.rb new file mode 100644 index 0000000..4aa5452 --- /dev/null +++ b/lib/convertkit/resources/subscriber_bulk_tag_failure_response.rb @@ -0,0 +1,12 @@ +module ConvertKit + module Resources + class SubscriberBulkTagFailureResponse + attr_accessor :tagging, :errors + + def initialize(response) + @tagging = SubscriberTagResponse.new(response['tagging']) if response['tagging'] + @errors = response['errors'] + end + end + end +end diff --git a/lib/convertkit/resources/subscriber_response.rb b/lib/convertkit/resources/subscriber_response.rb index 63da643..4b1103b 100644 --- a/lib/convertkit/resources/subscriber_response.rb +++ b/lib/convertkit/resources/subscriber_response.rb @@ -12,5 +12,17 @@ def initialize(response) @created_at = ConvertKit::Utils.to_datetime(response['created_at']) end end + + class TaggedSubscriberResponse + attr_accessor :id, :first_name, :email, :created_at, :tagged_at + + def initialize(response) + @id = response['id'] + @first_name = response['first_name'] + @email = response['email_address'] + @created_at = ConvertKit::Utils.to_datetime(response['created_at']) + @tagged_at = ConvertKit::Utils.to_datetime(response['tagged_at']) + end + end end end diff --git a/lib/convertkit/resources/subscriber_tag_response.rb b/lib/convertkit/resources/subscriber_tag_response.rb new file mode 100644 index 0000000..c198f79 --- /dev/null +++ b/lib/convertkit/resources/subscriber_tag_response.rb @@ -0,0 +1,12 @@ +module ConvertKit + module Resources + class SubscriberTagResponse + attr_accessor :tag_id, :subscriber_id + + def initialize(response) + @tag_id = response['tag_id'] + @subscriber_id = response['subscriber_id'] + end + end + end +end diff --git a/lib/convertkit/resources/tags.rb b/lib/convertkit/resources/tags.rb index b9625f4..e06fe4a 100644 --- a/lib/convertkit/resources/tags.rb +++ b/lib/convertkit/resources/tags.rb @@ -23,7 +23,18 @@ def list def create(name) response = @client.post(PATH, { name: name }) - TagResponse.new(response) + TagResponse.new(response['tag']) + end + + # Update the name of a Tag + # See https://developers.kit.com/v4#update-tag-name for details + # @param [Integer] tag_id + # @param [Hash] options + # @option options [String] :name New name for Tag + def update(tag_id, options = {}) + request_options = options.slice(:name) + response = @client.put("#{PATH}/#{tag_id}", request_options) + TagResponse.new(response['tag']) end # Tags a subscriber @@ -85,6 +96,30 @@ def subscriptions(tag_id, options = {}) SubscriptionsResponse.new(response) end + + # Bulk add tag to subscribers + # @param [Array] taggings + # @option taggings [String] :tag_id + # @option taggings [String] :subscriber_id + def bulk_add_to_subscribers(taggings = []) + raise ArgumentError, 'taggings must be an array' unless taggings.is_a?(Array) + + response = @client.post("bulk/#{PATH}/subscribers", { taggings: taggings }) + + ConvertKit::Resources::SubscriberBulkAddTagResponse.new(response) + end + + # Bulk remove tag from subscribers + # @param [Array] taggings + # @option taggings [String] :tag_id + # @option taggings [String] :subscriber_id + def bulk_remove_from_subscribers(taggings = []) + raise ArgumentError, 'taggings must be an array' unless taggings.is_a?(Array) + + response = @client.delete("bulk/#{PATH}/subscribers", { taggings: taggings }) + + ConvertKit::Resources::SubscriberBulkRemoveTagResponse.new(response) + end end end end diff --git a/spec/lib/convertkit/connection_spec.rb b/spec/lib/convertkit/connection_spec.rb index 866b272..2744425 100644 --- a/spec/lib/convertkit/connection_spec.rb +++ b/spec/lib/convertkit/connection_spec.rb @@ -3,7 +3,7 @@ describe '#initialize' do it 'creates a new Faraday connection' do - expect(Faraday).to receive(:new).with(url: url, headers: {'content-type' => 'application/json' }) + expect(Faraday).to receive(:new).with(url: url, headers: {'Content-Type' => 'application/json' }) ConvertKit::Connection.new(url) end end diff --git a/spec/lib/convertkit/developer_client_spec.rb b/spec/lib/convertkit/developer_client_spec.rb new file mode 100644 index 0000000..3e71098 --- /dev/null +++ b/spec/lib/convertkit/developer_client_spec.rb @@ -0,0 +1,13 @@ +describe ConvertKit::DeveloperClient do + let(:connection) { double('connection') } + + describe '#initialize' do + it 'sets the api_key' do + expect(ConvertKit::Connection).to receive(:new) + .with(ConvertKit::Client::API_URL, api_key: 'test_token') + .and_return(connection) + client = ConvertKit::DeveloperClient.new('test_token') + expect(client.instance_variable_get(:@connection)).to eq(connection) + end + end +end \ No newline at end of file diff --git a/spec/lib/convertkit/resources/tags_spec.rb b/spec/lib/convertkit/resources/tags_spec.rb index b8edb8d..5625291 100644 --- a/spec/lib/convertkit/resources/tags_spec.rb +++ b/spec/lib/convertkit/resources/tags_spec.rb @@ -31,10 +31,10 @@ context 'when name is a string' do it 'creates a tag' do - response = { 'id' => 1, 'name' => 'tag_name', account_id: 1, state: 'available', 'created_at' => '2023-08-09T04:30:00Z', updated_at: '2023-08-09T04:30:00Z'} + response = {'tag' => { 'id' => 1, 'name' => 'tag_name', account_id: 1, state: 'available', 'created_at' => '2023-08-09T04:30:00Z', updated_at: '2023-08-09T04:30:00Z'}} expect(client).to receive(:post).with('tags', {name: 'tag_name'}).and_return(response) tags_response = tags.create('tag_name') - validate_tag(tags_response, response) + validate_tag(tags_response, response['tag']) end end end @@ -142,4 +142,100 @@ validate_subscriptions(tag_subscriptions_response, response) end end + + describe '#bulk_add_to_subscribers' do + let(:tags) { ConvertKit::Resources::Tags.new(client) } + let(:taggings) { [{'tag_id' => 1, 'subscriber_id' => 1}] } + let(:response) do + { + 'subscribers' => [{ + 'id' => 1, + 'first_name' => 'foo', + 'email_address' => 'foo@bar.com', + 'created_at' => '2023-08-09T04:30:00Z', + 'tagged_at' => '2023-08-09T04:30:00Z' + }], + 'failures' => [] + } + end + + context 'with taggings provided' do + it 'tags listed subscribers' do + expect(client).to receive(:post).with('bulk/tags/subscribers', {taggings: taggings}).and_return(response) + + tags_response = tags.bulk_add_to_subscribers(taggings) + validate_tagged_subscribers(tags_response, response) + end + end + + # Failures are not well documented in the API documentation + context 'with failures' do + let(:response) do + { + 'subscribers' => [], + 'failures' => [{ + 'tagging' => { + 'subscriber_id' => 1, + 'tag_id' => 2, + }, + 'errors' => ['Test error message'] + }] + } + end + + it 'return failures with subscriber and error message' do + expect(client).to receive(:post).with('bulk/tags/subscribers', {taggings: taggings}).and_return(response) + + tags_response = tags.bulk_add_to_subscribers(taggings) + expect(tags_response.failures.count).to eq(1) + expect(tags_response.failures.first.tagging.subscriber_id).to eq(1) + expect(tags_response.failures.first.tagging.tag_id).to eq(2) + expect(tags_response.failures.first.errors.first).to eq('Test error message') + end + end + end + + describe '#bulk_remove_from_subscribers' do + let(:tags) { ConvertKit::Resources::Tags.new(client) } + let(:taggings) { [{'tag_id' => 1, 'subscriber_id' => 1}] } + let(:response) do + { + 'failures' => [] + } + end + + context 'with taggings provided' do + it 'return no failures' do + expect(client).to receive(:delete).with('bulk/tags/subscribers', {taggings: taggings}).and_return(response) + + tags_response = tags.bulk_remove_from_subscribers(taggings) + expect(tags_response.failures).to be_empty + end + end + + # Failures are not well documented in the API documentation + context 'with failures' do + let(:response) do + { + 'failures' => [{ + 'tagging' => { + 'subscriber_id' => 1, + 'tag_id' => 2, + }, + 'errors' => ['Test error message'] + }] + } + end + + it 'return failures with subscriber and error message' do + expect(client).to receive(:delete).with('bulk/tags/subscribers', {taggings: taggings}).and_return(response) + + tags_response = tags.bulk_remove_from_subscribers(taggings) + expect(tags_response.failures.count).to eq(1) + expect(tags_response.failures.first.tagging.subscriber_id).to eq(1) + expect(tags_response.failures.first.tagging.tag_id).to eq(2) + expect(tags_response.failures.first.errors.first).to eq('Test error message') + end + end + end end diff --git a/spec/validators/subscriber_validators.rb b/spec/validators/subscriber_validators.rb index dbc2548..9b646d5 100644 --- a/spec/validators/subscriber_validators.rb +++ b/spec/validators/subscriber_validators.rb @@ -19,6 +19,18 @@ def validate_subscribers(subscribers, values) end end + def validate_tagged_subscribers(subscribers, values) + subscribers.subscribers.each_with_index do |subscriber, index| + values = values['subscribers'][index] + + expect(subscriber.id).to eq(values['id']) + expect(subscriber.first_name).to eq(values['first_name']) + expect(subscriber.email).to eq(values['email_address']) + expect(subscriber.created_at).to eq(DateTime.parse(values['created_at'])) unless values.fetch('created_at', '').strip.empty? + expect(subscriber.tagged_at).to eq(DateTime.parse(values['tagged_at'])) unless values.fetch('tagged_at', '').strip.empty? + end + end + def validate_bulk_create_failure(bulk_create_failure_response, values) expect(bulk_create_failure_response.errors).to match_array(values['errors']) if values['errors'] validate_subscriber(bulk_create_failure_response.subscriber, values['subscriber']) if values['subscriber']