attio-ruby 0.1.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '033186202207414f4872ff4843ee0f4983fbe4fb13cd413654f6240346349cde'
4
- data.tar.gz: b5182d4744107a37af2e3326f017ecdbe9865b54323f9c0ca9168d3c2baf171e
3
+ metadata.gz: 55a5174c89fcc718a7152c4ab1027c5dd82766d6c35620e7dc3c8acc58b3f983
4
+ data.tar.gz: 205f0e84a49ba3e7f22d3a9d99a42586d6469977c84aea3163e16e9f80b7cd33
5
5
  SHA512:
6
- metadata.gz: ecad0920b62fdd402335d459ab64b9a8ee2f5afcc881680760ba96d01a4abe515afd0981f6d79792e32f20d5deab90a5e092c1f51e19abe8fc497ecb60626557
7
- data.tar.gz: b54ad915227b50620c70920d633358f829619949e8436487c7769a0bad137cf2529d52fe5c4803ffadd25abf34cb3ad11111baec7237ff7235a973d253aa556b
6
+ metadata.gz: ce8cb37b9adb959502703ff0e0bf04b96f757457494e9563f69dce4b6b104e534eaff0fc4badbf176fd4e5aa181dac867617261cf94a3e9cd5c66a7c64aa2052
7
+ data.tar.gz: 7318b994c5ba70fa0f57af7d2555711d6262c0a0fdec8aeef972ba77b79b7d4717a0c2ccaea6743bef1ee588f6412e40536a8ef4b7806e34dc26d5cb94c6b524
data/.rubocop.yml CHANGED
@@ -84,7 +84,7 @@ Style/Documentation:
84
84
 
85
85
  # RSpec Configuration
86
86
  RSpec/ExampleLength:
87
- Max: 30
87
+ Max: 45 # API mocking tests require comprehensive setup
88
88
  Exclude:
89
89
  - 'spec/integration/**/*'
90
90
 
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.5] - 2025-08-11
9
+
10
+ ### Fixed
11
+ - **Code Quality**: Comprehensive RuboCop compliance fixes
12
+ - Replaced all instance variables with `let` helpers in specs
13
+ - Fixed nested describe/context blocks exceeding depth limits
14
+ - Changed `before(:all)` to `before` to avoid state leakage
15
+ - Used proper RSpec matchers (`all` instead of iteration)
16
+ - Fixed context wording to follow RSpec conventions
17
+ - Used `described_class` instead of explicit class names
18
+ - Refactored long test examples for better readability
19
+ - Fixed indentation and formatting issues throughout
20
+
8
21
  ## [0.1.4] - 2025-08-08
9
22
 
10
23
  ### Added
data/examples/deals.rb CHANGED
@@ -69,7 +69,7 @@ puts "Is lost? #{deal.lost?}"
69
69
  puts "\n=== Deal Pipeline Analysis ==="
70
70
 
71
71
  # List all deals and analyze by stage
72
- all_deals = Attio::Deal.list(params: { limit: 50 })
72
+ all_deals = Attio::Deal.list(params: {limit: 50})
73
73
  stage_counts = Hash.new(0)
74
74
 
75
75
  all_deals.each do |d|
@@ -109,4 +109,4 @@ puts "\n=== Cleanup ==="
109
109
  puts "Deleted deal: #{d.name}"
110
110
  end
111
111
 
112
- puts "\nDone!"
112
+ puts "\nDone!"
@@ -24,7 +24,7 @@ module Attio
24
24
  else
25
25
  record[date_field]
26
26
  end
27
-
27
+
28
28
  if date_value
29
29
  parsed_date = date_value.is_a?(String) ? Time.parse(date_value) : date_value
30
30
  period.includes?(parsed_date)
@@ -33,39 +33,39 @@ module Attio
33
33
  end
34
34
  end
35
35
  end
36
-
36
+
37
37
  # Get records created in the last N days
38
38
  # @param days [Integer] Number of days to look back
39
39
  # @return [Array] Recently created records
40
40
  def recently_created(days = 7, **opts)
41
41
  in_period(Util::TimePeriod.last_days(days), date_field: :created_at, **opts)
42
42
  end
43
-
43
+
44
44
  # Get records updated in the last N days
45
45
  # @param days [Integer] Number of days to look back
46
46
  # @return [Array] Recently updated records
47
47
  def recently_updated(days = 7, **opts)
48
48
  in_period(Util::TimePeriod.last_days(days), date_field: :updated_at, **opts)
49
49
  end
50
-
50
+
51
51
  # Get records created this year
52
52
  # @return [Array] Records created in current year
53
53
  def created_this_year(**opts)
54
54
  in_period(Util::TimePeriod.current_year, date_field: :created_at, **opts)
55
55
  end
56
-
56
+
57
57
  # Get records created this month
58
58
  # @return [Array] Records created in current month
59
59
  def created_this_month(**opts)
60
60
  in_period(Util::TimePeriod.current_month, date_field: :created_at, **opts)
61
61
  end
62
-
62
+
63
63
  # Get records created year to date
64
64
  # @return [Array] Records created YTD
65
65
  def created_year_to_date(**opts)
66
66
  in_period(Util::TimePeriod.year_to_date, date_field: :created_at, **opts)
67
67
  end
68
-
68
+
69
69
  # Get records created in a specific month
70
70
  # @param year [Integer] The year
71
71
  # @param month [Integer] The month (1-12)
@@ -73,7 +73,7 @@ module Attio
73
73
  def created_in_month(year, month, **opts)
74
74
  in_period(Util::TimePeriod.month(year, month), date_field: :created_at, **opts)
