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 +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +13 -0
- data/examples/deals.rb +2 -2
- data/lib/attio/concerns/time_filterable.rb +18 -18
- data/lib/attio/internal/record.rb +1 -1
- data/lib/attio/resources/company.rb +3 -4
- data/lib/attio/resources/deal.rb +81 -77
- 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 +14 -14
- data/lib/attio/util/time_period.rb +40 -40
- data/lib/attio/version.rb +1 -1
- data/lib/attio-ruby.rb +1 -1
- metadata +1 -2
- data/attio-ruby.gemspec +0 -61
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55a5174c89fcc718a7152c4ab1027c5dd82766d6c35620e7dc3c8acc58b3f983
|
4
|
+
data.tar.gz: 205f0e84a49ba3e7f22d3a9d99a42586d6469977c84aea3163e16e9f80b7cd33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce8cb37b9adb959502703ff0e0bf04b96f757457494e9563f69dce4b6b104e534eaff0fc4badbf176fd4e5aa181dac867617261cf94a3e9cd5c66a7c64aa2052
|
7
|
+
data.tar.gz: 7318b994c5ba70fa0f57af7d2555711d6262c0a0fdec8aeef972ba77b79b7d4717a0c2ccaea6743bef1ee588f6412e40536a8ef4b7806e34dc26d5cb94c6b524
|
data/.rubocop.yml
CHANGED
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: {
|
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
|
-
|
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
|
{
|
data/lib/attio/resources/deal.rb
CHANGED
@@ -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) -
|
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
|
-
|
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
|
-
{
|
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| {
|
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
|
-
|
89
|
+
filter = if stage_names.length == 1
|
90
|
+
{stage: stage_names.first}
|
92
91
|
else
|
93
92
|
# Multiple stages need $or operator
|
94
|
-
|
95
|
-
"$or": stage_names.map { |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
|
-
#
|
154
|
-
# {stage: {"$
|
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
|
-
{
|
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: {
|
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
|
-
{
|
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: {
|
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
|
-
{
|
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
|
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 ||
|
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 &&
|
575
|
-
|
576
|
-
days_to_close = ((closed_at -
|
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
|
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
|
@@ -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
|
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
|
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).
|
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).
|
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
|
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
|
-
|
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
data/lib/attio-ruby.rb
CHANGED
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
|
+
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
|