diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 3fee6676..c2ba95b2 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -6,18 +6,15 @@ jobs: strategy: matrix: ruby: - - '2.7.x' - - '3.0.x' - - '3.1.x' + - '3.2.6' + - '3.3.6' active_support: - - 'active_support_6.0.x' - - 'active_support_6.1.x' - - 'active_support_7.0.x' + - 'active_support_7.2.x' steps: - name: Checkout uses: actions/checkout@v1 - name: Setup Ruby - uses: actions/setup-ruby@v1 + uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Bundle diff --git a/History.md b/History.md index 2b7db134..d94c267c 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,10 @@ +## 0.17.0 + + * Add various Ticket endpoints (#create!, #update!, #find) + * Add TicketProperties#create! + * Add Task#create! and Task#find + * BREAKING CHANGE : requires ruby 3.2+, as we're not testing against olders versions + ## 0.16.0 * Added the possibility to add custom properties to `Meeting#find` and `Meeting#find_by_contact`. #17 diff --git a/gemfiles/active_support_6.1.x.gemfile b/gemfiles/active_support_6.1.x.gemfile deleted file mode 100644 index ba563d8b..00000000 --- a/gemfiles/active_support_6.1.x.gemfile +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true -source 'https://rubygems.org' - -gem "activesupport", "~> 6.1.1" -gemspec path: '../' diff --git a/gemfiles/active_support_7.0.x.gemfile b/gemfiles/active_support_7.0.x.gemfile deleted file mode 100644 index a4c279ad..00000000 --- a/gemfiles/active_support_7.0.x.gemfile +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true -source 'https://rubygems.org' - -gem "activesupport", "~> 7.0.3" -gemspec path: '../' diff --git a/gemfiles/active_support_6.0.x.gemfile b/gemfiles/active_support_7.2.x.gemfile similarity index 71% rename from gemfiles/active_support_6.0.x.gemfile rename to gemfiles/active_support_7.2.x.gemfile index 6a34a59b..bf10d66f 100644 --- a/gemfiles/active_support_6.0.x.gemfile +++ b/gemfiles/active_support_7.2.x.gemfile @@ -1,5 +1,5 @@ # frozen_string_literal: true source 'https://rubygems.org' -gem "activesupport", "~> 6.0.0" +gem "activesupport", "~> 7.2.0" gemspec path: '../' diff --git a/hubspot-api-ruby.gemspec b/hubspot-api-ruby.gemspec index 5998b0ee..32f4db91 100644 --- a/hubspot-api-ruby.gemspec +++ b/hubspot-api-ruby.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "hubspot-api-ruby" - s.version = "0.16.0" + s.version = "0.17.0" s.require_paths = ["lib"] s.authors = ["Jonathan"] s.email = ["jonathan@hoggo.com"] @@ -15,7 +15,7 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/captaincontrat/hubspot-api-ruby/blob/master/History.md" } - s.required_ruby_version = ">= 2.7" + s.required_ruby_version = ">= 3.1" # Add runtime dependencies here s.add_runtime_dependency "activesupport", ">= 4.2.2" diff --git a/lib/hubspot-api-ruby.rb b/lib/hubspot-api-ruby.rb index 79cada64..d5413a27 100644 --- a/lib/hubspot-api-ruby.rb +++ b/lib/hubspot-api-ruby.rb @@ -29,6 +29,9 @@ require 'hubspot/oauth' require 'hubspot/file' require 'hubspot/meeting' +require 'hubspot/ticket' +require 'hubspot/ticket_properties' +require 'hubspot/task' module Hubspot def self.configure(config={}) diff --git a/lib/hubspot/association.rb b/lib/hubspot/association.rb index 1a826004..ccf11e76 100644 --- a/lib/hubspot/association.rb +++ b/lib/hubspot/association.rb @@ -17,6 +17,18 @@ class Hubspot::Association }, "Contact" => { "Deal" => 4 + }, + "Ticket" => { + "Contact" => 16, + "Deal" => 28, + "Company" => 339, + "Task" => 229 + }, + "Task" => { + "Contact" => 204, + "Deal" => 216, + "Company" => 192, + "Ticket" => 230 } }.freeze diff --git a/lib/hubspot/connection.rb b/lib/hubspot/connection.rb index 93c84956..e44eee04 100644 --- a/lib/hubspot/connection.rb +++ b/lib/hubspot/connection.rb @@ -10,30 +10,33 @@ def get_json(path, opts) handle_response(response).parsed_response end - def post_json(path, opts) - no_parse = opts[:params].delete(:no_parse) { false } + def post_json(path, options) + modification_query(:post, path, options) + end - url = generate_url(path, opts[:params]) - response = post( - url, - body: opts[:body].to_json, - headers: { 'Content-Type' => 'application/json' }, - format: :json, - read_timeout: read_timeout(opts), - open_timeout: open_timeout(opts) - ) + def put_json(path, options) + modification_query(:put, path, options) + end + + def patch_json(path, options) + modification_query(:patch, path, options) + end + def delete_json(path, opts) + url = generate_url(path, opts) + response = delete(url, format: :json, read_timeout: read_timeout(opts), open_timeout: open_timeout(opts)) log_request_and_response url, response, opts[:body] - handle_response(response).yield_self do |r| - no_parse ? r : r.parsed_response - end + handle_response(response) end - def put_json(path, options) + protected + + def modification_query(verb, path, options) no_parse = options[:params].delete(:no_parse) { false } url = generate_url(path, options[:params]) - response = put( + response = send( + verb, url, body: options[:body].to_json, headers: { "Content-Type" => "application/json" }, @@ -48,15 +51,6 @@ def put_json(path, options) end end - def delete_json(path, opts) - url = generate_url(path, opts) - response = delete(url, format: :json, read_timeout: read_timeout(opts), open_timeout: open_timeout(opts)) - log_request_and_response url, response, opts[:body] - handle_response(response) - end - - protected - def read_timeout(opts = {}) opts.delete(:read_timeout) || Hubspot::Config.read_timeout end diff --git a/lib/hubspot/task.rb b/lib/hubspot/task.rb new file mode 100644 index 00000000..fe596776 --- /dev/null +++ b/lib/hubspot/task.rb @@ -0,0 +1,46 @@ +require 'hubspot/utils' + +module Hubspot + # + # HubSpot Tasks API + # + # {https://developers.hubspot.com/beta-docs/guides/api/crm/engagements/tasks} + # + class Task + TASKS_PATH = '/crm/v3/objects/tasks' + TASK_PATH = '/crm/v3/objects/tasks/:task_id' + DEFAULT_TASK_FIELDS = 'hs_timestamp,hs_task_body,hubspot_owner_id,hs_task_subject,hs_task_status,hs_task_priority,'\ + 'hs_task_type,hs_task_reminders' + + attr_reader :properties, :id + + def initialize(response_hash) + @id = response_hash['id'] + @properties = response_hash['properties'].deep_symbolize_keys + end + + class << self + def create!(params = {}, ticket_id: nil) + associations_hash = { 'associations' => [] } + if ticket_id.present? + associations_hash['associations'] << { + "to": { "id": ticket_id }, + "types": [{ "associationCategory": 'HUBSPOT_DEFINED', + "associationTypeId": Hubspot::Association::ASSOCIATION_DEFINITIONS['Task']['Ticket'] }] + } + end + properties = { hs_task_status: 'NOT_STARTED', hs_task_type: 'TODO' }.merge(params) + post_data = associations_hash.merge({ properties: properties }) + + response = Hubspot::Connection.post_json(TASKS_PATH, params: {}, body: post_data) + new(response) + end + + def find(task_id, properties = DEFAULT_TASK_FIELDS) + response = Hubspot::Connection.get_json(TASK_PATH, { task_id: task_id, + properties: properties }) + new(response) + end + end + end +end diff --git a/lib/hubspot/ticket.rb b/lib/hubspot/ticket.rb new file mode 100644 index 00000000..2cb0d732 --- /dev/null +++ b/lib/hubspot/ticket.rb @@ -0,0 +1,62 @@ +require 'hubspot/utils' + +module Hubspot + # + # HubSpot Tickets API + # + # {https://developers.hubspot.com/beta-docs/guides/api/crm/objects/tickets} + # + class Ticket + TICKETS_PATH = '/crm/v3/objects/tickets' + TICKET_PATH = '/crm/v3/objects/tickets/:ticket_id' + + attr_reader :properties, :id + + def initialize(response_hash) + @id = response_hash['id'] + @properties = response_hash['properties'].deep_symbolize_keys + end + + class << self + def create!(params = {}, contact_id: nil, company_id: nil, deal_id: nil) + associations_hash = { 'associations' => [] } + if contact_id.present? + associations_hash['associations'] << { + "to": { "id": contact_id }, + "types": [{ "associationCategory": 'HUBSPOT_DEFINED', + "associationTypeId": Hubspot::Association::ASSOCIATION_DEFINITIONS['Ticket']['Contact'] }] + } + end + if company_id.present? + associations_hash['associations'] << { + "to": { "id": company_id }, + "types": [{ "associationCategory": 'HUBSPOT_DEFINED', + "associationTypeId": Hubspot::Association::ASSOCIATION_DEFINITIONS['Ticket']['Company'] }] + } + end + if deal_id.present? + associations_hash['associations'] << { + "to": { "id": deal_id }, + "types": [{ "associationCategory": 'HUBSPOT_DEFINED', + "associationTypeId": Hubspot::Association::ASSOCIATION_DEFINITIONS['Ticket']['Deal'] }] + } + end + post_data = associations_hash.merge({ properties: params }) + + response = Hubspot::Connection.post_json(TICKETS_PATH, params: {}, body: post_data) + new(response) + end + + def update!(id, properties = {}) + request = { properties: properties } + response = Hubspot::Connection.patch_json(TICKET_PATH, params: { ticket_id: id }, body: request) + new(response) + end + + def find(ticket_id) + response = Hubspot::Connection.get_json(TICKET_PATH, { ticket_id: ticket_id }) + new(response) + end + end + end +end diff --git a/lib/hubspot/ticket_properties.rb b/lib/hubspot/ticket_properties.rb new file mode 100644 index 00000000..a65f7941 --- /dev/null +++ b/lib/hubspot/ticket_properties.rb @@ -0,0 +1,10 @@ +module Hubspot + class TicketProperties < Properties + CREATE_PROPERTY_PATH = '/crm/v3/properties/ticket' + class << self + def create!(params = {}) + superclass.create!(CREATE_PROPERTY_PATH, params) + end + end + end +end diff --git a/spec/factories/contacts.rb b/spec/factories/contacts.rb index 09740a8b..9d4bcd57 100644 --- a/spec/factories/contacts.rb +++ b/spec/factories/contacts.rb @@ -5,6 +5,6 @@ firstname { Faker::Name.first_name } lastname { Faker::Name.last_name } - email { Faker::Internet.safe_email(name: "#{Time.new.to_i.to_s[-5..-1]}#{(0..3).map { (65 + rand(26)).chr }.join}") } + email { Faker::Internet.email(name: "#{Time.new.to_i.to_s[-5..-1]}#{(0..3).map { (65 + rand(26)).chr }.join}") } end end diff --git a/spec/fixtures/vcr_cassettes/task.yml b/spec/fixtures/vcr_cassettes/task.yml new file mode 100644 index 00000000..ceffaff4 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/task.yml @@ -0,0 +1,77 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.hubapi.com/crm/v3/objects/tasks + body: + encoding: UTF-8 + string: '{"associations":[{"to":{"id":16174569112},"types":[{"associationCategory":"HUBSPOT_DEFINED","associationTypeId":230}]}],"properties":{"hs_task_status":"NOT_STARTED","hs_task_type":"TODO","hs_task_body":"i + am a task","hs_task_subject":"title of task","hs_timestamp":"1730980291412"}}' + headers: + Authorization: + - Bearer + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 201 + message: Created + headers: + Date: + - Thu, 07 Nov 2024 11:51:32 GMT + Content-Type: + - application/json;charset=utf-8 + Content-Length: + - '1115' + Connection: + - keep-alive + Location: + - https://api.hubapi.com/crm/v3/objects/tasks/64075014222 + Cf-Ray: + - 8ded1ca5e8d0d70a-CDG + Cf-Cache-Status: + - DYNAMIC + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'false' + X-Content-Type-Options: + - nosniff + X-Hubspot-Correlation-Id: + - f6e0983f-f1f2-4dbb-9a03-28499f0e87df + X-Hubspot-Ratelimit-Daily: + - '1000000' + X-Hubspot-Ratelimit-Daily-Remaining: + - '999894' + X-Hubspot-Ratelimit-Interval-Milliseconds: + - '10000' + X-Hubspot-Ratelimit-Max: + - '190' + X-Hubspot-Ratelimit-Remaining: + - '189' + X-Hubspot-Ratelimit-Secondly: + - '19' + X-Hubspot-Ratelimit-Secondly-Remaining: + - '18' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=oxvVtGSHCCKhoRjRe14twDypKCjwgEIh6e22X%2FLmtWKVgsRRb27POoL5Gd7j1NcyqK%2BqRLzZ3JeUjhuS0gKc7css6LgrE5Tu7aOtG%2BW0iNszBavmdUBMNVJENnwtUAsXStB0Ql5UwoIdRPnu"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"id":"64075014222","properties":{"hs_body_preview":"i am a task","hs_body_preview_html":"\n + \n \n i am a task\n \n","hs_body_preview_is_truncated":"false","hs_createdate":"2024-11-07T11:51:31.747Z","hs_lastmodifieddate":"2024-11-07T11:51:31.747Z","hs_object_id":"64075014222","hs_object_source":"INTEGRATION","hs_object_source_id":"1024738","hs_object_source_label":"INTEGRATION","hs_task_body":"i + am a task","hs_task_completion_count":"0","hs_task_family":"SALES","hs_task_for_object_type":"OWNER","hs_task_is_all_day":"false","hs_task_is_completed":"0","hs_task_is_completed_call":"0","hs_task_is_completed_email":"0","hs_task_is_completed_linked_in":"0","hs_task_is_completed_sequence":"0","hs_task_is_overdue":"true","hs_task_is_past_due_date":"true","hs_task_missed_due_date":"true","hs_task_missed_due_date_count":"1","hs_task_priority":"NONE","hs_task_status":"NOT_STARTED","hs_task_subject":"title + of task","hs_task_type":"TODO","hs_timestamp":"2024-11-07T11:51:31.412Z"},"createdAt":"2024-11-07T11:51:31.747Z","updatedAt":"2024-11-07T11:51:31.747Z","archived":false}' + recorded_at: Thu, 07 Nov 2024 11:51:32 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/task_find.yml b/spec/fixtures/vcr_cassettes/task_find.yml new file mode 100644 index 00000000..d9262d0a --- /dev/null +++ b/spec/fixtures/vcr_cassettes/task_find.yml @@ -0,0 +1,149 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.hubapi.com/crm/v3/objects/tasks/64075014222?properties=hs_task_subject,hs_task_status + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 07 Nov 2024 12:00:43 GMT + Content-Type: + - application/json;charset=utf-8 + Content-Length: + - '316' + Connection: + - keep-alive + Cf-Ray: + - 8ded2a1c08efbb3a-CDG + Cf-Cache-Status: + - DYNAMIC + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'false' + X-Content-Type-Options: + - nosniff + X-Hubspot-Correlation-Id: + - db177d07-adc5-4bea-ae28-486c02332807 + X-Hubspot-Ratelimit-Daily: + - '1000000' + X-Hubspot-Ratelimit-Daily-Remaining: + - '999889' + X-Hubspot-Ratelimit-Interval-Milliseconds: + - '10000' + X-Hubspot-Ratelimit-Max: + - '190' + X-Hubspot-Ratelimit-Remaining: + - '189' + X-Hubspot-Ratelimit-Secondly: + - '19' + X-Hubspot-Ratelimit-Secondly-Remaining: + - '18' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=hxYTa1rH%2Bxm66LW5pR3x3TK26HGqLnVB8Os5q1b%2F0A0Kc1Sptk47jnY326u3KiE0kfG2lW26CTtaQGu2hm9%2B7dosdfjuxh3J%2F%2FpLJ%2Fn%2FX3PgedlsX0AIhu2%2FPyC1V8K7cEmS%2BsNHm3ovm%2B7q"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"id":"64075014222","properties":{"hs_createdate":"2024-11-07T11:51:31.747Z","hs_lastmodifieddate":"2024-11-07T11:51:32.654Z","hs_object_id":"64075014222","hs_task_status":"NOT_STARTED","hs_task_subject":"title + of task"},"createdAt":"2024-11-07T11:51:31.747Z","updatedAt":"2024-11-07T11:51:32.654Z","archived":false}' + recorded_at: Thu, 07 Nov 2024 12:00:43 GMT +- request: + method: get + uri: https://api.hubapi.com/crm/v3/objects/tasks/996174569112?properties=hs_task_subject,hs_task_status + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 404 + message: Not Found + headers: + Date: + - Thu, 07 Nov 2024 12:00:43 GMT + Content-Type: + - text/html;charset=iso-8859-1 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - 8ded2a1e6979041a-CDG + Cf-Cache-Status: + - DYNAMIC + Cache-Control: + - must-revalidate,no-cache,no-store + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'false' + X-Content-Type-Options: + - nosniff + X-Hubspot-Correlation-Id: + - d1c301f2-1bb9-433e-aad4-104ffb222f05 + X-Hubspot-Notfound: + - 'true' + X-Hubspot-Ratelimit-Daily: + - '1000000' + X-Hubspot-Ratelimit-Daily-Remaining: + - '999888' + X-Hubspot-Ratelimit-Interval-Milliseconds: + - '10000' + X-Hubspot-Ratelimit-Max: + - '190' + X-Hubspot-Ratelimit-Remaining: + - '188' + X-Hubspot-Ratelimit-Secondly: + - '19' + X-Hubspot-Ratelimit-Secondly-Remaining: + - '18' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=9xe%2Br07Yj3C6LmuTTaAJc87%2FTFssIn8wdG9BZR%2FwVvBLkRBLlMedQNvsmU06DjOZoSoSRFsPrCkKgiLEVsG5dTahI7B41I%2FDuj2N99C3bh%2FMNzJtZxUoMruPxVZUkYYP80AtcF%2FJAtBOBBoZ"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: | + + + + Error 404 Not Found + +