75
75
  end
76
-
76
+
77
77
  # Get records created in a specific quarter
78
78
  # @param year [Integer] The year
79
79
  # @param quarter [Integer] The quarter (1-4)
@@ -81,21 +81,21 @@ module Attio
81
81
  def created_in_quarter(year, quarter, **opts)
82
82
  in_period(Util::TimePeriod.quarter(year, quarter), date_field: :created_at, **opts)
83
83
  end
84
-
84
+
85
85
  # Get records created in a specific year
86
86
  # @param year [Integer] The year
87
87
  # @return [Array] Records created in that year
88
88
  def created_in_year(year, **opts)
89
89
  in_period(Util::TimePeriod.year(year), date_field: :created_at, **opts)
90
90
  end
91
-
91
+
92
92
  # Get activity metrics for a period
93
93
  # @param period [Util::TimePeriod] The time period
94
94
  # @return [Hash] Metrics about records in the period
95
95
  def activity_metrics(period, **opts)
96
96
  created = in_period(period, date_field: :created_at, **opts)
97
97
  updated = in_period(period, date_field: :updated_at, **opts)
98
-
98
+
99
99
  {
100
100
  period: period.label,
101
101
  created_count: created.size,
@@ -104,9 +104,9 @@ module Attio
104
104
  }
105
105
  end
106
106
  end
107
-
107
+
108
108
  # Instance methods for time-based checks
109
-
109
+
110
110
  # Check if this record was created in a specific period
111
111
  # @param period [Util::TimePeriod] The time period
112
112
  # @return [Boolean] True if created in the period
@@ -115,7 +115,7 @@ module Attio
115
115
  date = created_at.is_a?(String) ? Time.parse(created_at) : created_at
116
116
  period.includes?(date)
117
117
  end
118
-
118
+
119
119
  # Check if this record was updated in a specific period
120
120
  # @param period [Util::TimePeriod] The time period
121
121
  # @return [Boolean] True if updated in the period
@@ -124,7 +124,7 @@ module Attio
124
124
  date = updated_at.is_a?(String) ? Time.parse(updated_at) : updated_at
125
125
  period.includes?(date)
126
126
  end
127
-
127
+
128
128
  # Get the age of the record in days
129
129
  # @return [Integer] Days since creation
130
130
  def age_in_days
@@ -132,7 +132,7 @@ module Attio
132
132
  created = created_at.is_a?(String) ? Time.parse(created_at) : created_at
133
133
  ((Time.now - created) / (24 * 60 * 60)).round
134
134
  end
135
-
135
+
136
136
  # Check if record is new (created recently)
137
137
  # @param days [Integer] Number of days to consider "new"
138
138
  # @return [Boolean] True if created within specified days
@@ -140,7 +140,7 @@ module Attio
140
140
  age = age_in_days
141
141
  age && age <= days
142
142
  end
143
-
143
+
144
144
  # Check if record is old
145
145
  # @param days [Integer] Number of days to consider "old"
146
146
  # @return [Boolean] True if created more than specified days ago
@@ -150,4 +150,4 @@ module Attio
150
150
  end
151
151
  end
152
152
  end
153
- end
153
+ end
@@ -341,7 +341,7 @@ module Attio
341
341
  if value_data.key?(:value) || value_data.key?("value")
342
342
  value_data[:value] || value_data["value"]
343
343
  elsif value_data.key?(:target_object) || value_data.key?("target_object") ||
344
- value_data.key?(:referenced_actor_type) || value_data.key?("referenced_actor_type")
344
+ value_data.key?(:referenced_actor_type) || value_data.key?("referenced_actor_type")
345
345
  # Reference value - return the full reference object
346
346
  value_data
347
347
  elsif value_data.key?(:currency_value) || value_data.key?("currency_value")
@@ -176,7 +176,6 @@ module Attio
176
176
  super(values: values, **opts)
177
177
  end
178
178
 
179
-
180
179
  # Find companies by employee count range
181
180
  # @param min [Integer] Minimum employee count
182
181
  # @param max [Integer] Maximum employee count (optional)
@@ -196,9 +195,9 @@ module Attio
196
195
 
197
196
  list(**opts.merge(params: {filter: filter}))
198
197
  end
199
-
198
+
200
199
  private
201
-
200
+
202
201
  # Build filter for domain field
203
202
  def filter_by_domain(value)
204
203
  # Strip protocol if present
@@ -211,7 +210,7 @@ module Attio
211
210
  }
212
211
  }
213
212
  end
214
-
213
+
215
214
  # Build filter for name field
216
215
  def filter_by_name(value)
217
216
  {
@@ -32,53 +32,52 @@ module Attio
32
32
  # @param attributes [Hash] Deal attributes
33
33
  # @option attributes [String] :name Deal name (recommended)
34
34
  # @option attributes [Numeric] :value Deal value (recommended)
35
- # @option attributes [String] :stage Deal stage (recommended) - defaults: "Lead", "In Progress", "Won 🎉", "Lost"
35
+ # @option attributes [String] :stage Deal stage (recommended) - configurable via Attio.configuration
36
36
  # @option attributes [String] :status Deal status (alias for stage)
37
37
  # @option attributes [String] :owner Owner email or workspace member (recommended)
38
38
  # @option attributes [Array<String>] :associated_people Email addresses of associated people
39
39
  # @option attributes [Array<String>] :associated_company Domains of associated companies
40
40
  # @option attributes [Hash] :values Raw values hash (for advanced use)
41
41
  def create(name:, value: nil, stage: nil, status: nil, owner: nil,
42
- associated_people: nil, associated_company: nil, values: {}, **opts)
42
+ associated_people: nil, associated_company: nil, values: {}, **opts)
43
43
  # Name is required and simple
