attio 0.1.3 → 0.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +3 -51
- data/CHANGELOG.md +62 -1
- data/CLAUDE.md +360 -0
- data/CONCEPTS.md +428 -0
- data/Gemfile.lock +3 -1
- data/README.md +304 -7
- data/examples/advanced_filtering.rb +178 -0
- data/examples/basic_usage.rb +110 -0
- data/examples/collaboration_example.rb +173 -0
- data/examples/full_workflow.rb +348 -0
- data/examples/notes_and_tasks.rb +200 -0
- data/lib/attio/client.rb +86 -0
- data/lib/attio/errors.rb +30 -2
- data/lib/attio/http_client.rb +16 -4
- data/lib/attio/rate_limiter.rb +212 -0
- data/lib/attio/resources/base.rb +70 -6
- data/lib/attio/resources/bulk.rb +290 -0
- data/lib/attio/resources/comments.rb +147 -0
- data/lib/attio/resources/deals.rb +183 -0
- data/lib/attio/resources/lists.rb +9 -21
- data/lib/attio/resources/meta.rb +72 -0
- data/lib/attio/resources/notes.rb +110 -0
- data/lib/attio/resources/records.rb +11 -24
- data/lib/attio/resources/tasks.rb +131 -0
- data/lib/attio/resources/threads.rb +154 -0
- data/lib/attio/resources/workspace_members.rb +103 -0
- data/lib/attio/version.rb +1 -1
- data/lib/attio.rb +9 -0
- metadata +17 -1
@@ -0,0 +1,290 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Attio
|
4
|
+
module Resources
|
5
|
+
# Bulk operations for efficient batch processing
|
6
|
+
#
|
7
|
+
# @example Bulk create records
|
8
|
+
# client.bulk.create_records(
|
9
|
+
# object: "companies",
|
10
|
+
# records: [
|
11
|
+
# { name: "Acme Corp", domain: "acme.com" },
|
12
|
+
# { name: "Tech Co", domain: "techco.com" }
|
13
|
+
# ]
|
14
|
+
# )
|
15
|
+
#
|
16
|
+
# @example Bulk update records
|
17
|
+
# client.bulk.update_records(
|
18
|
+
# object: "people",
|
19
|
+
# updates: [
|
20
|
+
# { id: "person_123", data: { title: "CEO" } },
|
21
|
+
# { id: "person_456", data: { title: "CTO" } }
|
22
|
+
# ]
|
23
|
+
# )
|
24
|
+
class Bulk < Base
|
25
|
+
# Maximum number of records per bulk operation
|
26
|
+
MAX_BATCH_SIZE = 100
|
27
|
+
|
28
|
+
# Bulk create multiple records
|
29
|
+
#
|
30
|
+
# @param object [String] The object type (companies, people, etc.)
|
31
|
+
# @param records [Array<Hash>] Array of record data to create
|
32
|
+
# @param options [Hash] Additional options
|
33
|
+
# @option options [Boolean] :partial_success Allow partial success (default: false)
|
34
|
+
# @option options [Boolean] :return_records Return created records (default: true)
|
35
|
+
# @return [Hash] Results including created records and any errors
|
36
|
+
def create_records(object:, records:, options: {})
|
37
|
+
validate_required_string!(object, "Object type")
|
38
|
+
validate_bulk_records!(records, "create")
|
39
|
+
|
40
|
+
batches = records.each_slice(MAX_BATCH_SIZE).to_a
|
41
|
+
results = []
|
42
|
+
|
43
|
+
batches.each_with_index do |batch, index|
|
44
|
+
body = {
|
45
|
+
records: batch.map { |record| { data: record } },
|
46
|
+
partial_success: options.fetch(:partial_success, false),
|
47
|
+
return_records: options.fetch(:return_records, true),
|
48
|
+
}
|
49
|
+
|
50
|
+
result = request(:post, "objects/#{object}/records/bulk", body)
|
51
|
+
results << result.merge("batch" => index + 1)
|
52
|
+
end
|
53
|
+
|
54
|
+
merge_batch_results(results)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Bulk update multiple records
|
58
|
+
#
|
59
|
+
# @param object [String] The object type
|
60
|
+
# @param updates [Array<Hash>] Array of updates with :id and :data keys
|
61
|
+
# @param options [Hash] Additional options
|
62
|
+
# @option options [Boolean] :partial_success Allow partial success (default: false)
|
63
|
+
# @option options [Boolean] :return_records Return updated records (default: true)
|
64
|
+
# @return [Hash] Results including updated records and any errors
|
65
|
+
def update_records(object:, updates:, options: {})
|
66
|
+
validate_required_string!(object, "Object type")
|
67
|
+
validate_bulk_updates!(updates)
|
68
|
+
|
69
|
+
batches = updates.each_slice(MAX_BATCH_SIZE).to_a
|
70
|
+
results = []
|
71
|
+
|
72
|
+
batches.each_with_index do |batch, index|
|
73
|
+
body = {
|
74
|
+
updates: batch,
|
75
|
+
partial_success: options.fetch(:partial_success, false),
|
76
|
+
return_records: options.fetch(:return_records, true),
|
77
|
+
}
|
78
|
+
|
79
|
+
result = request(:patch, "objects/#{object}/records/bulk", body)
|
80
|
+
results << result.merge("batch" => index + 1)
|
81
|
+
end
|
82
|
+
|
83
|
+
merge_batch_results(results)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Bulk delete multiple records
|
87
|
+
#
|
88
|
+
# @param object [String] The object type
|
89
|
+
# @param ids [Array<String>] Array of record IDs to delete
|
90
|
+
# @param options [Hash] Additional options
|
91
|
+
# @option options [Boolean] :partial_success Allow partial success (default: false)
|
92
|
+
# @return [Hash] Results including deletion confirmations and any errors
|
93
|
+
def delete_records(object:, ids:, options: {})
|
94
|
+
validate_required_string!(object, "Object type")
|
95
|
+
validate_bulk_ids!(ids)
|
96
|
+
|
97
|
+
batches = ids.each_slice(MAX_BATCH_SIZE).to_a
|
98
|
+
results = []
|
99
|
+
|
100
|
+
batches.each_with_index do |batch, index|
|
101
|
+
body = {
|
102
|
+
ids: batch,
|
103
|
+
partial_success: options.fetch(:partial_success, false),
|
104
|
+
}
|
105
|
+
|
106
|
+
result = request(:delete, "objects/#{object}/records/bulk", body)
|
107
|
+
results << result.merge("batch" => index + 1)
|
108
|
+
end
|
109
|
+
|
110
|
+
merge_batch_results(results)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Bulk upsert records (create or update based on matching criteria)
|
114
|
+
#
|
115
|
+
# @param object [String] The object type
|
116
|
+
# @param records [Array<Hash>] Records to upsert
|
117
|
+
# @param match_attribute [String] Attribute to match on (e.g., "email", "domain")
|
118
|
+
# @param options [Hash] Additional options
|
119
|
+
# @return [Hash] Results including created/updated records
|
120
|
+
def upsert_records(object:, records:, match_attribute:, options: {})
|
121
|
+
validate_required_string!(object, "Object type")
|
122
|
+
validate_required_string!(match_attribute, "Match attribute")
|
123
|
+
validate_bulk_records!(records, "upsert")
|
124
|
+
|
125
|
+
batches = records.each_slice(MAX_BATCH_SIZE).to_a
|
126
|
+
results = []
|
127
|
+
|
128
|
+
batches.each_with_index do |batch, index|
|
129
|
+
body = {
|
130
|
+
records: batch.map { |record| { data: record } },
|
131
|
+
match_attribute: match_attribute,
|
132
|
+
partial_success: options.fetch(:partial_success, false),
|
133
|
+
return_records: options.fetch(:return_records, true),
|
134
|
+
}
|
135
|
+
|
136
|
+
result = request(:put, "objects/#{object}/records/bulk", body)
|
137
|
+
results << result.merge("batch" => index + 1)
|
138
|
+
end
|
139
|
+
|
140
|
+
merge_batch_results(results)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Bulk add entries to a list
|
144
|
+
#
|
145
|
+
# @param list_id [String] The list ID
|
146
|
+
# @param entries [Array<Hash>] Array of entries to add
|
147
|
+
# @param options [Hash] Additional options
|
148
|
+
# @return [Hash] Results including added entries
|
149
|
+
def add_list_entries(list_id:, entries:, options: {})
|
150
|
+
validate_id!(list_id, "List")
|
151
|
+
validate_bulk_records!(entries, "add to list")
|
152
|
+
|
153
|
+
batches = entries.each_slice(MAX_BATCH_SIZE).to_a
|
154
|
+
results = []
|
155
|
+
|
156
|
+
batches.each_with_index do |batch, index|
|
157
|
+
body = {
|
158
|
+
entries: batch,
|
159
|
+
partial_success: options.fetch(:partial_success, false),
|
160
|
+
}
|
161
|
+
|
162
|
+
result = request(:post, "lists/#{list_id}/entries/bulk", body)
|
163
|
+
results << result.merge("batch" => index + 1)
|
164
|
+
end
|
165
|
+
|
166
|
+
merge_batch_results(results)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Bulk remove entries from a list
|
170
|
+
#
|
171
|
+
# @param list_id [String] The list ID
|
172
|
+
# @param entry_ids [Array<String>] Array of entry IDs to remove
|
173
|
+
# @param options [Hash] Additional options
|
174
|
+
# @return [Hash] Results including removal confirmations
|
175
|
+
def remove_list_entries(list_id:, entry_ids:, options: {})
|
176
|
+
validate_id!(list_id, "List")
|
177
|
+
validate_bulk_ids!(entry_ids)
|
178
|
+
|
179
|
+
batches = entry_ids.each_slice(MAX_BATCH_SIZE).to_a
|
180
|
+
results = []
|
181
|
+
|
182
|
+
batches.each_with_index do |batch, index|
|
183
|
+
body = {
|
184
|
+
entry_ids: batch,
|
185
|
+
partial_success: options.fetch(:partial_success, false),
|
186
|
+
}
|
187
|
+
|
188
|
+
result = request(:delete, "lists/#{list_id}/entries/bulk", body)
|
189
|
+
results << result.merge("batch" => index + 1)
|
190
|
+
end
|
191
|
+
|
192
|
+
merge_batch_results(results)
|
193
|
+
end
|
194
|
+
|
195
|
+
private def validate_bulk_records!(records, operation)
|
196
|
+
raise ArgumentError, "Records array is required for bulk #{operation}" if records.nil?
|
197
|
+
raise ArgumentError, "Records must be an array for bulk #{operation}" unless records.is_a?(Array)
|
198
|
+
raise ArgumentError, "Records array cannot be empty for bulk #{operation}" if records.empty?
|
199
|
+
raise ArgumentError, "Too many records (max #{MAX_BATCH_SIZE * 10})" if records.size > MAX_BATCH_SIZE * 10
|
200
|
+
|
201
|
+
records.each_with_index do |record, index|
|
202
|
+
raise ArgumentError, "Record at index #{index} must be a hash" unless record.is_a?(Hash)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
private def validate_bulk_updates!(updates)
|
207
|
+
validate_array!(updates, "Updates", "bulk update")
|
208
|
+
validate_max_size!(updates, "updates")
|
209
|
+
|
210
|
+
updates.each_with_index do |update, index|
|
211
|
+
validate_update_item!(update, index)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
private def validate_update_item!(update, index)
|
216
|
+
raise ArgumentError, "Update at index #{index} must be a hash" unless update.is_a?(Hash)
|
217
|
+
raise ArgumentError, "Update at index #{index} must have an :id" unless update[:id]
|
218
|
+
raise ArgumentError, "Update at index #{index} must have :data" unless update[:data]
|
219
|
+
end
|
220
|
+
|
221
|
+
private def validate_bulk_ids!(ids)
|
222
|
+
validate_array!(ids, "IDs", "bulk operation")
|
223
|
+
validate_max_size!(ids, "IDs")
|
224
|
+
|
225
|
+
ids.each_with_index do |id, index|
|
226
|
+
validate_id_item!(id, index)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
private def validate_id_item!(id, index)
|
231
|
+
return unless id.nil? || id.to_s.strip.empty?
|
232
|
+
|
233
|
+
raise ArgumentError, "ID at index #{index} cannot be nil or empty"
|
234
|
+
end
|
235
|
+
|
236
|
+
private def validate_array!(array, name, operation)
|
237
|
+
raise ArgumentError, "#{name} array is required for #{operation}" if array.nil?
|
238
|
+
raise ArgumentError, "#{name} must be an array for #{operation}" unless array.is_a?(Array)
|
239
|
+
raise ArgumentError, "#{name} array cannot be empty for #{operation}" if array.empty?
|
240
|
+
end
|
241
|
+
|
242
|
+
private def validate_max_size!(array, name)
|
243
|
+
max = MAX_BATCH_SIZE * 10
|
244
|
+
return unless array.size > max
|
245
|
+
|
246
|
+
raise ArgumentError, "Too many #{name} (max #{max})"
|
247
|
+
end
|
248
|
+
|
249
|
+
private def merge_batch_results(results)
|
250
|
+
merged = initialize_merged_result(results.size)
|
251
|
+
|
252
|
+
results.each do |result|
|
253
|
+
merge_single_result!(merged, result)
|
254
|
+
end
|
255
|
+
|
256
|
+
merged
|
257
|
+
end
|
258
|
+
|
259
|
+
private def initialize_merged_result(batch_count)
|
260
|
+
{
|
261
|
+
"success" => true,
|
262
|
+
"total_batches" => batch_count,
|
263
|
+
"records" => [],
|
264
|
+
"errors" => [],
|
265
|
+
"statistics" => {
|
266
|
+
"created" => 0,
|
267
|
+
"updated" => 0,
|
268
|
+
"deleted" => 0,
|
269
|
+
"failed" => 0,
|
270
|
+
},
|
271
|
+
}
|
272
|
+
end
|
273
|
+
|
274
|
+
private def merge_single_result!(merged, result)
|
275
|
+
merged["records"].concat(result["records"] || [])
|
276
|
+
merged["errors"].concat(result["errors"] || [])
|
277
|
+
merge_statistics!(merged["statistics"], result["statistics"])
|
278
|
+
merged["success"] &&= result["success"] != false
|
279
|
+
end
|
280
|
+
|
281
|
+
private def merge_statistics!(target, source)
|
282
|
+
return unless source
|
283
|
+
|
284
|
+
%w[created updated deleted failed].each do |key|
|
285
|
+
target[key] += source[key] || 0
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Attio
|
4
|
+
module Resources
|
5
|
+
# API resource for managing comments in Attio
|
6
|
+
#
|
7
|
+
# Comments can be added to records and threads for collaboration.
|
8
|
+
#
|
9
|
+
# @example Creating a comment on a record
|
10
|
+
# client.comments.create(
|
11
|
+
# parent_object: "people",
|
12
|
+
# parent_record_id: "person_123",
|
13
|
+
# content: "Just had a great call with this lead!"
|
14
|
+
# )
|
15
|
+
#
|
16
|
+
# @example Creating a comment in a thread
|
17
|
+
# client.comments.create(
|
18
|
+
# thread_id: "thread_456",
|
19
|
+
# content: "Following up on our discussion..."
|
20
|
+
# )
|
21
|
+
class Comments < Base
|
22
|
+
# List comments for a parent record or thread
|
23
|
+
#
|
24
|
+
# @param params [Hash] Query parameters
|
25
|
+
# @option params [String] :parent_object Parent object type
|
26
|
+
# @option params [String] :parent_record_id Parent record ID
|
27
|
+
# @option params [String] :thread_id Thread ID
|
28
|
+
# @option params [Integer] :limit Number of comments to return
|
29
|
+
# @option params [String] :cursor Pagination cursor
|
30
|
+
#
|
31
|
+
# @return [Hash] API response containing comments
|
32
|
+
def list(**params)
|
33
|
+
validate_list_params!(params)
|
34
|
+
request(:get, "comments", params)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get a specific comment by ID
|
38
|
+
#
|
39
|
+
# @param id [String] The comment ID
|
40
|
+
#
|
41
|
+
# @return [Hash] The comment data
|
42
|
+
# @raise [ArgumentError] if id is nil or empty
|
43
|
+
def get(id:)
|
44
|
+
validate_id!(id, "Comment")
|
45
|
+
request(:get, "comments/#{id}")
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create a new comment
|
49
|
+
#
|
50
|
+
# @param content [String] The comment content (supports markdown)
|
51
|
+
# @param parent_object [String] Parent object type (required if no thread_id)
|
52
|
+
# @param parent_record_id [String] Parent record ID (required if no thread_id)
|
53
|
+
# @param thread_id [String] Thread ID (required if no parent_object/parent_record_id)
|
54
|
+
# @param data [Hash] Additional comment data
|
55
|
+
#
|
56
|
+
# @return [Hash] The created comment
|
57
|
+
# @raise [ArgumentError] if required parameters are missing
|
58
|
+
def create(content:, parent_object: nil, parent_record_id: nil, thread_id: nil, **data)
|
59
|
+
validate_required_string!(content, "Comment content")
|
60
|
+
validate_create_params!(parent_object, parent_record_id, thread_id)
|
61
|
+
|
62
|
+
params = data.merge(content: content)
|
63
|
+
|
64
|
+
if thread_id
|
65
|
+
params[:thread_id] = thread_id
|
66
|
+
else
|
67
|
+
params[:parent_object] = parent_object
|
68
|
+
params[:parent_record_id] = parent_record_id
|
69
|
+
end
|
70
|
+
|
71
|
+
request(:post, "comments", params)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Update an existing comment
|
75
|
+
#
|
76
|
+
# @param id [String] The comment ID
|
77
|
+
# @param content [String] The new content
|
78
|
+
#
|
79
|
+
# @return [Hash] The updated comment
|
80
|
+
# @raise [ArgumentError] if id or content is invalid
|
81
|
+
def update(id:, content:)
|
82
|
+
validate_id!(id, "Comment")
|
83
|
+
validate_required_string!(content, "Comment content")
|
84
|
+
request(:patch, "comments/#{id}", { content: content })
|
85
|
+
end
|
86
|
+
|
87
|
+
# Delete a comment
|
88
|
+
#
|
89
|
+
# @param id [String] The comment ID to delete
|
90
|
+
#
|
91
|
+
# @return [Hash] Deletion confirmation
|
92
|
+
# @raise [ArgumentError] if id is nil or empty
|
93
|
+
def delete(id:)
|
94
|
+
validate_id!(id, "Comment")
|
95
|
+
request(:delete, "comments/#{id}")
|
96
|
+
end
|
97
|
+
|
98
|
+
# React to a comment with an emoji
|
99
|
+
#
|
100
|
+
# @param id [String] The comment ID
|
101
|
+
# @param emoji [String] The emoji reaction (e.g., "👍", "❤️")
|
102
|
+
#
|
103
|
+
# @return [Hash] The updated comment with reaction
|
104
|
+
# @raise [ArgumentError] if id or emoji is invalid
|
105
|
+
def react(id:, emoji:)
|
106
|
+
validate_id!(id, "Comment")
|
107
|
+
validate_required_string!(emoji, "Emoji")
|
108
|
+
request(:post, "comments/#{id}/reactions", { emoji: emoji })
|
109
|
+
end
|
110
|
+
|
111
|
+
# Remove a reaction from a comment
|
112
|
+
#
|
113
|
+
# @param id [String] The comment ID
|
114
|
+
# @param emoji [String] The emoji reaction to remove
|
115
|
+
#
|
116
|
+
# @return [Hash] The updated comment
|
117
|
+
# @raise [ArgumentError] if id or emoji is invalid
|
118
|
+
def unreact(id:, emoji:)
|
119
|
+
validate_id!(id, "Comment")
|
120
|
+
validate_required_string!(emoji, "Emoji")
|
121
|
+
request(:delete, "comments/#{id}/reactions/#{CGI.escape(emoji)}")
|
122
|
+
end
|
123
|
+
|
124
|
+
private def validate_list_params!(params)
|
125
|
+
has_parent = params[:parent_object] && params[:parent_record_id]
|
126
|
+
has_thread = params[:thread_id]
|
127
|
+
|
128
|
+
return if has_parent || has_thread
|
129
|
+
|
130
|
+
raise ArgumentError, "Must provide either parent_object/parent_record_id or thread_id"
|
131
|
+
end
|
132
|
+
|
133
|
+
private def validate_create_params!(parent_object, parent_record_id, thread_id)
|
134
|
+
has_parent = parent_object && parent_record_id
|
135
|
+
has_thread = thread_id
|
136
|
+
|
137
|
+
unless has_parent || has_thread
|
138
|
+
raise ArgumentError, "Must provide either parent_object/parent_record_id or thread_id"
|
139
|
+
end
|
140
|
+
|
141
|
+
return unless has_parent && has_thread
|
142
|
+
|
143
|
+
raise ArgumentError, "Cannot provide both parent and thread parameters"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Attio
|
4
|
+
module Resources
|
5
|
+
# Deals resource for managing sales opportunities
|
6
|
+
#
|
7
|
+
# @example List all deals
|
8
|
+
# client.deals.list
|
9
|
+
#
|
10
|
+
# @example Create a new deal
|
11
|
+
# client.deals.create(
|
12
|
+
# data: {
|
13
|
+
# name: "Enterprise Contract",
|
14
|
+
# value: 50000,
|
15
|
+
# stage_id: "stage_123",
|
16
|
+
# company_id: "company_456"
|
17
|
+
# }
|
18
|
+
# )
|
19
|
+
#
|
20
|
+
# @example Update deal stage
|
21
|
+
# client.deals.update_stage(id: "deal_123", stage_id: "stage_won")
|
22
|
+
class Deals < Base
|
23
|
+
# List all deals
|
24
|
+
#
|
25
|
+
# @param params [Hash] Optional query parameters
|
26
|
+
# @option params [Hash] :filter Filter conditions
|
27
|
+
# @option params [Array<Hash>] :sorts Sort criteria
|
28
|
+
# @option params [Integer] :limit Maximum number of results
|
29
|
+
# @option params [String] :offset Pagination offset
|
30
|
+
# @return [Hash] The API response
|
31
|
+
def list(params = {})
|
32
|
+
request(:get, "objects/deals/records", params)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get a specific deal
|
36
|
+
#
|
37
|
+
# @param id [String] The deal ID
|
38
|
+
# @return [Hash] The deal data
|
39
|
+
def get(id:)
|
40
|
+
validate_id!(id, "Deal")
|
41
|
+
request(:get, "objects/deals/records/#{id}")
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create a new deal
|
45
|
+
#
|
46
|
+
# @param data [Hash] The deal data
|
47
|
+
# @option data [String] :name The deal name (required)
|
48
|
+
# @option data [Float] :value The deal value
|
49
|
+
# @option data [String] :stage_id The stage ID
|
50
|
+
# @option data [String] :company_id Associated company ID
|
51
|
+
# @option data [String] :owner_id The owner user ID
|
52
|
+
# @option data [Date] :close_date Expected close date
|
53
|
+
# @option data [String] :currency Currency code (USD, EUR, etc.)
|
54
|
+
# @option data [Float] :probability Win probability (0-100)
|
55
|
+
# @return [Hash] The created deal
|
56
|
+
def create(data:)
|
57
|
+
validate_required_hash!(data, "Data")
|
58
|
+
validate_required_string!(data[:name], "Deal name")
|
59
|
+
|
60
|
+
request(:post, "objects/deals/records", { data: data })
|
61
|
+
end
|
62
|
+
|
63
|
+
# Update a deal
|
64
|
+
#
|
65
|
+
# @param id [String] The deal ID to update
|
66
|
+
# @param data [Hash] The data to update
|
67
|
+
# @return [Hash] The updated deal
|
68
|
+
def update(id:, data:)
|
69
|
+
validate_id!(id, "Deal")
|
70
|
+
validate_required_hash!(data, "Data")
|
71
|
+
|
72
|
+
request(:patch, "objects/deals/records/#{id}", { data: data })
|
73
|
+
end
|
74
|
+
|
75
|
+
# Delete a deal
|
76
|
+
#
|
77
|
+
# @param id [String] The deal ID to delete
|
78
|
+
# @return [Hash] Confirmation of deletion
|
79
|
+
def delete(id:)
|
80
|
+
validate_id!(id, "Deal")
|
81
|
+
request(:delete, "objects/deals/records/#{id}")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Update a deal's stage
|
85
|
+
#
|
86
|
+
# @param id [String] The deal ID
|
87
|
+
# @param stage_id [String] The new stage ID
|
88
|
+
# @return [Hash] The updated deal
|
89
|
+
def update_stage(id:, stage_id:)
|
90
|
+
validate_id!(id, "Deal")
|
91
|
+
validate_required_string!(stage_id, "Stage")
|
92
|
+
|
93
|
+
update(id: id, data: { stage_id: stage_id })
|
94
|
+
end
|
95
|
+
|
96
|
+
# Mark a deal as won
|
97
|
+
#
|
98
|
+
# @param id [String] The deal ID
|
99
|
+
# @param won_date [Date, String] The date the deal was won (defaults to today)
|
100
|
+
# @param actual_value [Float] The actual value (optional, defaults to deal value)
|
101
|
+
# @return [Hash] The updated deal
|
102
|
+
def mark_won(id:, won_date: nil, actual_value: nil)
|
103
|
+
validate_id!(id, "Deal")
|
104
|
+
|
105
|
+
data = { status: "won" }
|
106
|
+
data[:won_date] = won_date if won_date
|
107
|
+
data[:actual_value] = actual_value if actual_value
|
108
|
+
|
109
|
+
update(id: id, data: data)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Mark a deal as lost
|
113
|
+
#
|
114
|
+
# @param id [String] The deal ID
|
115
|
+
# @param lost_reason [String] The reason for losing the deal
|
116
|
+
# @param lost_date [Date, String] The date the deal was lost (defaults to today)
|
117
|
+
# @return [Hash] The updated deal
|
118
|
+
def mark_lost(id:, lost_reason: nil, lost_date: nil)
|
119
|
+
validate_id!(id, "Deal")
|
120
|
+
|
121
|
+
data = { status: "lost" }
|
122
|
+
data[:lost_reason] = lost_reason if lost_reason
|
123
|
+
data[:lost_date] = lost_date if lost_date
|
124
|
+
|
125
|
+
update(id: id, data: data)
|
126
|
+
end
|
127
|
+
|
128
|
+
# List deals by stage
|
129
|
+
#
|
130
|
+
# @param stage_id [String] The stage ID to filter by
|
131
|
+
# @param params [Hash] Additional query parameters
|
132
|
+
# @return [Hash] Deals in the specified stage
|
133
|
+
def list_by_stage(stage_id:, params: {})
|
134
|
+
validate_required_string!(stage_id, "Stage")
|
135
|
+
|
136
|
+
filter = { stage_id: { "$eq" => stage_id } }
|
137
|
+
merged_params = params.merge(filter: filter)
|
138
|
+
list(merged_params)
|
139
|
+
end
|
140
|
+
|
141
|
+
# List deals by company
|
142
|
+
#
|
143
|
+
# @param company_id [String] The company ID to filter by
|
144
|
+
# @param params [Hash] Additional query parameters
|
145
|
+
# @return [Hash] Deals for the specified company
|
146
|
+
def list_by_company(company_id:, params: {})
|
147
|
+
validate_required_string!(company_id, "Company")
|
148
|
+
|
149
|
+
filter = { company_id: { "$eq" => company_id } }
|
150
|
+
merged_params = params.merge(filter: filter)
|
151
|
+
list(merged_params)
|
152
|
+
end
|
153
|
+
|
154
|
+
# List deals by owner
|
155
|
+
#
|
156
|
+
# @param owner_id [String] The owner user ID to filter by
|
157
|
+
# @param params [Hash] Additional query parameters
|
158
|
+
# @return [Hash] Deals owned by the specified user
|
159
|
+
def list_by_owner(owner_id:, params: {})
|
160
|
+
validate_required_string!(owner_id, "Owner")
|
161
|
+
|
162
|
+
filter = { owner_id: { "$eq" => owner_id } }
|
163
|
+
merged_params = params.merge(filter: filter)
|
164
|
+
list(merged_params)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Calculate pipeline value
|
168
|
+
#
|
169
|
+
# @param stage_id [String] Optional stage ID to filter by
|
170
|
+
# @param owner_id [String] Optional owner ID to filter by
|
171
|
+
# @return [Hash] Pipeline statistics including total value, count, and average
|
172
|
+
def pipeline_value(stage_id: nil, owner_id: nil)
|
173
|
+
params = { filter: {} }
|
174
|
+
params[:filter][:stage_id] = { "$eq" => stage_id } if stage_id
|
175
|
+
params[:filter][:owner_id] = { "$eq" => owner_id } if owner_id
|
176
|
+
|
177
|
+
# This would typically be a specialized endpoint, but we'll use list
|
178
|
+
# and let the client calculate the statistics
|
179
|
+
list(params)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -17,46 +17,34 @@ module Attio
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def get(id:)
|
20
|
-
validate_id!(id)
|
20
|
+
validate_id!(id, "List")
|
21
21
|
request(:get, "lists/#{id}")
|
22
22
|
end
|
23
23
|
|
24
24
|
def entries(id:, **params)
|
25
|
-
validate_id!(id)
|
25
|
+
validate_id!(id, "List")
|
26
26
|
request(:get, "lists/#{id}/entries", params)
|
27
27
|
end
|
28
28
|
|
29
29
|
def create_entry(id:, data:)
|
30
|
-
validate_id!(id)
|
31
|
-
|
30
|
+
validate_id!(id, "List")
|
31
|
+
validate_list_entry_data!(data)
|
32
32
|
request(:post, "lists/#{id}/entries", data)
|
33
33
|
end
|
34
34
|
|
35
35
|
def get_entry(list_id:, entry_id:)
|
36
|
-
|
37
|
-
|
36
|
+
validate_id!(list_id, "List")
|
37
|
+
validate_id!(entry_id, "Entry")
|
38
38
|
request(:get, "lists/#{list_id}/entries/#{entry_id}")
|
39
39
|
end
|
40
40
|
|
41
41
|
def delete_entry(list_id:, entry_id:)
|
42
|
-
|
43
|
-
|
42
|
+
validate_id!(list_id, "List")
|
43
|
+
validate_id!(entry_id, "Entry")
|
44
44
|
request(:delete, "lists/#{list_id}/entries/#{entry_id}")
|
45
45
|
end
|
46
46
|
|
47
|
-
private def
|
48
|
-
raise ArgumentError, "List ID is required" if id.nil? || id.to_s.strip.empty?
|
49
|
-
end
|
50
|
-
|
51
|
-
private def validate_list_id!(list_id)
|
52
|
-
raise ArgumentError, "List ID is required" if list_id.nil? || list_id.to_s.strip.empty?
|
53
|
-
end
|
54
|
-
|
55
|
-
private def validate_entry_id!(entry_id)
|
56
|
-
raise ArgumentError, "Entry ID is required" if entry_id.nil? || entry_id.to_s.strip.empty?
|
57
|
-
end
|
58
|
-
|
59
|
-
private def validate_data!(data)
|
47
|
+
private def validate_list_entry_data!(data)
|
60
48
|
raise ArgumentError, "Data is required" if data.nil?
|
61
49
|
raise ArgumentError, "Data must be a hash" unless data.is_a?(Hash)
|
62
50
|
end
|