active_call-zoho_crm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZohoCrm::Enumerable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include Enumerable
8
+
9
+ attr_reader :path, :list_key, :facade_klass, :page, :per_page
10
+
11
+ validates :path, :list_key, :facade_klass, presence: true
12
+ validates :page, :per_page, presence: true, numericality: { greater_than_or_equal_to: 1 }
13
+ end
14
+
15
+ def initialize(path:, list_key:, facade_klass:, page: 1, per_page: Float::INFINITY)
16
+ @path = path
17
+ @list_key = list_key
18
+ @facade_klass = facade_klass
19
+ @page = page
20
+ @per_page = per_page
21
+ end
22
+
23
+ def call
24
+ self
25
+ end
26
+
27
+ def each
28
+ return to_enum(:each) unless block_given?
29
+ return if invalid?
30
+
31
+ total = 0
32
+
33
+ catch :list_end do
34
+ loop do
35
+ @response = connection.get(path, params)
36
+ validate(:response)
37
+
38
+ unless success?
39
+ raise exception_for(response, errors) if bang?
40
+
41
+ throw :list_end
42
+ end
43
+
44
+ throw :list_end unless response.body
45
+
46
+ response.body[list_key].each do |hash|
47
+ yield facade_klass.new(hash)
48
+ total += 1
49
+ throw :list_end if total >= per_page
50
+ end
51
+
52
+ break unless response.body.dig('info', 'more_records')
53
+
54
+ @_params[:page] = response.body.dig('info', 'page') + 1
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def params
62
+ @_params ||= { page: page, per_page: max_per_page_per_request }
63
+ end
64
+
65
+ def max_per_page_per_request
66
+ @_max_per_page_per_request ||= per_page.infinite? ? 100 : [per_page, 100].min
67
+ end
68
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZohoCrm
4
+ class Error < StandardError; end
5
+
6
+ class ValidationError < Error
7
+ include ActiveCall::ValidationErrorable
8
+ end
9
+
10
+ class RequestError < Error
11
+ include ActiveCall::RequestErrorable
12
+ end
13
+
14
+ # 400..499
15
+ class ClientError < RequestError; end
16
+
17
+ # 400
18
+ class BadRequestError < ClientError; end
19
+
20
+ # 401
21
+ class UnauthorizedError < ClientError; end
22
+
23
+ # 403
24
+ class ForbiddenError < ClientError; end
25
+
26
+ # 404
27
+ class NotFoundError < ClientError; end
28
+
29
+ # 405
30
+ class MethodNotAllowedError < ClientError; end
31
+
32
+ # 406
33
+ class NotAcceptableError < ClientError; end
34
+
35
+ # 407
36
+ class ProxyAuthenticationRequiredError < ClientError; end
37
+
38
+ # 408
39
+ class RequestTimeoutError < ClientError; end
40
+
41
+ # 409
42
+ class ConflictError < ClientError; end
43
+
44
+ # 410
45
+ class GoneError < ClientError; end
46
+
47
+ # 413
48
+ class PayloadTooLargeError < ClientError; end
49
+
50
+ # 415
51
+ class UnsupportedMediaTypeError < ClientError; end
52
+
53
+ # 422
54
+ class UnprocessableEntityError < ClientError; end
55
+
56
+ # 429
57
+ class TooManyRequestsError < ClientError; end
58
+
59
+ # 500..599
60
+ class ServerError < RequestError; end
61
+
62
+ # 500
63
+ class InternalServerError < ServerError; end
64
+
65
+ # 501
66
+ class NotImplementedError < ServerError; end
67
+
68
+ # 502
69
+ class BadGatewayError < ServerError; end
70
+
71
+ # 503
72
+ class ServiceUnavailableError < ServerError; end
73
+
74
+ # 504
75
+ class GatewayTimeoutError < ServerError; end
76
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::GrantToken::Facade
4
+ attr_reader :access_token, :refresh_token, :scope, :api_domain, :expires_in, :token_type
5
+
6
+ def initialize(hash)
7
+ @access_token = hash['access_token']
8
+ @api_domain = hash['api_domain']
9
+ @expires_in = hash['expires_in']
10
+ @refresh_token = hash['refresh_token']
11
+ @scope = hash['scope']
12
+ @token_type = hash['token_type']
13
+ end
14
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::GrantToken::GetService < ZohoCrm::BaseService
4
+ attr_reader :grant_token, :client_id, :client_secret
5
+
6
+ skip_callback :call, :before, :set_access_token
7
+
8
+ after_call :set_facade
9
+
10
+ delegate_missing_to :@facade
11
+ delegate :refresh_token, to: :@facade, allow_nil: true
12
+
13
+ validates :grant_token, presence: true
14
+
15
+ def initialize(grant_token:, client_id: nil, client_secret: nil)
16
+ @grant_token = grant_token
17
+ @client_id = client_id.presence || ZohoCrm::BaseService.client_id
18
+ @client_secret = client_secret.presence || ZohoCrm::BaseService.client_secret
19
+ end
20
+
21
+ # Get refresh token from grant token.
22
+ #
23
+ # ==== Examples
24
+ #
25
+ # service = ZohoCrm::GrantToken::GetService.call(grant_token: '', client_id: '', client_secret: '')
26
+ # service.refresh_token # => '1000.xxxx.yyyy'
27
+ # service.expires_in # => 3600
28
+ #
29
+ # POST /oauth/v2/token
30
+ def call
31
+ connection.post('oauth/v2/token', params.to_param)
32
+ end
33
+
34
+ private
35
+
36
+ def connection
37
+ @_connection ||= Faraday.new do |conn|
38
+ conn.url_prefix = 'https://accounts.zoho.com'
39
+ conn.request :retry
40
+ conn.response :json
41
+ conn.response :logger, logger, **logger_options do |logger|
42
+ logger.filter(/(code|client_id|client_secret)=([^&]+)/i, '\1=[FILTERED]')
43
+ logger.filter(/"access_token":"([^"]+)"/i, '"access_token":"[FILTERED]"')
44
+ logger.filter(/"refresh_token":"([^"]+)"/i, '"refresh_token":"[FILTERED]"')
45
+ end
46
+ conn.adapter Faraday.default_adapter
47
+ end
48
+ end
49
+
50
+ def params
51
+ {
52
+ client_id: client_id,
53
+ client_secret: client_secret,
54
+ code: grant_token,
55
+ grant_type: 'authorization_code'
56
+ }
57
+ end
58
+
59
+ def set_facade
60
+ @facade = ZohoCrm::GrantToken::Facade.new(response.body)
61
+ end
62
+
63
+ def unauthorized?
64
+ response.status == 200 && response.body.key?('error')
65
+ end
66
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::Organization::Facade
4
+ attr_reader :id, :attributes
5
+
6
+ def initialize(hash)
7
+ @id = hash['org'][0]['id']
8
+ @attributes = hash['org'][0]
9
+ end
10
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::Organization::GetService < ZohoCrm::BaseService
4
+ after_call :set_facade
5
+
6
+ delegate_missing_to :@facade
7
+
8
+ # Get a document.
9
+ #
10
+ # ==== Examples
11
+ #
12
+ # service = ZohoCrm::Organization::GetService.call
13
+ #
14
+ # service.success? # => true
15
+ # service.errors # => #<ActiveModel::Errors []>
16
+ #
17
+ # service.response # => #<Faraday::Response ...>
18
+ # service.response.status # => 200
19
+ # service.response.body # => {}
20
+ #
21
+ # service.facade # => #<ZohoCrm::Organization::Facade ...>
22
+ # service.facade.attributes
23
+ # service.attributes
24
+ #
25
+ # GET /crm/v7/org
26
+ def call
27
+ connection.get('org')
28
+ end
29
+
30
+ private
31
+
32
+ def set_facade
33
+ @facade = ZohoCrm::Organization::Facade.new(response.body)
34
+ end
35
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::Record::CreateService < ZohoCrm::BaseService
4
+ attr_reader :module_name, :data
5
+
6
+ validates :module_name, :data, presence: true
7
+
8
+ after_call :set_facade
9
+
10
+ delegate_missing_to :@facade
11
+
12
+ def initialize(module_name:, data:)
13
+ @module_name = module_name
14
+ @data = data.is_a?(Array) ? data : [data]
15
+ end
16
+
17
+ # Create a record.
18
+ #
19
+ # ==== Examples
20
+ #
21
+ # service = ZohoCrm::Record::CreateService.call(
22
+ # module_name: 'Contacts',
23
+ # data: {
24
+ # 'Email' => 'eric.cartman@example.com',
25
+ # 'First_Name' => 'Eric',
26
+ # 'Last_Name' => 'Cartman'
27
+ # }
28
+ # )
29
+ #
30
+ # service.success? # => true
31
+ # service.errors # => #<ActiveModel::Errors []>
32
+ #
33
+ # service.response # => #<Faraday::Response ...>
34
+ # service.response.status # => 201
35
+ # service.response.body # => {}
36
+ #
37
+ # service.facade # => #<ZohoCrm::Record::Facade ...>
38
+ # service.facade.attributes
39
+ # service.attributes
40
+ #
41
+ # POST /crm/v7/:module_name
42
+ def call
43
+ connection.post(module_name, **params)
44
+ end
45
+
46
+ private
47
+
48
+ def params
49
+ {
50
+ data: data
51
+ }
52
+ end
53
+
54
+ def set_facade
55
+ @facade = ZohoCrm::Record::Facade.new(response.body)
56
+ end
57
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::Record::DeleteService < ZohoCrm::BaseService
4
+ attr_reader :module_name, :id
5
+
6
+ validates :module_name, :id, presence: true
7
+
8
+ after_call :set_facade
9
+
10
+ delegate_missing_to :@facade
11
+
12
+ def initialize(module_name:, id:)
13
+ @module_name = module_name
14
+ @id = id
15
+ end
16
+
17
+ # Delete a record.
18
+ #
19
+ # ==== Examples
20
+ #
21
+ # service = ZohoCrm::Record::DeleteService.call(module_name: 'Contacts', id: '')
22
+ #
23
+ # service.success? # => true
24
+ # service.errors # => #<ActiveModel::Errors []>
25
+ #
26
+ # service.response # => #<Faraday::Response ...>
27
+ # service.response.status # => 200
28
+ # service.response.body # => {}
29
+ #
30
+ # service.facade # => #<ZohoCrm::Record::Facade ...>
31
+ # service.facade.attributes
32
+ # service.attributes
33
+ #
34
+ # DELETE /crm/v7/:module_name/:id
35
+ def call
36
+ connection.delete("#{module_name}/#{id}")
37
+ end
38
+
39
+ private
40
+
41
+ def set_facade
42
+ @facade = ZohoCrm::Record::Facade.new(response.body)
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::Record::Facade
4
+ attr_reader :id, :attributes
5
+
6
+ def initialize(hash)
7
+ @id = hash['data'] ? (hash['data'][0]['id'] || hash['data'][0].dig('details', 'id')) : hash['id']
8
+ @attributes = hash['data'] ? hash['data'][0] : hash
9
+ end
10
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::Record::GetService < ZohoCrm::BaseService
4
+ attr_reader :module_name, :id
5
+
6
+ validates :module_name, :id, presence: true
7
+
8
+ after_call :set_facade
9
+
10
+ delegate_missing_to :@facade
11
+
12
+ def initialize(module_name:, id:)
13
+ @module_name = module_name
14
+ @id = id
15
+ end
16
+
17
+ # Get a document.
18
+ #
19
+ # ==== Examples
20
+ #
21
+ # service = ZohoCrm::Record::GetService.call(module_name: 'Contacts', id: '')
22
+ #
23
+ # service.success? # => true
24
+ # service.errors # => #<ActiveModel::Errors []>
25
+ #
26
+ # service.response # => #<Faraday::Response ...>
27
+ # service.response.status # => 200
28
+ # service.response.body # => {}
29
+ #
30
+ # service.facade # => #<ZohoCrm::Record::Facade ...>
31
+ # service.facade.attributes
32
+ # service.attributes
33
+ #
34
+ # GET /crm/v7/:module_name/:id
35
+ def call
36
+ connection.get("#{module_name}/#{id}")
37
+ end
38
+
39
+ private
40
+
41
+ def set_facade
42
+ @facade = ZohoCrm::Record::Facade.new(response.body)
43
+ end
44
+
45
+ def not_found?
46
+ response.status == 204
47
+ end
48
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::Record::ListService < ZohoCrm::BaseService
4
+ SORT_COLUMNS = %w[id Created_Time Modified_Time].freeze
5
+ SORT_ORDERS = %w[asc desc].freeze
6
+
7
+ include ZohoCrm::Enumerable
8
+
9
+ attr_reader :module_name, :fields, :sort_by, :sort_order
10
+
11
+ validates :module_name, :fields, presence: true
12
+ validates :sort_order, inclusion: { in: SORT_ORDERS, message: "Sort order must be one of #{SORT_ORDERS.join(', ')}" }
13
+
14
+ validates :sort_by, inclusion: {
15
+ in: SORT_COLUMNS,
16
+ message: "Sort by must be one of #{SORT_COLUMNS.join(', ')}"
17
+ }
18
+
19
+ # List records.
20
+ #
21
+ # ==== Examples
22
+ #
23
+ # service = ZohoCrm::Record::ListService.call(module_name: 'Contacts', fields: 'Email,Last_Name').first
24
+ # service.id
25
+ #
26
+ # If you don't provide the `per_page` argument, multiple API requests will be made untill all records have been
27
+ # returned. You could be rate limited, so use wisely.
28
+ #
29
+ # ZohoCrm::Record::ListService.call(module_name: 'Contacts', fields: 'Email,Last_Name', page: 1, per_page: 10).each { _1 }
30
+ #
31
+ # Sort by column.
32
+ #
33
+ # ZohoCrm::Record::ListService.call(module_name: 'Contacts', fields: 'Email,Last_Name', sort_by: 'id', sort_order: 'asc').map { _1 }
34
+ #
35
+ # Columns to sort by are `id`, `Created_Time` and `Modified_Time`.
36
+ #
37
+ # GET /crm/v7/:module_name
38
+ def initialize(module_name:, fields:, page: 1, per_page: Float::INFINITY, sort_by: 'id', sort_order: 'desc')
39
+ @module_name = module_name
40
+ @fields = fields
41
+ @sort_by = sort_by
42
+ @sort_order = sort_order
43
+
44
+ super(
45
+ path: module_name,
46
+ list_key: 'data',
47
+ facade_klass: ZohoCrm::Record::Facade,
48
+ page: page,
49
+ per_page: per_page
50
+ )
51
+ end
52
+
53
+ private
54
+
55
+ def params
56
+ @_params ||= {
57
+ page: page,
58
+ per_page: max_per_page_per_request,
59
+ sort_by: sort_by,
60
+ sort_order: sort_order,
61
+ fields: fields
62
+ }
63
+ end
64
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::Record::SearchService < ZohoCrm::BaseService
4
+ SORT_COLUMNS = %w[id Created_Time Modified_Time].freeze
5
+ SORT_ORDERS = %w[asc desc].freeze
6
+
7
+ include ZohoCrm::Enumerable
8
+
9
+ attr_reader :module_name, :sort_by, :sort_order, :email, :phone, :criteria, :word
10
+
11
+ validates :module_name, presence: true
12
+ validates :sort_order, inclusion: { in: SORT_ORDERS, message: "Sort order must be one of #{SORT_ORDERS.join(', ')}" }
13
+
14
+ validates :sort_by, inclusion: {
15
+ in: SORT_COLUMNS,
16
+ message: "Sort by must be one of #{SORT_COLUMNS.join(', ')}"
17
+ }
18
+
19
+ # List records.
20
+ #
21
+ # ==== Examples
22
+ #
23
+ # service = ZohoCrm::Record::SearchService.call(module_name: 'Contacts', email: 'eric.cartman@example.com').first
24
+ # service.id
25
+ #
26
+ # If you don't provide the `per_page` argument, multiple API requests will be made untill all records have been
27
+ # returned. You could be rate limited, so use wisely.
28
+ #
29
+ # ZohoCrm::Record::SearchService.call(module_name: 'Contacts', email: 'eric.cartman@example.com', page: 1, per_page: 10).each { _1 }
30
+ #
31
+ # Sort by column.
32
+ #
33
+ # ZohoCrm::Record::SearchService.call(module_name: 'Contacts', email: 'eric.cartman@example.com', sort_by: 'Modified_Time', sort_order: 'asc').map { _1 }
34
+ #
35
+ # Columns to sort by are `id`, `Created_Time` and `Modified_Time`.
36
+ #
37
+ # Search by email.
38
+ #
39
+ # ZohoCrm::Record::SearchService.call(module_name: 'Contacts', email: 'eric.cartman@example.com').map { _1 }
40
+ #
41
+ # Search by phone.
42
+ #
43
+ # ZohoCrm::Record::SearchService.call(module_name: 'Contacts', phone: '0123456789').map { _1 }
44
+ #
45
+ # Search by criteria.
46
+ #
47
+ # ZohoCrm::Record::SearchService.call(module_name: 'Contacts', criteria: 'Created_Time:between:2025-01-01T06:00:00+00:00,2025-01-30T06:00:00+00:00').map { _1 }
48
+ #
49
+ # Search by word.
50
+ #
51
+ # ZohoCrm::Record::SearchService.call(module_name: 'Contacts', word: 'eric').map { _1 }
52
+ #
53
+ # GET /crm/v7/:module_name
54
+ def initialize(module_name:, page: 1, per_page: Float::INFINITY, sort_by: 'id', sort_order: 'desc', email: nil, phone: nil, criteria: nil, word: nil)
55
+ @module_name = module_name
56
+ @sort_by = sort_by
57
+ @sort_order = sort_order
58
+ @email = email
59
+ @phone = phone
60
+ @criteria = criteria
61
+ @word = word
62
+
63
+ super(
64
+ path: "#{module_name}/search",
65
+ list_key: 'data',
66
+ facade_klass: ZohoCrm::Record::Facade,
67
+ page: page,
68
+ per_page: per_page
69
+ )
70
+ end
71
+
72
+ private
73
+
74
+ def params
75
+ @_params ||= {
76
+ page: page,
77
+ per_page: max_per_page_per_request,
78
+ sort_by: sort_by,
79
+ sort_order: sort_order,
80
+ email: email,
81
+ phone: phone,
82
+ criteria: criteria,
83
+ word: word
84
+ }.compact_blank
85
+ end
86
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ZohoCrm::Record::UpdateService < ZohoCrm::BaseService
4
+ attr_reader :module_name, :data
5
+
6
+ validates :module_name, :data, presence: true
7
+
8
+ after_call :set_facade
9
+
10
+ delegate_missing_to :@facade
11
+
12
+ def initialize(module_name:, data:)
13
+ @module_name = module_name
14
+ @data = data.is_a?(Array) ? data : [data]
15
+ end
16
+
17
+ # Create a record.
18
+ #
19
+ # ==== Examples
20
+ #
21
+ # service = ZohoCrm::Record::UpdateService.call(
22
+ # module_name: 'Contacts',
23
+ # data: {
24
+ # 'id' => '',
25
+ # 'First_Name' => 'Eric Theodore'
26
+ # }
27
+ # )
28
+ #
29
+ # service.success? # => true
30
+ # service.errors # => #<ActiveModel::Errors []>
31
+ #
32
+ # service.response # => #<Faraday::Response ...>
33
+ # service.response.status # => 200
34
+ # service.response.body # => {}
35
+ #
36
+ # service.facade # => #<ZohoCrm::Record::Facade ...>
37
+ # service.facade.attributes
38
+ # service.attributes
39
+ #
40
+ # PUT /crm/v7/:module_name
41
+ def call
42
+ connection.put(module_name, **params)
43
+ end
44
+
45
+ private
46
+
47
+ def params
48
+ {
49
+ data: data
50
+ }
51
+ end
52
+
53
+ def set_facade
54
+ @facade = ZohoCrm::Record::Facade.new(response.body)
55
+ end
56
+ end