44
44
  values[:name] = name if name && !values[:name]
45
-
45
+
46
46
  # Add optional fields
47
47
  values[:value] = value if value && !values[:value]
48
-
48
+
49
49
  # Handle stage vs status - API uses "stage" but we support both
50
50
  if (stage || status) && !values[:stage]
51
51
  values[:stage] = stage || status
52
52
  end
53
-
54
-
53
+
55
54
  # Handle owner - can be email address or workspace member reference
56
55
  if owner && !values[:owner]
57
56
  values[:owner] = owner
58
57
  end
59
-
58
+
60
59
  # Handle associated people - convert email array to proper format
61
60
  if associated_people && !values[:associated_people]
62
61
  values[:associated_people] = associated_people.map do |email|
63
62
  {
64
63
  target_object: "people",
65
64
  email_addresses: [
66
- { email_address: email }
65
+ {email_address: email}
67
66
  ]
68
67
  }
69
68
  end
70
69
  end
71
-
70
+
72
71
  # Handle associated company - convert domain array to proper format
73
72
  if associated_company && !values[:associated_company]
74
73
  # associated_company can be array of domains or single domain
75
74
  domains = associated_company.is_a?(Array) ? associated_company : [associated_company]
76
75
  values[:associated_company] = {
77
76
  target_object: "companies",
78
- domains: domains.map { |domain| { domain: domain } }
77
+ domains: domains.map { |domain| {domain: domain} }
79
78
  }
80
79
  end
81
-
80
+
82
81
  super(values: values, **opts)
83
82
  end
84
83
 
@@ -87,15 +86,15 @@ module Attio
87
86
  # @return [Attio::ListObject] List of matching deals
88
87
  def in_stage(stage_names:, **opts)
89
88
  # If only one stage, use simple equality
90
- if stage_names.length == 1
91
- filter = { stage: stage_names.first }
89
+ filter = if stage_names.length == 1
90
+ {stage: stage_names.first}
92
91
  else
93
92
  # Multiple stages need $or operator
94
- filter = {
95
- "$or": stage_names.map { |stage| { stage: stage } }
93
+ {
94
+ "$or": stage_names.map { |stage| {stage: stage} }
96
95
  }
97
96
  end
98
-
97
+
99
98
  list(**opts.merge(params: {filter: filter}))
100
99
  end
101
100
 
@@ -126,7 +125,7 @@ module Attio
126
125
  filters = []
127
126
  filters << {value: {"$gte": min}} if min
128
127
  filters << {value: {"$lte": max}} if max
129
-
128
+
130
129
  filter = if filters.length == 1
131
130
  filters.first
132
131
  elsif filters.length > 1
@@ -134,7 +133,7 @@ module Attio
134
133
  else
135
134
  {}
136
135
  end
137
-
136
+
138
137
  list(**opts.merge(params: {filter: filter}))
139
138
  end
140
139
 
@@ -144,14 +143,15 @@ module Attio
144
143
  # def closing_soon(days: 30, **opts)
145
144
  # today = Date.today
146
145
  # end_date = today + days
147
- #
146
+ #
148
147
  # list(**opts.merge(params: {
149
148
  # filter: {
150
149
  # "$and": [
151
150
  # {close_date: {"$gte": today.iso8601}},
152
151
  # {close_date: {"$lte": end_date.iso8601}},
153
- # {stage: {"$ne": "Won 🎉"}},
154
- # {stage: {"$ne": "Lost"}}
152
+ # # Exclude won and lost statuses
153
+ # {"$not": {stage: {"$in": Attio.configuration.won_statuses}}},
154
+ # {"$not": {stage: {"$in": Attio.configuration.lost_statuses}}}
155
155
  # ]
156
156
  # }
157
157
  # }))
@@ -170,7 +170,7 @@ module Attio
170
170
  }
171
171
  }))
172
172
  end
173
-
173
+
174
174
  # Get deals that closed in a specific time period
175
175
  # @param period [Util::TimePeriod] The time period
176
176
  # @return [Array<Attio::Deal>] List of deals closed in the period
@@ -180,7 +180,7 @@ module Attio
180
180
  closed_date && period.includes?(closed_date)
181
181
  end
182
182
  end
183
-
183
+
184
184
  # Get deals that closed in a specific quarter
185
185
  # @param year [Integer] The year
186
186
  # @param quarter [Integer] The quarter (1-4)
@@ -189,7 +189,7 @@ module Attio
189
189
  period = Util::TimePeriod.quarter(year, quarter)
190
190
  closed_in_period(period, **opts)
191
191
  end
192
-
192
+
193
193
  # Get metrics for any time period
194
194
  # @param period [Util::TimePeriod] The time period
195
195
  # @return [Hash] Metrics for the period
@@ -205,29 +205,33 @@ module Attio
205
205
  }
206
206
  }
207
207
  }
208
-
208
+
209
209
  # Fetch won deals closed in the period
210
+ won_statuses = ::Attio.configuration.won_statuses
211
+ won_conditions = won_statuses.map { |status| {"stage" => status} }
210
212
  won_filter = {
211
213
  "$and" => [
212
- { "stage" => "Won 🎉" },
214
+ ((won_conditions.size > 1) ? {"$or" => won_conditions} : won_conditions.first),
213
215
  date_filter
214
- ]
216
+ ].compact
215
217
  }
216
- won_response = list(**opts.merge(params: { filter: won_filter }))
217
-
218
+ won_response = list(**opts.merge(params: {filter: won_filter}))
219
+
218
220
  # Fetch lost deals closed in the period
