attio-ruby 0.1.3 → 0.1.5
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/.rubocop.yml +1 -1
- data/CHANGELOG.md +56 -0
- data/examples/deals.rb +2 -2
- data/lib/attio/api_resource.rb +5 -4
- data/lib/attio/concerns/time_filterable.rb +153 -0
- data/lib/attio/internal/record.rb +1 -1
- data/lib/attio/resources/company.rb +3 -4
- data/lib/attio/resources/deal.rb +288 -57
- data/lib/attio/resources/meta.rb +5 -7
- data/lib/attio/resources/object.rb +2 -2
- data/lib/attio/resources/person.rb +2 -3
- data/lib/attio/resources/typed_record.rb +5 -5
- data/lib/attio/resources/workspace_member.rb +1 -1
- data/lib/attio/util/currency_formatter.rb +143 -0
- data/lib/attio/util/time_period.rb +219 -0
- data/lib/attio/version.rb +1 -1
- data/lib/attio-ruby.rb +1 -1
- metadata +4 -2
- data/attio-ruby.gemspec +0 -61
data/lib/attio/resources/deal.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "typed_record"
|
4
|
+
require_relative "../util/time_period"
|
5
|
+
require_relative "../util/currency_formatter"
|
4
6
|
|
5
7
|
module Attio
|
6
8
|
# Represents a Deal record in Attio
|
@@ -30,53 +32,52 @@ module Attio
|
|
30
32
|
# @param attributes [Hash] Deal attributes
|
31
33
|
# @option attributes [String] :name Deal name (recommended)
|
32
34
|
# @option attributes [Numeric] :value Deal value (recommended)
|
33
|
-
# @option attributes [String] :stage Deal stage (recommended) -
|
35
|
+
# @option attributes [String] :stage Deal stage (recommended) - configurable via Attio.configuration
|
34
36
|
# @option attributes [String] :status Deal status (alias for stage)
|
35
37
|
# @option attributes [String] :owner Owner email or workspace member (recommended)
|
36
38
|
# @option attributes [Array<String>] :associated_people Email addresses of associated people
|
37
39
|
# @option attributes [Array<String>] :associated_company Domains of associated companies
|
38
40
|
# @option attributes [Hash] :values Raw values hash (for advanced use)
|
39
41
|
def create(name:, value: nil, stage: nil, status: nil, owner: nil,
|
40
|
-
|
42
|
+
associated_people: nil, associated_company: nil, values: {}, **opts)
|
41
43
|
# Name is required and simple
|
42
44
|
values[:name] = name if name && !values[:name]
|
43
|
-
|
45
|
+
|
44
46
|
# Add optional fields
|
45
47
|
values[:value] = value if value && !values[:value]
|
46
|
-
|
48
|
+
|
47
49
|
# Handle stage vs status - API uses "stage" but we support both
|
48
50
|
if (stage || status) && !values[:stage]
|
49
51
|
values[:stage] = stage || status
|
50
52
|
end
|
51
|
-
|
52
|
-
|
53
|
+
|
53
54
|
# Handle owner - can be email address or workspace member reference
|
54
55
|
if owner && !values[:owner]
|
55
56
|
values[:owner] = owner
|
56
57
|
end
|
57
|
-
|
58
|
+
|
58
59
|
# Handle associated people - convert email array to proper format
|
59
60
|
if associated_people && !values[:associated_people]
|
60
61
|
values[:associated_people] = associated_people.map do |email|
|
61
62
|
{
|
62
63
|
target_object: "people",
|
63
64
|
email_addresses: [
|
64
|
-
{
|
65
|
+
{email_address: email}
|
65
66
|
]
|
66
67
|
}
|
67
68
|
end
|
68
69
|
end
|
69
|
-
|
70
|
+
|
70
71
|
# Handle associated company - convert domain array to proper format
|
71
72
|
if associated_company && !values[:associated_company]
|
72
73
|
# associated_company can be array of domains or single domain
|
73
74
|
domains = associated_company.is_a?(Array) ? associated_company : [associated_company]
|
74
75
|
values[:associated_company] = {
|
75
76
|
target_object: "companies",
|
76
|
-
domains: domains.map { |domain| {
|
77
|
+
domains: domains.map { |domain| {domain: domain} }
|
77
78
|
}
|
78
79
|
end
|
79
|
-
|
80
|
+
|
80
81
|
super(values: values, **opts)
|
81
82
|
end
|
82
83
|
|
@@ -85,15 +86,15 @@ module Attio
|
|
85
86
|
# @return [Attio::ListObject] List of matching deals
|
86
87
|
def in_stage(stage_names:, **opts)
|
87
88
|
# If only one stage, use simple equality
|
88
|
-
if stage_names.length == 1
|
89
|
-
|
89
|
+
filter = if stage_names.length == 1
|
90
|
+
{stage: stage_names.first}
|
90
91
|
else
|
91
92
|
# Multiple stages need $or operator
|
92
|
-
|
93
|
-
"$or": stage_names.map { |stage| {
|
93
|
+
{
|
94
|
+
"$or": stage_names.map { |stage| {stage: stage} }
|
94
95
|
}
|
95
96
|
end
|
96
|
-
|
97
|
+
|
97
98
|
list(**opts.merge(params: {filter: filter}))
|
98
99
|
end
|
99
100
|
|
@@ -124,7 +125,7 @@ module Attio
|
|
124
125
|
filters = []
|
125
126
|
filters << {value: {"$gte": min}} if min
|
126
127
|
filters << {value: {"$lte": max}} if max
|
127
|
-
|
128
|
+
|
128
129
|
filter = if filters.length == 1
|
129
130
|
filters.first
|
130
131
|
elsif filters.length > 1
|
@@ -132,7 +133,7 @@ module Attio
|
|
132
133
|
else
|
133
134
|
{}
|
134
135
|
end
|
135
|
-
|
136
|
+
|
136
137
|
list(**opts.merge(params: {filter: filter}))
|
137
138
|
end
|
138
139
|
|
@@ -142,14 +143,15 @@ module Attio
|
|
142
143
|
# def closing_soon(days: 30, **opts)
|
143
144
|
# today = Date.today
|
144
145
|
# end_date = today + days
|
145
|
-
#
|
146
|
+
#
|
146
147
|
# list(**opts.merge(params: {
|
147
148
|
# filter: {
|
148
149
|
# "$and": [
|
149
150
|
# {close_date: {"$gte": today.iso8601}},
|
150
151
|
# {close_date: {"$lte": end_date.iso8601}},
|
151
|
-
#
|
152
|
-
# {stage: {"$
|
152
|
+
# # Exclude won and lost statuses
|
153
|
+
# {"$not": {stage: {"$in": Attio.configuration.won_statuses}}},
|
154
|
+
# {"$not": {stage: {"$in": Attio.configuration.lost_statuses}}}
|
153
155
|
# ]
|
154
156
|
# }
|
155
157
|
# }))
|
@@ -168,12 +170,138 @@ module Attio
|
|
168
170
|
}
|
169
171
|
}))
|
170
172
|
end
|
171
|
-
|
173
|
+
|
174
|
+
# Get deals that closed in a specific time period
|
175
|
+
# @param period [Util::TimePeriod] The time period
|
176
|
+
# @return [Array<Attio::Deal>] List of deals closed in the period
|
177
|
+
def closed_in_period(period, **opts)
|
178
|
+
all(**opts).select do |deal|
|
179
|
+
closed_date = deal.closed_at
|
180
|
+
closed_date && period.includes?(closed_date)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Get deals that closed in a specific quarter
|
185
|
+
# @param year [Integer] The year
|
186
|
+
# @param quarter [Integer] The quarter (1-4)
|
187
|
+
# @return [Array<Attio::Deal>] List of deals closed in the quarter
|
188
|
+
def closed_in_quarter(year, quarter, **opts)
|
189
|
+
period = Util::TimePeriod.quarter(year, quarter)
|
190
|
+
closed_in_period(period, **opts)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Get metrics for any time period
|
194
|
+
# @param period [Util::TimePeriod] The time period
|
195
|
+
# @return [Hash] Metrics for the period
|
196
|
+
def metrics_for_period(period, **opts)
|
197
|
+
# Build date filter for stage.active_from
|
198
|
+
# Note: We need to add a day to end_date to include all of that day
|
199
|
+
# since stage.active_from includes time
|
200
|
+
date_filter = {
|
201
|
+
"stage" => {
|
202
|
+
"active_from" => {
|
203
|
+
"$gte" => period.start_date.strftime("%Y-%m-%d"),
|
204
|
+
"$lte" => (period.end_date + 1).strftime("%Y-%m-%d")
|
205
|
+
}
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
# Fetch won deals closed in the period
|
210
|
+
won_statuses = ::Attio.configuration.won_statuses
|
211
|
+
won_conditions = won_statuses.map { |status| {"stage" => status} }
|
212
|
+
won_filter = {
|
213
|
+
"$and" => [
|
214
|
+
((won_conditions.size > 1) ? {"$or" => won_conditions} : won_conditions.first),
|
215
|
+
date_filter
|
216
|
+
].compact
|
217
|
+
}
|
218
|
+
won_response = list(**opts.merge(params: {filter: won_filter}))
|
219
|
+
|
220
|
+
# Fetch lost deals closed in the period
|
221
|
+
lost_statuses = ::Attio.configuration.lost_statuses
|
222
|
+
lost_conditions = lost_statuses.map { |status| {"stage" => status} }
|
223
|
+
lost_filter = {
|
224
|
+
"$and" => [
|
225
|
+
((lost_conditions.size > 1) ? {"$or" => lost_conditions} : lost_conditions.first),
|
226
|
+
date_filter
|
227
|
+
].compact
|
228
|
+
}
|
229
|
+
lost_response = list(**opts.merge(params: {filter: lost_filter}))
|
230
|
+
|
231
|
+
won_deals = won_response.data
|
232
|
+
lost_deals = lost_response.data
|
233
|
+
total_closed = won_deals.size + lost_deals.size
|
234
|
+
|
235
|
+
{
|
236
|
+
period: period.label,
|
237
|
+
won_count: won_deals.size,
|
238
|
+
won_amount: won_deals.sum(&:amount),
|
239
|
+
lost_count: lost_deals.size,
|
240
|
+
lost_amount: lost_deals.sum(&:amount),
|
241
|
+
total_closed: total_closed,
|
242
|
+
win_rate: (total_closed > 0) ? (won_deals.size.to_f / total_closed * 100).round(2) : 0.0
|
243
|
+
}
|
244
|
+
end
|
245
|
+
|
246
|
+
# Get current quarter metrics
|
247
|
+
# @return [Hash] Metrics for the current quarter
|
248
|
+
def current_quarter_metrics(**opts)
|
249
|
+
metrics_for_period(Util::TimePeriod.current_quarter, **opts)
|
250
|
+
end
|
251
|
+
|
252
|
+
# Get year-to-date metrics
|
253
|
+
# @return [Hash] Metrics for year to date
|
254
|
+
def year_to_date_metrics(**opts)
|
255
|
+
metrics_for_period(Util::TimePeriod.year_to_date, **opts)
|
256
|
+
end
|
257
|
+
|
258
|
+
# Get month-to-date metrics
|
259
|
+
# @return [Hash] Metrics for month to date
|
260
|
+
def month_to_date_metrics(**opts)
|
261
|
+
metrics_for_period(Util::TimePeriod.month_to_date, **opts)
|
262
|
+
end
|
263
|
+
|
264
|
+
# Get last 30 days metrics
|
265
|
+
# @return [Hash] Metrics for last 30 days
|
266
|
+
def last_30_days_metrics(**opts)
|
267
|
+
metrics_for_period(Util::TimePeriod.last_30_days, **opts)
|
268
|
+
end
|
269
|
+
|
270
|
+
# Get high-value deals above a threshold
|
271
|
+
# @param threshold [Numeric] The minimum value threshold (defaults to 50,000)
|
272
|
+
# @return [Array<Attio::Deal>] List of high-value deals
|
273
|
+
def high_value(threshold = 50_000, **opts)
|
274
|
+
all(**opts).select { |deal| deal.amount > threshold }
|
275
|
+
end
|
276
|
+
|
277
|
+
# Get deals without owners
|
278
|
+
# @return [Array<Attio::Deal>] List of unassigned deals
|
279
|
+
def unassigned(**opts)
|
280
|
+
all(**opts).select { |deal| deal.owner.nil? }
|
281
|
+
end
|
282
|
+
|
283
|
+
# Get recently created deals
|
284
|
+
# @param days [Integer] Number of days to look back (defaults to 7)
|
285
|
+
# @return [Array<Attio::Deal>] List of recently created deals
|
286
|
+
def recently_created(days = 7, **opts)
|
287
|
+
created_in_period(Util::TimePeriod.last_days(days), **opts)
|
288
|
+
end
|
289
|
+
|
290
|
+
# Get deals created in a specific period
|
291
|
+
# @param period [Util::TimePeriod] The time period
|
292
|
+
# @return [Array<Attio::Deal>] List of deals created in the period
|
293
|
+
def created_in_period(period, **opts)
|
294
|
+
all(**opts).select do |deal|
|
295
|
+
created_at = deal.created_at
|
296
|
+
created_at && period.includes?(created_at)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
172
300
|
private
|
173
|
-
|
301
|
+
|
174
302
|
# Build filter for status field (maps to stage)
|
175
303
|
def filter_by_status(value)
|
176
|
-
{
|
304
|
+
{stage: value}
|
177
305
|
end
|
178
306
|
end
|
179
307
|
|
@@ -183,23 +311,53 @@ module Attio
|
|
183
311
|
self[:name]
|
184
312
|
end
|
185
313
|
|
186
|
-
# Get the deal value
|
187
|
-
# @return [
|
314
|
+
# Get the monetary amount from the deal value
|
315
|
+
# @return [Float] The deal amount (0.0 if not set)
|
316
|
+
def amount
|
317
|
+
return 0.0 unless self[:value].is_a?(Hash)
|
318
|
+
(self[:value]["currency_value"] || 0).to_f
|
319
|
+
end
|
320
|
+
|
321
|
+
# Get the currency code
|
322
|
+
# @return [String] The currency code (defaults to "USD")
|
323
|
+
def currency
|
324
|
+
return "USD" unless self[:value].is_a?(Hash)
|
325
|
+
self[:value]["currency_code"] || "USD"
|
326
|
+
end
|
327
|
+
|
328
|
+
# Get formatted amount for display
|
329
|
+
# @return [String] The formatted currency amount
|
330
|
+
def formatted_amount
|
331
|
+
Util::CurrencyFormatter.format(amount, currency)
|
332
|
+
end
|
333
|
+
|
334
|
+
# Get the raw deal value (for backward compatibility)
|
335
|
+
# @deprecated Use {#amount} for monetary values or {#raw_value} for raw API response
|
336
|
+
# @return [Object] The raw value from the API
|
188
337
|
def value
|
338
|
+
warn "[DEPRECATION] `value` is deprecated. Use `amount` for monetary values or `raw_value` for the raw API response." unless ENV["ATTIO_SUPPRESS_DEPRECATION"]
|
339
|
+
amount
|
340
|
+
end
|
341
|
+
|
342
|
+
# Get the raw value data from the API
|
343
|
+
# @return [Object] The raw value data
|
344
|
+
def raw_value
|
189
345
|
self[:value]
|
190
346
|
end
|
191
347
|
|
192
|
-
# Get the deal stage
|
193
|
-
# @return [String, nil] The deal stage
|
348
|
+
# Get the normalized deal stage/status
|
349
|
+
# @return [String, nil] The deal stage title
|
194
350
|
def stage
|
195
|
-
self[:stage]
|
351
|
+
stage_data = self[:stage]
|
352
|
+
return nil unless stage_data.is_a?(Hash)
|
353
|
+
|
354
|
+
# Attio always returns stage as a hash with nested status.title
|
355
|
+
stage_data.dig("status", "title")
|
196
356
|
end
|
197
|
-
|
357
|
+
|
198
358
|
# Alias for stage (for compatibility)
|
199
359
|
# @return [String, nil] The deal stage
|
200
|
-
|
201
|
-
self[:stage]
|
202
|
-
end
|
360
|
+
alias_method :status, :stage
|
203
361
|
|
204
362
|
# # Get the close date (if attribute exists)
|
205
363
|
# # @return [String, nil] The close date
|
@@ -231,7 +389,7 @@ module Attio
|
|
231
389
|
def update_stage(new_stage, **opts)
|
232
390
|
self.class.update(id, values: {stage: new_stage}, **opts)
|
233
391
|
end
|
234
|
-
|
392
|
+
|
235
393
|
# Alias for update_stage (for compatibility)
|
236
394
|
# @param new_status [String] The new status/stage
|
237
395
|
# @return [Attio::Deal] The updated deal
|
@@ -257,7 +415,7 @@ module Attio
|
|
257
415
|
# @return [Attio::Company, nil] The company record if associated
|
258
416
|
def company_record(**opts)
|
259
417
|
return nil unless company
|
260
|
-
|
418
|
+
|
261
419
|
company_id = company.is_a?(Hash) ? company["target_record_id"] : company
|
262
420
|
Company.retrieve(company_id, **opts) if company_id
|
263
421
|
end
|
@@ -266,7 +424,7 @@ module Attio
|
|
266
424
|
# @return [Attio::WorkspaceMember, nil] The owner record if assigned
|
267
425
|
def owner_record(**opts)
|
268
426
|
return nil unless owner
|
269
|
-
|
427
|
+
|
270
428
|
owner_id = if owner.is_a?(Hash)
|
271
429
|
owner["referenced_actor_id"] || owner["target_record_id"]
|
272
430
|
else
|
@@ -282,35 +440,27 @@ module Attio
|
|
282
440
|
# (value * probability / 100.0).round(2)
|
283
441
|
# end
|
284
442
|
|
285
|
-
# Get the current status title
|
443
|
+
# Get the current status title (delegates to stage for simplicity)
|
286
444
|
# @return [String, nil] The current status title
|
287
445
|
def current_status
|
288
|
-
|
289
|
-
|
290
|
-
if stage.is_a?(Hash)
|
291
|
-
stage.dig("status", "title")
|
292
|
-
else
|
293
|
-
stage
|
294
|
-
end
|
446
|
+
stage
|
295
447
|
end
|
296
448
|
|
297
449
|
# Get the timestamp when the status changed
|
298
450
|
# @return [Time, nil] The timestamp when status changed
|
299
451
|
def status_changed_at
|
300
|
-
return nil unless stage
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
nil
|
306
|
-
end
|
452
|
+
return nil unless self[:stage].is_a?(Hash)
|
453
|
+
|
454
|
+
# Attio returns active_from at the top level of the stage hash
|
455
|
+
timestamp = self[:stage]["active_from"]
|
456
|
+
timestamp ? Time.parse(timestamp) : nil
|
307
457
|
end
|
308
458
|
|
309
459
|
# Check if the deal is open
|
310
460
|
# @return [Boolean] True if the deal is open
|
311
461
|
def open?
|
312
462
|
return false unless current_status
|
313
|
-
|
463
|
+
|
314
464
|
all_open_statuses = Attio.configuration.open_statuses + Attio.configuration.in_progress_statuses
|
315
465
|
all_open_statuses.include?(current_status)
|
316
466
|
end
|
@@ -319,7 +469,7 @@ module Attio
|
|
319
469
|
# @return [Boolean] True if the deal is won
|
320
470
|
def won?
|
321
471
|
return false unless current_status
|
322
|
-
|
472
|
+
|
323
473
|
Attio.configuration.won_statuses.include?(current_status)
|
324
474
|
end
|
325
475
|
|
@@ -327,20 +477,22 @@ module Attio
|
|
327
477
|
# @return [Boolean] True if the deal is lost
|
328
478
|
def lost?
|
329
479
|
return false unless current_status
|
330
|
-
|
480
|
+
|
331
481
|
Attio.configuration.lost_statuses.include?(current_status)
|
332
482
|
end
|
333
483
|
|
334
484
|
# Get the timestamp when the deal was won
|
335
485
|
# @return [Time, nil] The timestamp when deal was won, or nil if not won
|
336
486
|
def won_at
|
337
|
-
|
487
|
+
return nil unless won?
|
488
|
+
status_changed_at
|
338
489
|
end
|
339
490
|
|
340
491
|
# Get the timestamp when the deal was closed (won or lost)
|
341
492
|
# @return [Time, nil] The timestamp when deal was closed, or nil if still open
|
342
493
|
def closed_at
|
343
|
-
|
494
|
+
return nil unless won? || lost?
|
495
|
+
status_changed_at
|
344
496
|
end
|
345
497
|
|
346
498
|
# # Check if the deal is overdue
|
@@ -349,10 +501,89 @@ module Attio
|
|
349
501
|
# return false unless close_date && open?
|
350
502
|
# Date.parse(close_date) < Date.today
|
351
503
|
# end
|
504
|
+
|
505
|
+
# Check if this is an enterprise deal
|
506
|
+
# @return [Boolean] True if amount > 100,000
|
507
|
+
def enterprise?
|
508
|
+
amount > 100_000
|
509
|
+
end
|
510
|
+
|
511
|
+
# Check if this is a mid-market deal
|
512
|
+
# @return [Boolean] True if amount is between 10,000 and 100,000
|
513
|
+
def mid_market?
|
514
|
+
amount.between?(10_000, 100_000)
|
515
|
+
end
|
516
|
+
|
517
|
+
# Check if this is a small deal
|
518
|
+
# @return [Boolean] True if amount < 10,000
|
519
|
+
def small?
|
520
|
+
amount < 10_000
|
521
|
+
end
|
522
|
+
|
523
|
+
# Get the number of days the deal has been in current stage
|
524
|
+
# @return [Integer] Number of days in current stage
|
525
|
+
def days_in_stage
|
526
|
+
return 0 unless status_changed_at
|
527
|
+
((Time.now - status_changed_at) / (24 * 60 * 60)).round
|
528
|
+
end
|
529
|
+
|
530
|
+
# Check if the deal is stale (no activity for specified days)
|
531
|
+
# @param days [Integer] Number of days to consider stale (defaults to 30)
|
532
|
+
# @return [Boolean] True if deal is open and hasn't changed in specified days
|
533
|
+
def stale?(days = 30)
|
534
|
+
return false if closed?
|
535
|
+
days_in_stage > days
|
536
|
+
end
|
537
|
+
|
538
|
+
# Check if the deal is closed (won or lost)
|
539
|
+
# @return [Boolean] True if deal is won or lost
|
540
|
+
def closed?
|
541
|
+
won? || lost?
|
542
|
+
end
|
543
|
+
|
544
|
+
# Get a simple summary of the deal
|
545
|
+
# @return [String] Summary string with name, amount, and stage
|
546
|
+
def summary
|
547
|
+
"#{name || "Unnamed Deal"}: #{formatted_amount} (#{stage || "No Stage"})"
|
548
|
+
end
|
549
|
+
|
550
|
+
# Convert to string for display
|
551
|
+
# @return [String] The deal summary
|
552
|
+
def to_s
|
553
|
+
summary
|
554
|
+
end
|
555
|
+
|
556
|
+
# Get deal size category
|
557
|
+
# @return [Symbol] :enterprise, :mid_market, or :small
|
558
|
+
def size_category
|
559
|
+
if enterprise?
|
560
|
+
:enterprise
|
561
|
+
elsif mid_market?
|
562
|
+
:mid_market
|
563
|
+
else
|
564
|
+
:small
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
# Check if deal needs attention (stale and not closed)
|
569
|
+
# @param stale_days [Integer] Days to consider stale
|
570
|
+
# @return [Boolean] True if deal needs attention
|
571
|
+
def needs_attention?(stale_days = 30)
|
572
|
+
!closed? && stale?(stale_days)
|
573
|
+
end
|
574
|
+
|
575
|
+
# Get deal velocity (amount per day if closed)
|
576
|
+
# @return [Float, nil] Amount per day or nil if not closed
|
577
|
+
def velocity
|
578
|
+
return nil unless closed? && closed_at && created_at
|
579
|
+
|
580
|
+
days_to_close = ((closed_at - created_at) / (24 * 60 * 60)).round
|
581
|
+
(days_to_close > 0) ? (amount / days_to_close).round(2) : amount
|
582
|
+
end
|
352
583
|
end
|
353
584
|
|
354
585
|
# Alias for Deal (plural form)
|
355
586
|
# @example
|
356
587
|
# Attio::Deals.create(name: "New Deal", value: 10000)
|
357
588
|
Deals = Deal
|
358
|
-
end
|
589
|
+
end
|
data/lib/attio/resources/meta.rb
CHANGED
@@ -26,7 +26,7 @@ module Attio
|
|
26
26
|
# Build workspace object from flat attributes
|
27
27
|
def workspace
|
28
28
|
return nil unless self[:workspace_id]
|
29
|
-
|
29
|
+
|
30
30
|
{
|
31
31
|
id: self[:workspace_id],
|
32
32
|
name: self[:workspace_name],
|
@@ -34,22 +34,22 @@ module Attio
|
|
34
34
|
logo_url: self[:workspace_logo_url]
|
35
35
|
}.compact
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
# Build token object from flat attributes
|
39
39
|
def token
|
40
40
|
return nil unless self[:client_id]
|
41
|
-
|
41
|
+
|
42
42
|
{
|
43
43
|
id: self[:client_id],
|
44
44
|
type: self[:token_type] || "Bearer",
|
45
45
|
scope: self[:scope]
|
46
46
|
}.compact
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
# Build actor object from flat attributes
|
50
50
|
def actor
|
51
51
|
return nil unless self[:authorized_by_workspace_member_id]
|
52
|
-
|
52
|
+
|
53
53
|
{
|
54
54
|
type: "workspace-member",
|
55
55
|
id: self[:authorized_by_workspace_member_id]
|
@@ -138,7 +138,5 @@ module Attio
|
|
138
138
|
def inspect
|
139
139
|
"#<#{self.class.name}:#{object_id} workspace=#{workspace_slug.inspect} token=#{token_name.inspect}>"
|
140
140
|
end
|
141
|
-
|
142
|
-
private
|
143
141
|
end
|
144
142
|
end
|
@@ -53,7 +53,7 @@ module Attio
|
|
53
53
|
known_opts.each do |opt|
|
54
54
|
opts[opt] = conditions.delete(opt) if conditions.key?(opt)
|
55
55
|
end
|
56
|
-
|
56
|
+
|
57
57
|
# Currently only supports slug
|
58
58
|
if conditions.key?(:slug)
|
59
59
|
slug = conditions[:slug]
|
@@ -66,7 +66,7 @@ module Attio
|
|
66
66
|
raise ArgumentError, "find_by only supports slug attribute for objects"
|
67
67
|
end
|
68
68
|
end
|
69
|
-
|
69
|
+
|
70
70
|
# Find by API slug (deprecated - use find_by(slug: ...) instead)
|
71
71
|
def self.find_by_slug(slug, **opts)
|
72
72
|
find_by(slug: slug, **opts)
|
@@ -251,7 +251,6 @@ module Attio
|
|
251
251
|
super(values: values, **opts)
|
252
252
|
end
|
253
253
|
|
254
|
-
|
255
254
|
# Search people by query
|
256
255
|
# @param query [String] Query to search for
|
257
256
|
def search(query, **opts)
|
@@ -268,7 +267,7 @@ module Attio
|
|
268
267
|
end
|
269
268
|
|
270
269
|
private
|
271
|
-
|
270
|
+
|
272
271
|
# Build filter for email field
|
273
272
|
def filter_by_email(value)
|
274
273
|
{
|
@@ -279,7 +278,7 @@ module Attio
|
|
279
278
|
}
|
280
279
|
}
|
281
280
|
end
|
282
|
-
|
281
|
+
|
283
282
|
# Build filter for name field (searches across first, last, and full name)
|
284
283
|
def filter_by_name(value)
|
285
284
|
{
|
@@ -64,18 +64,18 @@ module Attio
|
|
64
64
|
# Supports Rails-style hash syntax: find_by(name: "Test")
|
65
65
|
def find_by(**conditions)
|
66
66
|
raise ArgumentError, "find_by requires at least one condition" if conditions.empty?
|
67
|
-
|
67
|
+
|
68
68
|
# Extract any opts that aren't conditions (like api_key)
|
69
69
|
opts = {}
|
70
70
|
known_opts = [:api_key, :timeout, :idempotency_key]
|
71
71
|
known_opts.each do |opt|
|
72
72
|
opts[opt] = conditions.delete(opt) if conditions.key?(opt)
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
# Build filter from conditions
|
76
76
|
filters = []
|
77
77
|
search_query = nil
|
78
|
-
|
78
|
+
|
79
79
|
conditions.each do |field, value|
|
80
80
|
# Check if there's a special filter method for this field
|
81
81
|
filter_method = "filter_by_#{field}"
|
@@ -92,7 +92,7 @@ module Attio
|
|
92
92
|
filters << {field => value}
|
93
93
|
end
|
94
94
|
end
|
95
|
-
|
95
|
+
|
96
96
|
# If we have a search query, use search instead of filter
|
97
97
|
if search_query
|
98
98
|
search(search_query, **opts).first
|
@@ -105,7 +105,7 @@ module Attio
|
|
105
105
|
else
|
106
106
|
{}
|
107
107
|
end
|
108
|
-
|
108
|
+
|
109
109
|
list(**opts.merge(params: {
|
110
110
|
filter: final_filter
|
111
111
|
})).first
|