client_success 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +466 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +111 -0
  8. data/README.md +52 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/client_success.gemspec +44 -0
  13. data/lib/client_success/access_token.rb +25 -0
  14. data/lib/client_success/client.rb +115 -0
  15. data/lib/client_success/client_type.rb +18 -0
  16. data/lib/client_success/connection.rb +99 -0
  17. data/lib/client_success/contact.rb +104 -0
  18. data/lib/client_success/domain_model/access_token.rb +11 -0
  19. data/lib/client_success/domain_model/client.rb +14 -0
  20. data/lib/client_success/domain_model/client_type.rb +14 -0
  21. data/lib/client_success/domain_model/contact.rb +14 -0
  22. data/lib/client_success/domain_model/custom_field_value.rb +9 -0
  23. data/lib/client_success/domain_model/employee.rb +14 -0
  24. data/lib/client_success/domain_model/product.rb +10 -0
  25. data/lib/client_success/domain_model/security_role.rb +9 -0
  26. data/lib/client_success/domain_model/status.rb +9 -0
  27. data/lib/client_success/domain_model/subscription.rb +12 -0
  28. data/lib/client_success/domain_model/success_cycle.rb +9 -0
  29. data/lib/client_success/domain_model/to_do.rb +9 -0
  30. data/lib/client_success/employee.rb +18 -0
  31. data/lib/client_success/product.rb +16 -0
  32. data/lib/client_success/schema/client/create.rb +34 -0
  33. data/lib/client_success/schema/client/update.rb +37 -0
  34. data/lib/client_success/schema/contact/create.rb +28 -0
  35. data/lib/client_success/schema/contact/update.rb +27 -0
  36. data/lib/client_success/schema/subscription/create.rb +20 -0
  37. data/lib/client_success/schema/subscription/update.rb +21 -0
  38. data/lib/client_success/status.rb +23 -0
  39. data/lib/client_success/subscription.rb +51 -0
  40. data/lib/client_success/to_do.rb +23 -0
  41. data/lib/client_success/types.rb +7 -0
  42. data/lib/client_success/version.rb +3 -0
  43. data/lib/client_success.rb +18 -0
  44. metadata +266 -0