221
+ lost_statuses = ::Attio.configuration.lost_statuses
222
+ lost_conditions = lost_statuses.map { |status| {"stage" => status} }
219
223
  lost_filter = {
220
224
  "$and" => [
221
- { "stage" => "Lost" },
225
+ ((lost_conditions.size > 1) ? {"$or" => lost_conditions} : lost_conditions.first),
222
226
  date_filter
223
- ]
227
+ ].compact
224
228
  }
225
- lost_response = list(**opts.merge(params: { filter: lost_filter }))
226
-
229
+ lost_response = list(**opts.merge(params: {filter: lost_filter}))
230
+
227
231
  won_deals = won_response.data
228
232
  lost_deals = lost_response.data
229
233
  total_closed = won_deals.size + lost_deals.size
230
-
234
+
231
235
  {
232
236
  period: period.label,
233
237
  won_count: won_deals.size,
@@ -235,54 +239,54 @@ module Attio
235
239
  lost_count: lost_deals.size,
236
240
  lost_amount: lost_deals.sum(&:amount),
237
241
  total_closed: total_closed,
238
- win_rate: total_closed > 0 ? (won_deals.size.to_f / total_closed * 100).round(2) : 0.0
242
+ win_rate: (total_closed > 0) ? (won_deals.size.to_f / total_closed * 100).round(2) : 0.0
239
243
  }
240
244
  end
241
-
245
+
242
246
  # Get current quarter metrics
243
247
  # @return [Hash] Metrics for the current quarter
244
248
  def current_quarter_metrics(**opts)
245
249
  metrics_for_period(Util::TimePeriod.current_quarter, **opts)
246
250
  end
247
-
251
+
248
252
  # Get year-to-date metrics
249
253
  # @return [Hash] Metrics for year to date
250
254
  def year_to_date_metrics(**opts)
251
255
  metrics_for_period(Util::TimePeriod.year_to_date, **opts)
252
256
  end
253
-
257
+
254
258
  # Get month-to-date metrics
255
259
  # @return [Hash] Metrics for month to date
256
260
  def month_to_date_metrics(**opts)
257
261
  metrics_for_period(Util::TimePeriod.month_to_date, **opts)
258
262
  end
259
-
263
+
260
264
  # Get last 30 days metrics
261
265
  # @return [Hash] Metrics for last 30 days
262
266
  def last_30_days_metrics(**opts)
263
267
  metrics_for_period(Util::TimePeriod.last_30_days, **opts)
264
268
  end
265
-
269
+
266
270
  # Get high-value deals above a threshold
267
271
  # @param threshold [Numeric] The minimum value threshold (defaults to 50,000)
268
272
  # @return [Array<Attio::Deal>] List of high-value deals
269
273
  def high_value(threshold = 50_000, **opts)
270
274
  all(**opts).select { |deal| deal.amount > threshold }
271
275
  end
272
-
276
+
273
277
  # Get deals without owners
274
278
  # @return [Array<Attio::Deal>] List of unassigned deals
275
279
  def unassigned(**opts)
276
280
  all(**opts).select { |deal| deal.owner.nil? }
277
281
  end
278
-
282
+
279
283
  # Get recently created deals
280
284
  # @param days [Integer] Number of days to look back (defaults to 7)
281
285
  # @return [Array<Attio::Deal>] List of recently created deals
282
286
  def recently_created(days = 7, **opts)
283
287
  created_in_period(Util::TimePeriod.last_days(days), **opts)
284
288
  end
285
-
289
+
286
290
  # Get deals created in a specific period
287
291
  # @param period [Util::TimePeriod] The time period
288
292
  # @return [Array<Attio::Deal>] List of deals created in the period
@@ -292,12 +296,12 @@ module Attio
292
296
  created_at && period.includes?(created_at)
293
297
  end
294
298
  end
295
-
299
+
296
300
  private
297
-
301
+
298
302
  # Build filter for status field (maps to stage)
299
303
  def filter_by_status(value)
300
- { stage: value }
304
+ {stage: value}
301
305
  end
302
306
  end
303
307
 
@@ -313,20 +317,20 @@ module Attio
313
317
  return 0.0 unless self[:value].is_a?(Hash)
314
318
  (self[:value]["currency_value"] || 0).to_f
315
319
  end
316
-
320
+
317
321
  # Get the currency code
318
322
  # @return [String] The currency code (defaults to "USD")
319
323
  def currency
320
324
  return "USD" unless self[:value].is_a?(Hash)
321
325
  self[:value]["currency_code"] || "USD"
322
326
  end
323
-
327
+
324
328
  # Get formatted amount for display
325
329
  # @return [String] The formatted currency amount
326
330
  def formatted_amount
327
331
  Util::CurrencyFormatter.format(amount, currency)
328
332
  end
329
-
333
+
330
334
  # Get the raw deal value (for backward compatibility)
331
335
  # @deprecated Use {#amount} for monetary values or {#raw_value} for raw API response
332
336
  # @return [Object] The raw value from the API
@@ -334,7 +338,7 @@ module Attio
334
338
  warn "[DEPRECATION] `value` is deprecated. Use `amount` for monetary values or `raw_value` for the raw API response." unless ENV["ATTIO_SUPPRESS_DEPRECATION"]
335
339
  amount
336
340
  end
337
-
341
+
338
342
  # Get the raw value data from the API
339
343
  # @return [Object] The raw value data
340
344
  def raw_value
@@ -346,11 +350,11 @@ module Attio
346
350
  def stage