HTTP ERROR 404

+

Resource not found

+ + + recorded_at: Thu, 07 Nov 2024 12:00:43 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/ticket.yml b/spec/fixtures/vcr_cassettes/ticket.yml new file mode 100644 index 00000000..75e7da3b --- /dev/null +++ b/spec/fixtures/vcr_cassettes/ticket.yml @@ -0,0 +1,75 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.hubapi.com/crm/v3/objects/tickets + body: + encoding: UTF-8 + string: '{"associations":[{"to":{"id":75761595194},"types":[{"associationCategory":"HUBSPOT_DEFINED","associationTypeId":16}]},{"to":{"id":25571271600},"types":[{"associationCategory":"HUBSPOT_DEFINED","associationTypeId":339}]},{"to":{"id":28806796888},"types":[{"associationCategory":"HUBSPOT_DEFINED","associationTypeId":28}]}],"properties":{"hs_pipeline":"0","hs_pipeline_stage":"1","hs_ticket_priority":"MEDIUM","subject":"test + ticket"}}' + headers: + Authorization: + - Bearer + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 201 + message: Created + headers: + Date: + - Thu, 07 Nov 2024 10:07:53 GMT + Content-Type: + - application/json;charset=utf-8 + Content-Length: + - '572' + Connection: + - keep-alive + Location: + - https://api.hubapi.com/crm/v3/objects/tickets/16174569112 + Cf-Ray: + - 8dec84d6cc7b6fc6-CDG + Cf-Cache-Status: + - DYNAMIC + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'false' + X-Content-Type-Options: + - nosniff + X-Hubspot-Correlation-Id: + - 0fce89d5-9ed8-41f5-b4c4-db3e9777c5f5 + X-Hubspot-Ratelimit-Daily: + - '1000000' + X-Hubspot-Ratelimit-Daily-Remaining: + - '999901' + X-Hubspot-Ratelimit-Interval-Milliseconds: + - '10000' + X-Hubspot-Ratelimit-Max: + - '190' + X-Hubspot-Ratelimit-Remaining: + - '189' + X-Hubspot-Ratelimit-Secondly: + - '19' + X-Hubspot-Ratelimit-Secondly-Remaining: + - '18' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=13A2W1hpXwra7kRMXj5%2B9YDL9ozfMAPvr4wxkFQDJAmX%2BnKNttItIlGV6Ez%2B1mNob714GAu%2BT%2B9g1QKiGmJNY0EQww5ErZ1Xabuzs7K9WvmiJHIhqjR%2BilBAd7AShEPpu1B0rRXy82XWCfZ3"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"id":"16174569112","properties":{"createdate":"2024-11-07T10:07:53.620Z","hs_helpdesk_sort_timestamp":"2024-11-07T10:07:53.620Z","hs_last_message_from_visitor":"false","hs_lastmodifieddate":"2024-11-07T10:07:53.620Z","hs_object_id":"16174569112","hs_object_source":"INTEGRATION","hs_object_source_id":"1024738","hs_object_source_label":"INTEGRATION","hs_pipeline":"0","hs_pipeline_stage":"1","hs_ticket_id":"16174569112","hs_ticket_priority":"MEDIUM","subject":"test + ticket"},"createdAt":"2024-11-07T10:07:53.620Z","updatedAt":"2024-11-07T10:07:53.620Z","archived":false}' + recorded_at: Thu, 07 Nov 2024 10:07:54 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/ticket_create_property.yml b/spec/fixtures/vcr_cassettes/ticket_create_property.yml new file mode 100644 index 00000000..5b1c3577 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/ticket_create_property.yml @@ -0,0 +1,77 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.hubapi.com/crm/v3/properties/ticket + body: + encoding: UTF-8 + string: '{"name":"my_new_property","groupName":"ticketinformation","description":"How + much money do you have?","fieldType":"text","formField":true,"type":"string","displayOrder":0,"label":"This + is my new property","options":[]}' + headers: + Authorization: + - Bearer + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 201 + message: Created + headers: + Date: + - Thu, 07 Nov 2024 13:45:39 GMT + Content-Type: + - application/json;charset=utf-8 + Content-Length: + - '572' + Connection: + - keep-alive + Location: + - https://api.hubapi.com/crm/v3/properties/ticket/my_new_property + Cf-Ray: + - 8dedc3d0e97e791b-CDG + Cf-Cache-Status: + - DYNAMIC + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'false' + X-Content-Type-Options: + - nosniff + X-Hubspot-Correlation-Id: + - bfb3025c-450f-406e-88fb-4abfd85f3bee + X-Hubspot-Ratelimit-Daily: + - '1000000' + X-Hubspot-Ratelimit-Daily-Remaining: + - '999885' + X-Hubspot-Ratelimit-Interval-Milliseconds: + - '10000' + X-Hubspot-Ratelimit-Max: + - '190' + X-Hubspot-Ratelimit-Remaining: + - '189' + X-Hubspot-Ratelimit-Secondly: + - '19' + X-Hubspot-Ratelimit-Secondly-Remaining: + - '18' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=e99LL9waDkXPGdwmnLIQtR64tjqWh02Zv6v46w0wnRQbKejVC64T9h47sLTNX4MiIpIByQNTUqyJpvTeIRG7I8wHWJYGEzwZPZSNlgEop7vjME1vODWAFFiqJ%2FYCI2egkHPO0pgb4OXr51dc"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"updatedAt":"2024-11-07T13:45:38.929Z","createdAt":"2024-11-07T13:45:38.929Z","name":"my_new_property","label":"This + is my new property","type":"string","fieldType":"text","description":"How + much money do you have?","groupName":"ticketinformation","options":[],"createdUserId":"5555888","updatedUserId":"5555888","displayOrder":0,"calculated":false,"externalOptions":false,"archived":false,"hasUniqueValue":false,"hidden":false,"modificationMetadata":{"archivable":true,"readOnlyDefinition":false,"readOnlyValue":false},"formField":true,"dataSensitivity":"non_sensitive"}' + recorded_at: Thu, 07 Nov 2024 13:45:39 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/ticket_find.yml b/spec/fixtures/vcr_cassettes/ticket_find.yml new file mode 100644 index 00000000..dc0a054d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/ticket_find.yml @@ -0,0 +1,149 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.hubapi.com/crm/v3/objects/tickets/16174569112 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 07 Nov 2024 10:13:53 GMT + Content-Type: + - application/json;charset=utf-8 + Content-Length: + - '385' + Connection: + - keep-alive + Cf-Ray: + - 8dec8da119892a4f-CDG + Cf-Cache-Status: + - DYNAMIC + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'false' + X-Content-Type-Options: + - nosniff + X-Hubspot-Correlation-Id: + - 31508645-2648-4e6e-97c3-7142b9b96adf + X-Hubspot-Ratelimit-Daily: + - '1000000' + X-Hubspot-Ratelimit-Daily-Remaining: + - '999898' + X-Hubspot-Ratelimit-Interval-Milliseconds: + - '10000' + X-Hubspot-Ratelimit-Max: + - '190' + X-Hubspot-Ratelimit-Remaining: + - '189' + X-Hubspot-Ratelimit-Secondly: + - '19' + X-Hubspot-Ratelimit-Secondly-Remaining: + - '18' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=SNRbl9GPqZ2HWCZB2lAEretT1%2Fqgt3p65%2FUAWuVPf6v9n%2Bz7nE31KefUEsQK1ZjS7InIo6kTNg8QhSkkrKpl6ySWW5QNhQenoEAhjeqAfXFSTJL%2Bn%2BxBjXrkSAydGd4mf2SmXe%2BhHeiU8p34"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"id":"16174569112","properties":{"content":null,"createdate":"2024-11-07T10:07:53.620Z","hs_lastmodifieddate":"2024-11-07T10:07:58.585Z","hs_object_id":"16174569112","hs_pipeline":"0","hs_pipeline_stage":"1","hs_ticket_category":null,"hs_ticket_priority":"MEDIUM","subject":"test + ticket"},"createdAt":"2024-11-07T10:07:53.620Z","updatedAt":"2024-11-07T10:07:58.585Z","archived":false}' + recorded_at: Thu, 07 Nov 2024 10:13:54 GMT +- request: + method: get + uri: https://api.hubapi.com/crm/v3/objects/tickets/996174569112 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 404 + message: Not Found + headers: + Date: + - Thu, 07 Nov 2024 10:14:12 GMT + Content-Type: + - text/html;charset=iso-8859-1 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - 8dec8e157f2a6fb4-CDG + Cf-Cache-Status: + - DYNAMIC + Cache-Control: + - must-revalidate,no-cache,no-store + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'false' + X-Content-Type-Options: + - nosniff + X-Hubspot-Correlation-Id: + - 573a8517-3029-4383-a702-e522d8d7224a + X-Hubspot-Notfound: + - 'true' + X-Hubspot-Ratelimit-Daily: + - '1000000' + X-Hubspot-Ratelimit-Daily-Remaining: + - '999897' + X-Hubspot-Ratelimit-Interval-Milliseconds: + - '10000' + X-Hubspot-Ratelimit-Max: + - '190' + X-Hubspot-Ratelimit-Remaining: + - '189' + X-Hubspot-Ratelimit-Secondly: + - '19' + X-Hubspot-Ratelimit-Secondly-Remaining: + - '18' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=4hxmisUUPmjx1eS%2Fq9QVapUy8Uu9YNkPQl31J7BY9s6i6L%2F2KeFbQNu53QFgIwsRip1QVM0tcjOhjofbw7bkpbKhQVeF5UA3LEb97HF23O37h0mWDBSCsjFyq%2FHh1mpkRlvfrqmI2avLSILz"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: | + + + + Error 404 Not Found + +