@@ -0,0 +1,44 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "client_success/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "client_success"
7
+ spec.version = ClientSuccess::VERSION
8
+ spec.authors = ["Practice Ignition"]
9
+ spec.email = ["dev@practiceignition.com"]
10
+
11
+ spec.summary = "An unofficial Ruby wrapper for Client Success's REST API"
12
+ spec.homepage = "https://github.com/ignitionapp/"
13
+
14
+ # Specify which files should be added to the gem when it is released.
15
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
16
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
17
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency("bundler", "~> 2.0")
24
+ spec.add_development_dependency("rake", ">= 12.3.3")
25
+ spec.add_development_dependency("rspec", "~> 3.0")
26
+
27
+ spec.add_development_dependency("vcr")
28
+ spec.add_development_dependency("faker")
29
+
30
+ spec.add_development_dependency("rubocop", "0.77.0")
31
+ spec.add_development_dependency("rubocop-rspec")
32
+
33
+ spec.add_runtime_dependency("faraday")
34
+ spec.add_runtime_dependency("faraday_middleware")
35
+
36
+ # TODO: don't force the use of typhoeus
37
+ spec.add_runtime_dependency("typhoeus")
38
+
39
+ spec.add_runtime_dependency("dry-types", "0.11.0")
40
+ spec.add_runtime_dependency("hashie")
41
+
42
+ # TODO: remove activesupport as a dependency
43
+ spec.add_runtime_dependency("activesupport", "~> 5.2")
44
+ end
@@ -0,0 +1,25 @@
1
+ require_relative "connection"
2
+ require_relative "domain_model/access_token"
3
+
4
+ module ClientSuccess
5
+ module AccessToken
6
+ extend self
7
+
8
+ class Error < StandardError; end
9
+ class InvalidCredentials < Error; end
10
+
11
+ def create(username:, password:,
12
+ connection: ClientSuccess::Connection.new)
13
+ response = connection.post("/v1/auth",
14
+ username: username,
15
+ password: password)
16
+
17
+ payload = response.body
18
+
19
+ DomainModel::AccessToken.new(
20
+ payload.deep_transform_keys(&:underscore))
21
+ rescue Connection::Unauthorised
22
+ raise InvalidCredentials, "invalid username or password"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,115 @@
1
+ require_relative "connection"
2
+ require_relative "domain_model/client"
3
+ require_relative "schema/client/create"
4
+ require_relative "schema/client/update"
5
+
6
+ module ClientSuccess
7
+ module Client
8
+ extend self
9
+
10
+ class Error < StandardError; end
11
+ class NotFound < Error; end
12
+
13
+ def list_all(assigned_csm_id: nil, active_only: true,
14
+ connection:)
15
+ params = {
16
+ "assignedCsmId" => assigned_csm_id,
17
+ "activeOnly" => active_only
18
+ }
19
+
20
+ response = connection.get(
21
+ "/v1/clients?#{params.compact.to_query}")
22
+
23
+ response.body.map do |payload|
24
+ DomainModel::Client.new(
25
+ payload.deep_transform_keys(&:underscore))
26
+ end
27
+ end
28
+
29
+ def get_details(client_id:, connection:)
30
+ response = connection.get(
31
+ "/v1/clients/#{client_id}")
32
+
33
+ payload = response.body
34
+
35
+ DomainModel::Client.new(
36
+ payload.deep_transform_keys(&:underscore))
37
+ rescue Connection::NotFound
38
+ raise NotFound, "client with id '#{client_id}' not found"
39
+ end
40
+
41
+ def get_details_by_external_id(external_id:, connection:)
42
+ params = {
43
+ "externalId" => external_id
44
+ }
45
+
46
+ response = connection.get(
47
+ "/v1/clients?#{params.compact.to_query}")
48
+
49
+ payload = response.body
50
+
51
+ DomainModel::Client.new(
52
+ payload.deep_transform_keys(&:underscore))
53
+ rescue Connection::NotFound
54
+ raise NotFound, "client with external id '#{external_id}' not found"
55
+ end
56
+
57
+ def create(attributes:, connection:)
58
+ body = Schema::Client::Create[attributes]
59
+ .transform_keys { |k| k.to_s.camelize(:lower) }
60
+ .to_json
61
+
62
+ response = connection.post(
63
+ "/v1/clients", body)
64
+
65
+ payload = response.body
66
+
67
+ # TODO: find a better way to deal with api weirdness here
68
+ payload["customFieldValues"].compact!
69
+
70
+ DomainModel::Client.new(
71
+ payload.deep_transform_keys(&:underscore))
72
+ end
73
+
74
+ # NOTE: according to the api documentation, this is _not_ a PATCH operation,
75
+ # i.e. any fields not supplied will be set to null - so make sure you
76
+ # supply everything :)
77
+ def update(client_id:, attributes:, connection:)
78
+ body = attributes
79
+ .deep_transform_keys { |k| k.to_s.camelize(:lower) }
80
+ .to_json
81
+
82
+ response = connection.put(
83
+ "/v1/clients/#{client_id}", body)
84
+
85
+ payload = response.body
86
+
87
+ # TODO: find a better way to deal with api weirdness here
88
+ payload["customFieldValues"].compact!
89
+
90
+ DomainModel::Client.new(
91
+ payload.deep_transform_keys(&:underscore))
92
+ end
93
+
94
+ def update_custom_field(client_id:, custom_field_name:, value:, connection:)
95
+ body = {
96
+ custom_field_name => value.to_s
97
+ }
98
+
99
+ begin
100
+ connection.patch(
101
+ "/v1/customfield/value/client/#{client_id}", body)
102
+ rescue Connection::ParsingError => error
103
+ # NOTE: request submitted to resolve invalid JSON response from client
104
+ # success for this endpoint
105
+ raise error unless error.message =~ /Update successful/
106
+ end
107
+ end
108
+
109
+ def delete(client_id:, connection:)
110
+ # TODO: handle response
111
+ connection.delete(
112
+ "/v1/clients/#{client_id}")
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "connection"
2
+ require_relative "domain_model/client_type"
3
+
4
+ module ClientSuccess
5
+ module ClientType
6
+ extend self
7
+
8
+ def list_all(connection:)
9
+ response = connection.get(
10
+ "/v1/client-segments")
11
+
12
+ response.body.map do |payload|
13
+ DomainModel::ClientType.new(
14
+ payload.deep_transform_keys(&:underscore))
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,99 @@
1
+ require "faraday"
2
+ require "faraday_middleware"
3
+
4
+ module ClientSuccess
5
+ class Connection
6
+ class Error < StandardError; end
7
+
8
+ class BadRequest < Error; end
9
+ class Unauthorised < Error; end
10
+ class NotFound < Error; end
11
+
12
+ class ParsingError < Error; end
13
+
14
+ class << self
15
+ def authorised(access_token)
16
+ new.tap { |conn| conn.access_token = access_token }
17
+ end
18
+ end
19
+
20
+ def logger
21
+ # TODO: something better than this
22
+ Logger.new(STDOUT).tap do |l|
23
+ l.level = Logger::WARN
24
+ end
25
+ end
26
+
27
+ def access_token
28
+ headers["Authorization"]
29
+ end
30
+
31
+ def access_token=(access_token)
32
+ headers["Authorization"] = access_token
33
+ end
34
+
35
+ def get(path, headers = {}, &block)
36
+ request { adapter.get(path, headers, &block) }
37
+ end
38
+
39
+ def head(path, headers = {}, &block)
40
+ raise NotImplementedError
41
+ end
42
+
43
+ def post(path, body = nil, headers = {}, &block)
44
+ request { adapter.post(path, body, headers, &block) }
45
+ end
46
+
47
+ def put(path, body = nil, headers = {}, &block)
48
+ request { adapter.put(path, body, headers, &block) }
49
+ end
50
+
51
+ def patch(path, body = nil, headers = {}, &block)
52
+ request { adapter.patch(path, body, headers, &block) }
53
+ end
54
+
55
+ def delete(path, headers = {}, &block)
56
+ request { adapter.delete(path, headers, &block) }
57
+ end
58
+
59
+ def options(path, headers = {}, &block)
60
+ raise NotImplementedError
61
+ end
62
+
63
+ private
64
+
65
+ def headers
66
+ adapter.headers
67
+ end
68
+
69
+ def adapter
70
+ @adapter ||= Faraday.new(url: "https://api.clientsuccess.com") do |faraday|
71
+ faraday.request(:json)
72
+ faraday.response(:json, content_type: /\bjson$/)
73
+ faraday.response(:logger, logger)
74
+ faraday.use(Faraday::Response::RaiseError)
75
+ faraday.adapter(Faraday.default_adapter)
76
+ end
77
+ end
78
+
79
+ def request
80
+ yield
81
+ rescue Faraday::ParsingError => error
82
+ raise ParsingError, error
83
+ rescue Faraday::ClientError => error
84
+ case error.response[:status]
85
+ when 400
86
+ # TODO: parse out userMessage from the response here
87
+ raise BadRequest, error
88
+ when 401
89
+ raise Unauthorised, error
90
+ when 404
91
+ raise NotFound, error
92
+ else
93
+ raise Error, error
94
+ end
95
+ rescue SignalException => error
96
+ raise Error, error
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,104 @@
1
+ require_relative "domain_model/contact"
2
+ require_relative "schema/contact/create"
3
+ require_relative "schema/contact/update"
4
+
5
+ module ClientSuccess
6
+ module Contact
7
+ extend self
8
+
9
+ class Error < StandardError; end
10
+ class NotFound < Error; end
11
+
12
+ def list_all(client_id:, connection:)
13
+ response = connection.get(
14
+ "/v1/clients/#{client_id}/contacts")
15
+
16
+ response.body.map do |payload|
17
+ DomainModel::Contact.new(
18
+ payload.deep_transform_keys(&:underscore))
19
+ end
20
+ end
21
+
22
+ def get_summary(client_id:, contact_id:, connection:)
23
+ response = connection.get(
24
+ "/v1/clients/#{client_id}/contacts/#{contact_id}")
25
+
26
+ payload = response.body
27
+
28
+ DomainModel::Contact.new(
29
+ payload.deep_transform_keys(&:underscore))
30
+ end
31
+
32
+ def get_details(client_id:, contact_id:, connection:)
33
+ response = connection.get(
34
+ "/v1/clients/#{client_id}/contacts/#{contact_id}/details")
35
+
36
+ payload = response.body
37
+
38
+ DomainModel::Contact.new(
39
+ payload.deep_transform_keys(&:underscore))
40
+ end
41
+
42
+ def create(client_id:, attributes:, connection:)
43
+ body = Schema::Contact::Create[attributes]
44
+ .transform_keys { |k| k.to_s.camelize(:lower) }
45
+ .to_json
46
+
47
+ response = connection.post(
48
+ "/v1/clients/#{client_id}/contacts", body)
49
+
50
+ payload = response.body
51
+
52
+ DomainModel::Contact.new(
53
+ payload.deep_transform_keys(&:underscore))
54
+ end
55
+
56
+ def update(id:, client_id:, attributes:, connection:)
57
+ body = Schema::Contact::Update[attributes]
58
+ .transform_keys { |k| k.to_s.camelize(:lower) }
59
+ .to_json
60
+
61
+ response = connection.put("/v1/clients/#{client_id}/contacts/#{id}/details", body)
62
+
63
+ payload = response.body
64
+
65
+ DomainModel::Contact.new(
66
+ payload.deep_transform_keys(&:underscore))
67
+ end
68
+
69
+ def delete(id:, client_id:, connection:)
70
+ connection.delete("/v1/clients/#{client_id}/contacts/#{id}")
71
+ end
72
+
73
+ def search_by_email(email:, connection:)
74
+ search(term: email, connection: connection)
75
+ .reject { |contact| contact["email"].nil? }
76
+ .select { |contact| contact["email"].downcase == email.downcase }
77
+ end
78
+
79
+ def get_details_by_client_external_id_and_email(client_external_id:, email:, connection:)
80
+ params = {
81
+ "clientExternalId" => client_external_id,
82
+ "email" => email
83
+ }
84
+
85
+ response = connection.get(
86
+ "/v1/contacts?#{params.compact.to_query}")
87
+
88
+ # for some reason the ClientSuccess API does not return a 404
89
+ # but instead a body containing the string "null" if the contact is not found
90
+ # this is probably a security restriction on their end, but we will instead raise an error
91
+
92
+ if response.body.blank?
93
+ raise NotFound, "contact with email '#{email}' not found on client '#{client_external_id}'"
94
+ else
95
+ payload = response.body
96
+ DomainModel::Contact.new(payload.deep_transform_keys(&:underscore))
97
+ end
98
+ end
99
+
100
+ def search(term:, connection:)
101
+ connection.get("/v1/contacts/search?term='#{term}'").body
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,11 @@
1
+ module ClientSuccess
2
+ module DomainModel
3
+ class AccessToken < Hashie::Dash
4
+ include Hashie::Extensions::IndifferentAccess
5
+
6
+ property :access_token
7
+ property :token_type
8
+ property :expires_in
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "custom_field_value"
2
+
3
+ module ClientSuccess
4
+ module DomainModel
5
+ class Client < Hash
6
+ include Hashie::Extensions::MergeInitializer
7
+ include Hashie::Extensions::IndifferentAccess
8
+ include Hashie::Extensions::MethodAccess
9
+ include Hashie::Extensions::Coercion
10
+
11
+ coerce_key :custom_field_values, Array[CustomFieldValue]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "success_cycle"
2
+
3
+ module ClientSuccess
4
+ module DomainModel
5
+ class ClientType < Hash
6
+ include Hashie::Extensions::MergeInitializer
7
+ include Hashie::Extensions::IndifferentAccess
8
+ include Hashie::Extensions::MethodAccess
9
+ include Hashie::Extensions::Coercion
10
+
11
+ coerce_key :success_cycles, Array[SuccessCycle]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # require_relative "custom_field_value"
2
+
3
+ module ClientSuccess
4
+ module DomainModel
5
+ class Contact < Hash
6
+ include Hashie::Extensions::MergeInitializer
7
+ include Hashie::Extensions::IndifferentAccess
8
+ include Hashie::Extensions::MethodAccess
9
+ # include Hashie::Extensions::Coercion
10
+
11
+ # coerce_key :custom_field_values, Array[CustomFieldValue]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module ClientSuccess
2
+ module DomainModel
3
+ class CustomFieldValue < Hash
4
+ include Hashie::Extensions::MergeInitializer
5
+ include Hashie::Extensions::IndifferentAccess
6
+ include Hashie::Extensions::MethodAccess
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "security_role"
2
+
3
+ module ClientSuccess
4
+ module DomainModel
5
+ class Employee < Hash
6
+ include Hashie::Extensions::MergeInitializer
7
+ include Hashie::Extensions::IndifferentAccess
8
+ include Hashie::Extensions::MethodAccess
9
+ include Hashie::Extensions::Coercion
10
+
11
+ coerce_key :security_roles, Array[SecurityRole]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ module ClientSuccess
2
+ module DomainModel
3
+ class Product < Hash
4
+ include Hashie::Extensions::MergeInitializer
5
+ include Hashie::Extensions::IndifferentAccess
6
+ include Hashie::Extensions::MethodAccess
7
+ include Hashie::Extensions::Coercion
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module ClientSuccess
2
+ module DomainModel
3
+ class SecurityRole < Hash
4
+ include Hashie::Extensions::MergeInitializer
5
+ include Hashie::Extensions::IndifferentAccess
6
+ include Hashie::Extensions::MethodAccess
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module ClientSuccess
2
+ module DomainModel
3
+ class Status < Hash
4
+ include Hashie::Extensions::MergeInitializer
5
+ include Hashie::Extensions::IndifferentAccess
6
+ include Hashie::Extensions::MethodAccess
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "custom_field_value"
2
+
3
+ module ClientSuccess
4
+ module DomainModel
5
+ class Subscription < Hash
6
+ include Hashie::Extensions::MergeInitializer
7
+ include Hashie::Extensions::IndifferentAccess
8
+ include Hashie::Extensions::MethodAccess
9
+ include Hashie::Extensions::Coercion
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module ClientSuccess
2
+ module DomainModel
3
+ class SuccessCycle < Hash
4
+ include Hashie::Extensions::MergeInitializer
5
+ include Hashie::Extensions::IndifferentAccess
6
+ include Hashie::Extensions::MethodAccess
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module ClientSuccess
2
+ module DomainModel
3
+ class ToDo < Hash
4
+ include Hashie::Extensions::MergeInitializer
5
+ include Hashie::Extensions::IndifferentAccess
6
+ include Hashie::Extensions::MethodAccess
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "connection"
2
+ require_relative "domain_model/employee"
3
+
4
+ module ClientSuccess
5
+ module Employee
6
+ extend self
7
+
8
+ def list_all(connection:)
9
+ response = connection.get(
10
+ "/v1/employees")
11
+
12
+ response.body.map do |payload|
13
+ DomainModel::Employee.new(
14
+ payload.deep_transform_keys(&:underscore))
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ require_relative "domain_model/product"
2
+
3
+ module ClientSuccess
4
+ module Product
5
+ extend self
6
+
7
+ def list_all(connection:)
8
+ response = connection.get("/v1/products")
9
+
10
+ response.body.map do |payload|
11
+ DomainModel::Product.new(
12
+ payload.deep_transform_keys(&:underscore))
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ require_relative "../../types"
2
+
3
+ module ClientSuccess
4
+ module Schema
5
+ module Client
6
+ # TODO: upgrade dry types to use latest schema functions
7
+ Create = Types::Hash.schema(
8
+ name: Types::Strict::String,
9
+ site_url: Types::Strict::String,
10
+ zip: Types::Strict::String,
11
+ status_id: Types::Coercible::Int,
12
+ inception_date: Types::Strict::String, # TODO
13
+ created_by_employee_id: Types::Coercible::Int,
14
+ linkedin_url: Types::Strict::String,
15
+ managed_by_employee_id: Types::Coercible::Int,
16
+ active: Types::Strict::Bool,
17
+ active_client_success_cycle_id: Types::Coercible::Int,
18
+ zendesk_id: Types::Coercible::String,
19
+ desk_id: Types::Coercible::String,
20
+ freshdesk_id: Types::Coercible::String,
21
+ jira_id: Types::Coercible::String,
22
+ terminated_date: Types::Strict::String, # TODO
23
+ assigned_sales_rep: Types::Coercible::Int,
24
+ salesforce_account_id: Types::Coercible::String,
25
+ usage_id: Types::Coercible::String,
26
+ street: Types::Strict::String,
27
+ state: Types::Strict::String,
28
+ country: Types::Strict::String,
29
+ city: Types::Strict::String,
30
+ external_id: Types::Coercible::String
31
+ )
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ require_relative "../../types"
2
+
3
+ module ClientSuccess
4
+ module Schema
5
+ module Client
6
+ # TODO: upgrade dry types to use latest schema functions
7
+ Update = Types::Hash.schema(
8
+ external_id: Types::Coercible::String,
9
+ name: Types::Strict::String,
10
+ site_url: Types::Strict::String,
11
+ client_segment_id: Types::Coercible::Int,
12
+ zip: Types::Strict::String,
13
+ modified_by_employee_id: Types::Coercible::Int,
14
+ status_id: Types::Coercible::Int,
15
+ inception_date: Types::Strict::String, # TODO
16
+ created_by_employee: Types::Coercible::Int,
17
+ linkedin_url: Types::Strict::String,
18
+ managed_by_employee_id: Types::Coercible::Int,
19
+ active: Types::Strict::Bool,
20
+ success_score: Types::Strict::Int,
21
+ active_client_success_cycle_id: Types::Coercible::Int,
22
+ crm_customer_id: Types::Coercible::String,
23
+ crm_customer_url: Types::Strict::String,
24
+ zendesk_id: Types::Coercible::String,
25
+ desk_id: Types::Coercible::String,
26
+ freshdesk_id: Types::Coercible::String,
27
+ uservoice_id: Types::Coercible::String,
28
+ assigned_sales_rep: Types::Strict::String,
29
+ custom_field_values: Types::Strict::Array.default([]), # TODO
30
+ success_cycle_id: Types::Coercible::Int,
31
+ salesforce_account_id: Types::Coercible::String,
32
+ jira_id: Types::Coercible::String,
33
+ nps_score: Types::Strict::Int
34
+ )
35
+ end
36
+ end
37
+ end