347
351
  stage_data = self[:stage]
348
352
  return nil unless stage_data.is_a?(Hash)
349
-
353
+
350
354
  # Attio always returns stage as a hash with nested status.title
351
355
  stage_data.dig("status", "title")
352
356
  end
353
-
357
+
354
358
  # Alias for stage (for compatibility)
355
359
  # @return [String, nil] The deal stage
356
360
  alias_method :status, :stage
@@ -385,7 +389,7 @@ module Attio
385
389
  def update_stage(new_stage, **opts)
386
390
  self.class.update(id, values: {stage: new_stage}, **opts)
387
391
  end
388
-
392
+
389
393
  # Alias for update_stage (for compatibility)
390
394
  # @param new_status [String] The new status/stage
391
395
  # @return [Attio::Deal] The updated deal
@@ -411,7 +415,7 @@ module Attio
411
415
  # @return [Attio::Company, nil] The company record if associated
412
416
  def company_record(**opts)
413
417
  return nil unless company
414
-
418
+
415
419
  company_id = company.is_a?(Hash) ? company["target_record_id"] : company
416
420
  Company.retrieve(company_id, **opts) if company_id
417
421
  end
@@ -420,7 +424,7 @@ module Attio
420
424
  # @return [Attio::WorkspaceMember, nil] The owner record if assigned
421
425
  def owner_record(**opts)
422
426
  return nil unless owner
423
-
427
+
424
428
  owner_id = if owner.is_a?(Hash)
425
429
  owner["referenced_actor_id"] || owner["target_record_id"]
426
430
  else
@@ -446,7 +450,7 @@ module Attio
446
450
  # @return [Time, nil] The timestamp when status changed
447
451
  def status_changed_at
448
452
  return nil unless self[:stage].is_a?(Hash)
449
-
453
+
450
454
  # Attio returns active_from at the top level of the stage hash
451
455
  timestamp = self[:stage]["active_from"]
452
456
  timestamp ? Time.parse(timestamp) : nil
@@ -456,7 +460,7 @@ module Attio
456
460
  # @return [Boolean] True if the deal is open
457
461
  def open?
458
462
  return false unless current_status
459
-
463
+
460
464
  all_open_statuses = Attio.configuration.open_statuses + Attio.configuration.in_progress_statuses
461
465
  all_open_statuses.include?(current_status)
462
466
  end
@@ -465,7 +469,7 @@ module Attio
465
469
  # @return [Boolean] True if the deal is won
466
470
  def won?
467
471
  return false unless current_status
468
-
472
+
469
473
  Attio.configuration.won_statuses.include?(current_status)
470
474
  end
471
475
 
@@ -473,7 +477,7 @@ module Attio
473
477
  # @return [Boolean] True if the deal is lost
474
478
  def lost?
475
479
  return false unless current_status
476
-
480
+
477
481
  Attio.configuration.lost_statuses.include?(current_status)
478
482
  end
479
483
 
@@ -487,7 +491,7 @@ module Attio
487
491
  # Get the timestamp when the deal was closed (won or lost)
488
492
  # @return [Time, nil] The timestamp when deal was closed, or nil if still open
489
493
  def closed_at
490
- return nil unless (won? || lost?)
494
+ return nil unless won? || lost?
491
495
  status_changed_at
492
496
  end
493
497
 
@@ -497,32 +501,32 @@ module Attio
497
501
  # return false unless close_date && open?
498
502
  # Date.parse(close_date) < Date.today
499
503
  # end
500
-
504
+
501
505
  # Check if this is an enterprise deal
502
506
  # @return [Boolean] True if amount > 100,000
503
507
  def enterprise?
504
508
  amount > 100_000
505
509
  end
506
-
510
+
507
511
  # Check if this is a mid-market deal
508
512
  # @return [Boolean] True if amount is between 10,000 and 100,000
509
513
  def mid_market?
510
514
  amount.between?(10_000, 100_000)
511
515
  end
512
-
516
+
513
517
  # Check if this is a small deal
514
518
  # @return [Boolean] True if amount < 10,000
515
519
  def small?
516
520
  amount < 10_000
517
521
  end
518
-
522
+
519
523
  # Get the number of days the deal has been in current stage
520
524
  # @return [Integer] Number of days in current stage
521
525
  def days_in_stage
522
526
  return 0 unless status_changed_at
523
527
  ((Time.now - status_changed_at) / (24 * 60 * 60)).round
524
528
  end
525
-
529
+
526
530
  # Check if the deal is stale (no activity for specified days)
527
531
  # @param days [Integer] Number of days to consider stale (defaults to 30)
528
532
  # @return [Boolean] True if deal is open and hasn't changed in specified days
@@ -530,25 +534,25 @@ module Attio
530
534
  return false if closed?
531
535
  days_in_stage > days
532
536
  end
533
-
537
+
534
538
  # Check if the deal is closed (won or lost)
535
539
  # @return [Boolean] True if deal is won or lost
536
540
  def closed?
537
541
  won? || lost?
538
542
  end
539
-
543
+
540
544
  # Get a simple summary of the deal
541
545
  # @return [String] Summary string with name, amount, and stage
542
546
  def summary
543
- "#{name || 'Unnamed Deal'}: #{formatted_amount} (#{stage || 'No Stage'})"
547
+ "#{name || "Unnamed Deal"}: #{formatted_amount} (#{stage || "No Stage"})"
544
548
  end
545
-
549
+
546
550
  # Convert to string for display
547
551
  # @return [String] The deal summary
548
552
  def to_s
549
553
  summary
550
554
  end
551
-
555
+
552
556
  # Get deal size category