HTTP ERROR 404

+

Resource not found

+ + + recorded_at: Thu, 07 Nov 2024 10:14:12 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/ticket_update.yml b/spec/fixtures/vcr_cassettes/ticket_update.yml new file mode 100644 index 00000000..00ec35b4 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/ticket_update.yml @@ -0,0 +1,72 @@ +--- +http_interactions: +- request: + method: patch + uri: https://api.hubapi.com/crm/v3/objects/tickets/16174569112 + body: + encoding: UTF-8 + string: '{"properties":{"hs_ticket_priority":"HIGH","subject":"New name"}}' + headers: + Authorization: + - Bearer + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 07 Nov 2024 16:48:44 GMT + Content-Type: + - application/json;charset=utf-8 + Content-Length: + - '562' + Connection: + - keep-alive + Cf-Ray: + - 8deed002fbeef0fc-CDG + Cf-Cache-Status: + - DYNAMIC + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'false' + X-Content-Type-Options: + - nosniff + X-Hubspot-Correlation-Id: + - a94dd03a-71e7-4172-b88b-988ede4e72dd + X-Hubspot-Ratelimit-Daily: + - '1000000' + X-Hubspot-Ratelimit-Daily-Remaining: + - '999876' + X-Hubspot-Ratelimit-Interval-Milliseconds: + - '10000' + X-Hubspot-Ratelimit-Max: + - '190' + X-Hubspot-Ratelimit-Remaining: + - '189' + X-Hubspot-Ratelimit-Secondly: + - '19' + X-Hubspot-Ratelimit-Secondly-Remaining: + - '18' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=xgPkmXEzCbZLy9OaiIOynT6XltDMxHB920181AHREgRZN7jvsVRKBv2lMHqyd29j9bc%2B3fsAb2H1pyPBit8kRqmhyxfChbBGdWdU04sscRAMDjf4fni1VoDhJZoxQWd65J7%2BUYmiScnksVs0"}],"group":"cf-nel","max_age":604800}' + Nel: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"id":"16174569112","properties":{"createdate":"2024-11-07T10:07:53.620Z","hs_helpdesk_sort_timestamp":"2024-11-07T10:07:53.620Z","hs_lastmodifieddate":"2024-11-07T16:48:44.224Z","hs_object_id":"16174569112","hs_object_source":"INTEGRATION","hs_object_source_id":"1024738","hs_object_source_label":"INTEGRATION","hs_pipeline":"0","hs_pipeline_stage":"1","hs_ticket_id":"16174569112","hs_ticket_priority":"HIGH","hs_updated_by_user_id":"5555888","subject":"New + name"},"createdAt":"2024-11-07T10:07:53.620Z","updatedAt":"2024-11-07T16:48:44.224Z","archived":false}' + recorded_at: Thu, 07 Nov 2024 16:48:44 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/lib/hubspot/connection_spec.rb b/spec/lib/hubspot/connection_spec.rb index 3accd5ff..4ca8af1b 100644 --- a/spec/lib/hubspot/connection_spec.rb +++ b/spec/lib/hubspot/connection_spec.rb @@ -84,6 +84,56 @@ end end + describe ".patch_json" do + it "issues a PATCH request and returns the parsed body" do + path = "/some/path" + update_options = { params: {}, body: {} } + + stub_request(:patch, "https://api.hubapi.com/some/path").to_return(status: 200, body: JSON.generate(vid: 123)) + + response = Hubspot::Connection.patch_json(path, update_options) + + assert_requested( + :patch, + "https://api.hubapi.com/some/path", + { + body: "{}", + headers: { "Content-Type" => "application/json" }, + } + ) + expect(response).to eq({ "vid" => 123 }) + end + + it "logs information about the request and response" do + path = "/some/path" + update_options = { params: {}, body: {} } + + logger = stub_logger + + stub_request(:patch, "https://api.hubapi.com/some/path").to_return(status: 200, + body: JSON.generate("response body")) + + Hubspot::Connection.patch_json(path, update_options) + + expect(logger).to have_received(:info).with(<<~MSG) + Hubspot: https://api.hubapi.com/some/path. + Body: {}. + Response: 200 "response body" + MSG + end + + it "raises when the request fails" do + path = "/some/path" + update_options = { params: {}, body: {} } + + stub_request(:patch, "https://api.hubapi.com/some/path").to_return(status: 401) + + expect { + Hubspot::Connection.patch_json(path, update_options) + }.to raise_error(Hubspot::RequestError) + end + end + context 'private methods' do describe ".generate_url" do let(:path) { "/test/:email/profile" } diff --git a/spec/lib/hubspot/contact_spec.rb b/spec/lib/hubspot/contact_spec.rb index 20e0aaff..355abfd3 100644 --- a/spec/lib/hubspot/contact_spec.rb +++ b/spec/lib/hubspot/contact_spec.rb @@ -42,7 +42,7 @@ def set_property(contact) context 'without properties' do cassette - let(:email) { Faker::Internet.safe_email(name: "#{(0..3).map { (65 + rand(26)).chr }.join}#{Time.new.to_i.to_s[-5..-1]}") } + let(:email) { Faker::Internet.email(name: "#{(0..3).map { (65 + rand(26)).chr }.join}#{Time.new.to_i.to_s[-5..-1]}") } subject { described_class.create email } it 'creates a new contact' do @@ -54,7 +54,7 @@ def set_property(contact) context 'with properties' do cassette - let(:email) { Faker::Internet.safe_email(name: "#{(0..3).map { (65 + rand(26)).chr }.join}#{Time.new.to_i.to_s[-5..-1]}") } + let(:email) { Faker::Internet.email(name: "#{(0..3).map { (65 + rand(26)).chr }.join}#{Time.new.to_i.to_s[-5..-1]}") } let(:firstname) { "Allison" } let(:properties) { { firstname: firstname } } diff --git a/spec/lib/hubspot/task_spec.rb b/spec/lib/hubspot/task_spec.rb new file mode 100644 index 00000000..8ca29f46 --- /dev/null +++ b/spec/lib/hubspot/task_spec.rb @@ -0,0 +1,44 @@ +RSpec.describe Hubspot::Task do + describe 'create!' do + subject(:new_task) do + params = { + hs_task_body: 'i am a task', + hs_task_subject: 'title of task', + hs_timestamp: DateTime.now.strftime('%Q') + } + described_class.create!(params, ticket_id: 16_174_569_112) + end + + it 'creates a new task with valid properties' do + VCR.use_cassette 'task' do + expect(new_task.id).not_to be_nil + expect(new_task.properties[:hs_task_status]).to eq('NOT_STARTED') + expect(new_task.properties[:hs_task_subject]).to eq('title of task') + expect(new_task.properties[:hs_body_preview]).to eq('i am a task') + end + end + end + + describe 'find' do + let(:task_id) { 64_075_014_222 } + + subject(:existing_task) { described_class.find(task_id, 'hs_task_subject,hs_task_status') } + + it 'gets existing task' do + VCR.use_cassette 'task_find' do + expect(existing_task.id).not_to be_nil + expect(existing_task.properties[:hs_task_subject]).to eq('title of task') + end + end + + context 'when task does not exist' do + let(:task_id) { 996_174_569_112 } + + it 'returns nil' do + VCR.use_cassette 'task_find' do + expect { existing_task }.to raise_error Hubspot::NotFoundError + end + end + end + end +end diff --git a/spec/lib/hubspot/ticket_properties_spec.rb b/spec/lib/hubspot/ticket_properties_spec.rb new file mode 100644 index 00000000..2999c013 --- /dev/null +++ b/spec/lib/hubspot/ticket_properties_spec.rb @@ -0,0 +1,41 @@ +describe Hubspot::TicketProperties do + describe '.create' do + context 'with all valid parameters' do + let(:params) do + { + 'name' => 'my_new_property', + 'label' => 'This is my new property', + 'description' => 'How much money do you have?', + 'groupName' => 'ticketinformation', + 'type' => 'string', + 'fieldType' => 'text', + 'hidden' => false, + 'deleted' => false, + 'displayOrder' => 0, + 'formField' => true, + 'readOnlyValue' => false, + 'readOnlyDefinition' => false, + 'mutableDefinitionNotDeletable' => false, + 'calculated' => false, + 'externalOptions' => false, + 'displayMode' => 'current_value' + } + end + + it 'should return the valid parameters' do + VCR.use_cassette 'ticket_create_property' do + response = Hubspot::TicketProperties.create!(params) + expect(Hubspot::TicketProperties.same?(params, response.compact.except('options'))).to be true + end + end + end + + context 'with invalid parameters' do + it 'should return nil' do + VCR.use_cassette 'ticket_fail_to_create_property' do + expect(Hubspot::TicketProperties.create!({})).to be(nil) + end + end + end + end +end diff --git a/spec/lib/hubspot/ticket_spec.rb b/spec/lib/hubspot/ticket_spec.rb new file mode 100644 index 00000000..8a39ffa5 --- /dev/null +++ b/spec/lib/hubspot/ticket_spec.rb @@ -0,0 +1,63 @@ +RSpec.describe Hubspot::Ticket do + describe 'create!' do + subject(:new_ticket) do + params = { + hs_pipeline: '0', + hs_pipeline_stage: '1', + hs_ticket_priority: 'MEDIUM', + subject: 'test ticket' + } + described_class.create!(params, contact_id: 75_761_595_194, company_id: 25_571_271_600, deal_id: 28_806_796_888) + end + + it 'creates a new ticket with valid properties' do + VCR.use_cassette 'ticket' do + expect(new_ticket.id).not_to be_nil + expect(new_ticket.properties[:subject]).to eq('test ticket') + end + end + end + + describe 'find' do + let(:ticket_id) { 16_174_569_112 } + + subject(:existing_ticket) { described_class.find(ticket_id) } + + it 'gets existing ticket' do + VCR.use_cassette 'ticket_find' do + expect(existing_ticket.id).not_to be_nil + expect(existing_ticket.properties[:subject]).to eq('test ticket') + end + end + + context 'when ticket does not exist' do + let(:ticket_id) { 996_174_569_112 } + + it 'returns nil' do + VCR.use_cassette 'ticket_find' do + expect { existing_ticket }.to raise_error Hubspot::NotFoundError + end + end + end + end + + describe 'update!' do + let(:ticket_id) { 16_174_569_112 } + let(:properties) do + { + hs_ticket_priority: 'HIGH', + subject: 'New name' + } + end + + subject(:update_ticket) { described_class.update!(ticket_id, properties) } + + it 'updates existing ticket, returns the updated entity' do + VCR.use_cassette 'ticket_update' do + ticket = update_ticket + ticket.properties[:subject] = 'New name' + ticket.properties[:subject] = 'HIGH' + end + end + end +end