553
557
  # @return [Symbol] :enterprise, :mid_market, or :small
554
558
  def size_category
@@ -560,21 +564,21 @@ module Attio
560
564
  :small
561
565
  end
562
566
  end
563
-
567
+
564
568
  # Check if deal needs attention (stale and not closed)
565
569
  # @param stale_days [Integer] Days to consider stale
566
570
  # @return [Boolean] True if deal needs attention
567
571
  def needs_attention?(stale_days = 30)
568
572
  !closed? && stale?(stale_days)
569
573
  end
570
-
574
+
571
575
  # Get deal velocity (amount per day if closed)
572
576
  # @return [Float, nil] Amount per day or nil if not closed
573
577
  def velocity
574
- return nil unless closed? && closed_at && self.created_at
575
-
576
- days_to_close = ((closed_at - self.created_at) / (24 * 60 * 60)).round
577
- days_to_close > 0 ? (amount / days_to_close).round(2) : amount
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
578
582
  end
579
583
  end
580
584
 
@@ -582,4 +586,4 @@ module Attio
582
586
  # @example
583
587
  # Attio::Deals.create(name: "New Deal", value: 10000)
584
588
  Deals = Deal
585
- end
589
+ end
@@ -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
@@ -111,7 +111,7 @@ module Attio
111
111
  known_opts.each do |opt|
112
112
  opts[opt] = conditions.delete(opt) if conditions.key?(opt)
113
113
  end
114
-
114
+
115
115
  # Currently only supports email
116
116
  if conditions.key?(:email)
117
117
  email = conditions[:email]
@@ -42,10 +42,10 @@ module Attio
42
42
  "PEN" => "S/",
43
43
  "ARS" => "$"
44
44
  }.freeze
45
-
45
+
46
46
  # Currencies that typically don't use decimal places
47
47
  NO_DECIMAL_CURRENCIES = %w[JPY KRW VND IDR CLP].freeze
48
-
48
+
49
49
  class << self
50
50
  # Format an amount with the appropriate currency symbol
51
51
  # @param amount [Numeric] The amount to format
@@ -58,12 +58,12 @@ module Attio
58
58
  def format(amount, currency_code = "USD", options = {})
59
59
  currency_code = currency_code.to_s.upcase
60
60
  symbol = symbol_for(currency_code)
61
-
61
+
62
62
  # Determine decimal places
63
63
  decimal_places = options[:decimal_places] || decimal_places_for(currency_code)
64
64
  thousands_sep = options[:thousands_separator] || ","
65
65
  decimal_sep = options[:decimal_separator] || "."
66
-
66
+
67
67
  # Handle zero amounts
68
68
  if amount == 0
69
69
  if decimal_places > 0
@@ -72,11 +72,11 @@ module Attio
72
72
  return "#{symbol}0"
73
73
  end
74
74
  end
75
-
75
+
76
76
  # Handle negative amounts
77
77
  negative = amount < 0
78
78
  abs_amount = amount.abs
79
-
79
+
80
80
  # Format the amount
81
81
  if decimal_places == 0
82
82
  # No decimal places
@@ -86,14 +86,14 @@ module Attio
86
86
  else
87
87
  # With decimal places
88
88
  whole = abs_amount.to_i
89
- decimal = ((abs_amount - whole) * (10 ** decimal_places)).round
89
+ decimal = ((abs_amount - whole) * (10**decimal_places)).round
90
90
  formatted_whole = format_with_separators(whole, thousands_sep)
91
91
  formatted_whole = "-#{formatted_whole}" if negative
92
92
  formatted_decimal = decimal.to_s.rjust(decimal_places, "0")
93
93
  "#{symbol}#{formatted_whole}#{decimal_sep}#{formatted_decimal}"
94
94
  end
95
95
  end
96
-
96
+
97
97
  # Get the currency symbol for a given code
98
98
  # @param currency_code [String] The ISO 4217 currency code
99
99
  # @return [String] The currency symbol or code with space
@@ -101,7 +101,7 @@ module Attio
101
101
  currency_code = currency_code.to_s.upcase
102
102
  CURRENCY_SYMBOLS[currency_code] || "#{currency_code} "
103
103
  end
104
-
104
+
105
105
  # Determine the number of decimal places for a currency
106
106
  # @param currency_code [String] The ISO 4217 currency code
107
107
  # @return [Integer] Number of decimal places
@@ -109,14 +109,14 @@ module Attio
109
109
  currency_code = currency_code.to_s.upcase
110
110
  NO_DECIMAL_CURRENCIES.include?(currency_code) ? 0 : 2
111
111
  end
112
-
112
+
113
113
  # Check if a currency typically uses decimal places
114
114
  # @param currency_code [String] The ISO 4217 currency code
115
115
  # @return [Boolean] True if the currency uses decimals
116
116
  def uses_decimals?(currency_code)
117
117
  decimal_places_for(currency_code) > 0
118
118
  end
119
-
119
+
120
120
  # Format just the numeric part without currency symbol
121
121
  # @param amount [Numeric] The amount to format
122
122
  # @param currency_code [String] The ISO 4217 currency code
@@ -127,9 +127,9 @@ module Attio
127
127
  symbol = symbol_for(currency_code)
128
128
  result.sub(/^#{Regexp.escape(symbol)}/, "")
129
129
  end
130
-
130
+
131
131
  private
132
-
132
+
133
133
  # Add thousands separators to a number
134
134
  # @param number [Integer] The number to format
135
135
  # @param separator [String] The separator character
@@ -140,4 +140,4 @@ module Attio
140
140
  end
141
141
  end
142
142
  end
143
- end
143
+ end
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'date'
3
+ require "date"
4
4
 
5
5
  module Attio
6
6
  module Util
7
7
  # Utility class for time period calculations
8
8
  class TimePeriod
9
9
  attr_reader :start_date, :end_date
10
-
10
+
11
11
  def initialize(start_date, end_date)
12
12
  @start_date = parse_date(start_date)
13
13
  @end_date = parse_date(end_date)
14
14
  end
15
-
15
+
16
16
  private
17
-
17
+
18
18
  def parse_date(date)
19
19
  case date
20
20
  when Date
@@ -25,23 +25,23 @@ module Attio
25
25
  date.to_date
26
26
  end
27
27
  end
28
-
28
+
29
29
  public
30
-
30
+
31
31
  # Named constructors for common periods
32
-
32
+
33
33
  # Current year to date
34
34
  def self.year_to_date
35
35
  today = Date.today
36
36
  new(Date.new(today.year, 1, 1), today)
37
37
  end
38
-
38
+
39
39
  # Current month to date
40
40
  def self.month_to_date
41
41
  today = Date.today
42
42
  new(Date.new(today.year, today.month, 1), today)
43
43
  end
44
-
44
+
45
45
  # Current quarter to date
46
46
  def self.quarter_to_date
47
47
  today = Date.today
@@ -49,21 +49,21 @@ module Attio
49
49
  quarter_start = Date.new(today.year, (quarter - 1) * 3 + 1, 1)
50
50
  new(quarter_start, today)
51
51
  end
52
-
52
+
53
53
  # Specific quarter
54
54
  def self.quarter(year, quarter_num)
55
- raise ArgumentError, "Quarter must be between 1 and 4" unless (1..4).include?(quarter_num)
55
+ raise ArgumentError, "Quarter must be between 1 and 4" unless (1..4).cover?(quarter_num)
56
56
  quarter_start = Date.new(year, (quarter_num - 1) * 3 + 1, 1)
57
57
  quarter_end = (quarter_start >> 3) - 1
58
58
  new(quarter_start, quarter_end)
59
59
  end
60
-
60
+
61
61
  # Current quarter (full quarter, not QTD)
62
62
  def self.current_quarter
63
63
  today = Date.today
64
64
  quarter(today.year, (today.month - 1) / 3 + 1)
65
65
  end
66
-
66
+
67
67
  # Previous quarter
68
68
  def self.previous_quarter
69
69
  today = Date.today
@@ -74,21 +74,21 @@ module Attio
74
74
  quarter(today.year, current_q - 1)
75
75
  end
76
76
  end
77
-
77
+
78
78
  # Specific month
79
79
  def self.month(year, month_num)
80
- raise ArgumentError, "Month must be between 1 and 12" unless (1..12).include?(month_num)
80
+ raise ArgumentError, "Month must be between 1 and 12" unless (1..12).cover?(month_num)
81
81
  month_start = Date.new(year, month_num, 1)
82
82
  month_end = (month_start >> 1) - 1
83
83
  new(month_start, month_end)
84
84
  end
85
-
85
+
86
86
  # Current month (full month, not MTD)
87
87
  def self.current_month
88
88
  today = Date.today
89
89
  month(today.year, today.month)
90
90
  end
91
-
91
+
92
92
  # Previous month
93
93
  def self.previous_month
94
94
  today = Date.today
@@ -98,71 +98,71 @@ module Attio
98
98
  month(today.year, today.month - 1)
99
99
  end
100
100
  end
101
-
101
+
102
102
  # Specific year
103
103
  def self.year(year_num)
104
104
  new(Date.new(year_num, 1, 1), Date.new(year_num, 12, 31))
105
105
  end
106
-
106
+
107
107
  # Current year (full year, not YTD)
108
108
  def self.current_year
109
109
  year(Date.today.year)
110
110
  end
111
-
111
+
112
112
  # Previous year
113
113
  def self.previous_year
114
114
  year(Date.today.year - 1)
115
115
  end
116
-
116
+
117
117
  # Last N days (including today)
118
118
  def self.last_days(num_days)
119
119
  today = Date.today
120
120
  new(today - num_days + 1, today)
121
121
  end
122
-
122
+
123
123
  # Last 7 days
124
124
  def self.last_week
125
125
  last_days(7)
126
126
  end
127
-
127
+
128
128
  # Last 30 days
129
129
  def self.last_30_days
130
130
  last_days(30)
131
131
  end
132
-
132
+
133
133
  # Last 90 days
134
134
  def self.last_90_days
135
135
  last_days(90)
136
136
  end
137
-
137
+
138
138
  # Last 365 days
139
139
  def self.last_year_rolling
140
140
  last_days(365)
141
141
  end
142
-
142
+
143
143
  # Custom range
144
144
  def self.between(start_date, end_date)
145
145
  new(start_date, end_date)
146
146
  end
147
-
147
+
148
148
  # Instance methods
149
-
149
+
150
150
  # Check if a date falls within this period
151
151
  def includes?(date)
152
152
  date = date.to_date
153
- date >= @start_date && date <= @end_date
153
+ date.between?(@start_date, @end_date)
154
154
  end
155
-
155
+
156
156
  # Get the date range
157
157
  def to_range
158
158
  @start_date..@end_date
159
159
  end
160
-
160
+
161
161
  # Number of days in the period
162
162
  def days
163
163
  (@end_date - @start_date).to_i + 1
164
164
  end
165
-
165
+
166
166
  # String representation
167
167
  def to_s
168
168
  if @start_date == @end_date
@@ -171,18 +171,18 @@ module Attio
171
171
  "#{@start_date} to #{@end_date}"
172
172
  end
173
173
  end
174
-
174
+
175
175
  # Human-readable label
176
176
  def label
177
177
  today = Date.today
178
-
178
+
179
179
  # Check for common patterns
180
180
  if @start_date == Date.new(today.year, 1, 1) && @end_date == today
181
181
  "Year to Date"
182
182
  elsif @start_date == Date.new(today.year, today.month, 1) && @end_date == today
183
183
  "Month to Date"
184
184
  elsif @start_date == Date.new(today.year, 1, 1) && @end_date == Date.new(today.year, 12, 31)
185
- "#{today.year}"
185
+ today.year.to_s
186
186
  elsif @start_date.day == 1 && @end_date == (@start_date >> 1) - 1
187
187
  @start_date.strftime("%B %Y")
188
188
  elsif days == 7 && @end_date == today
@@ -195,19 +195,19 @@ module Attio
195
195
  # Check for quarters
196
196
  quarter = detect_quarter
197
197
  return quarter if quarter
198
-
198
+
199
199
  to_s
200
200
  end
201
201
  end
202
-
202
+
203
203
  private
204
-
204
+
205
205
  def detect_quarter
206
206
  # Check if this is a complete quarter
207
207
  [1, 2, 3, 4].each do |q|
208
208
  quarter_start = Date.new(@start_date.year, (q - 1) * 3 + 1, 1)
209
209
  quarter_end = (quarter_start >> 3) - 1
210
-
210
+
211
211
  if @start_date == quarter_start && @end_date == quarter_end
212
212
  return "Q#{q} #{@start_date.year}"
213
213
  end
@@ -216,4 +216,4 @@ module Attio
216
216
  end
217
217
  end
218
218
  end
219
- end
219
+ end
data/lib/attio/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Attio
4
4
  # Current version of the Attio Ruby gem
5
- VERSION = "0.1.4"
5
+ VERSION = "0.1.5"
6
6
  end
data/lib/attio-ruby.rb CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  # This file exists to match the gem name for auto-requiring
4
4
  # It simply requires the main attio module
5
- require_relative "attio"
5
+ require_relative "attio"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attio-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Beene
@@ -335,7 +335,6 @@ files:
335
335
  - LICENSE
336
336
  - README.md
337
337
  - Rakefile
338
- - attio-ruby.gemspec
339
338
  - docs/CODECOV_SETUP.md
340
339
  - examples/app_specific_typed_record.md
341
340
  - examples/basic_usage.rb
data/attio-ruby.gemspec DELETED
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/attio/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "attio-ruby"
7
- spec.version = Attio::VERSION
8
- spec.authors = ["Robert Beene"]
9
- spec.email = ["robert@ismly.com"]
10
-
11
- spec.summary = "Ruby client library for the Attio API"
12
- spec.description = "A comprehensive Ruby client library for the Attio CRM API with OAuth support, type safety, and extensive test coverage"
13
- spec.homepage = "https://github.com/rbeene/attio_ruby"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.4.0"
16
-
17
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
- spec.metadata["homepage_uri"] = spec.homepage
19
- spec.metadata["source_code_uri"] = "https://github.com/rbeene/attio_ruby"
20
- spec.metadata["changelog_uri"] = "https://github.com/rbeene/attio_ruby/blob/main/CHANGELOG.md"
21
- spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/attio-ruby"
22
- spec.metadata["bug_tracker_uri"] = "https://github.com/rbeene/attio_ruby/issues"
23
-
24
- # Specify which files should be added to the gem when it is released.
25
- spec.files = Dir.chdir(__dir__) do
26
- `git ls-files -z`.split("\x0").reject do |f|
27
- (File.expand_path(f) == __FILE__) ||
28
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
29
- end
30
- end
31
- spec.bindir = "exe"
32
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
- spec.require_paths = ["lib"]
34
-
35
- # Runtime dependencies
36
- spec.add_dependency "faraday", "~> 2.0"
37
- spec.add_dependency "faraday-retry", "~> 2.0"
38
- spec.add_dependency "ostruct", "~> 0.6"
39
-
40
- # Development dependencies
41
- spec.add_development_dependency "bundler", "~> 2.0"
42
- spec.add_development_dependency "rake", "~> 13.0"
43
- spec.add_development_dependency "rspec", "~> 3.12"
44
- spec.add_development_dependency "webmock", "~> 3.18"
45
- spec.add_development_dependency "simplecov", "~> 0.22"
46
- spec.add_development_dependency "simplecov-cobertura", "~> 2.1"
47
- spec.add_development_dependency "yard", "~> 0.9"
48
- spec.add_development_dependency "redcarpet", "~> 3.6"
49
- spec.add_development_dependency "rubocop", "~> 1.50"
50
- spec.add_development_dependency "rubocop-rspec", "~> 2.20"
51
- spec.add_development_dependency "rubocop-performance", "~> 1.17"
52
- spec.add_development_dependency "standard", "~> 1.28"
53
- spec.add_development_dependency "benchmark-ips", "~> 2.12"
54
- spec.add_development_dependency "pry", "~> 0.14"
55
- spec.add_development_dependency "pry-byebug", "~> 3.10"
56
- spec.add_development_dependency "dotenv", "~> 2.8"
57
- spec.add_development_dependency "timecop", "~> 0.9"
58
- spec.add_development_dependency "bundle-audit", "~> 0.1"
59
- spec.add_development_dependency "brakeman", "~> 6.0"
60
- spec.metadata["rubygems_mfa_required"] = "true"
61
- end