Class: Attio::Deal

Inherits:
TypedRecord show all
Defined in:
lib/attio/resources/deal.rb

Overview

Represents a Deal record in Attio

Examples:

Create a deal

deal = Attio::Deal.create(
  name: "Enterprise Deal",
  value: 50000,
  stage: "In Progress"
)

Find deals by status

open_deals = Attio::Deal.find_by(status: "open")
won_deals = Attio::Deal.find_by(status: "won")

Find high-value deals

big_deals = Attio::Deal.find_by_value_range(min: 100000)

Update deal status

deal.update_status("won")

Constant Summary

Constants inherited from APIResource

APIResource::SKIP_KEYS

Instance Attribute Summary

Attributes inherited from Internal::Record

#attio_object_id, #object_api_slug

Attributes inherited from APIResource

#created_at, #id, #metadata

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from TypedRecord

all, delete, #destroy, find, find_by, #initialize, list, object_type, retrieve, #save, search, update

Methods inherited from Internal::Record

#add_to_list, #destroy, #initialize, #inspect, list, #lists, resource_path, #resource_path, retrieve, #save, search, #to_h, update

Methods inherited from APIResource

#==, #[], #[]=, api_operations, attr_attio, #changed, #changed?, #changed_attributes, #changes, #destroy, #each, execute_request, #fetch, #hash, id_param_name, #initialize, #inspect, #key?, #keys, #persisted?, prepare_params_for_create, prepare_params_for_update, #reset_changes!, resource_name, resource_path, #resource_path, #revert!, #save, #to_h, #to_json, #update_attributes, #update_from, validate_id!, #values

Constructor Details

This class inherits a constructor from Attio::TypedRecord

Class Method Details

.closed_in_period(period, **opts) ⇒ Array<Attio::Deal>

Get deals that closed in a specific time period

Parameters:

Returns:

  • (Array<Attio::Deal>)

    List of deals closed in the period



177
178
179
180
181
182
# File 'lib/attio/resources/deal.rb', line 177

def closed_in_period(period, **opts)
  all(**opts).select do |deal|
    closed_date = deal.closed_at
    closed_date && period.includes?(closed_date)
  end
end

.closed_in_quarter(year, quarter, **opts) ⇒ Array<Attio::Deal>

Get deals that closed in a specific quarter

Parameters:

  • year (Integer)

    The year

  • quarter (Integer)

    The quarter (1-4)

Returns:

  • (Array<Attio::Deal>)

    List of deals closed in the quarter



188
189
190
191
# File 'lib/attio/resources/deal.rb', line 188

def closed_in_quarter(year, quarter, **opts)
  period = Util::TimePeriod.quarter(year, quarter)
  closed_in_period(period, **opts)
end

.create(name:, value: nil, stage: nil, status: nil, owner: nil, associated_people: nil, associated_company: nil, values: {}, **opts) ⇒ Object

Create a deal with a simplified interface

Parameters:

  • attributes (Hash)

    Deal attributes



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/attio/resources/deal.rb', line 41

def create(name:, value: nil, stage: nil, status: nil, owner: nil,
  associated_people: nil, associated_company: nil, values: {}, **opts)
  # Name is required and simple
  values[:name] = name if name && !values[:name]

  # Add optional fields
  values[:value] = value if value && !values[:value]

  # Handle stage vs status - API uses "stage" but we support both
  if (stage || status) && !values[:stage]
    values[:stage] = stage || status
  end

  # Handle owner - can be email address or workspace member reference
  if owner && !values[:owner]
    values[:owner] = owner
  end

  # Handle associated people - convert email array to proper format
  if associated_people && !values[:associated_people]
    values[:associated_people] = associated_people.map do |email|
      {
        target_object: "people",
        email_addresses: [
          {email_address: email}
        ]
      }
    end
  end

  # Handle associated company - convert domain array to proper format
  if associated_company && !values[:associated_company]
    # associated_company can be array of domains or single domain
    domains = associated_company.is_a?(Array) ? associated_company : [associated_company]
    values[:associated_company] = {
      target_object: "companies",
      domains: domains.map { |domain| {domain: domain} }
    }
  end

  super(values: values, **opts)
end

.created_in_period(period, **opts) ⇒ Array<Attio::Deal>

Get deals created in a specific period

Parameters:

Returns:

  • (Array<Attio::Deal>)

    List of deals created in the period



293
294
295
296
297
298
# File 'lib/attio/resources/deal.rb', line 293

def created_in_period(period, **opts)
  all(**opts).select do |deal|
    created_at = deal.created_at
    created_at && period.includes?(created_at)
  end
end

.current_quarter_metrics(**opts) ⇒ Hash

Get current quarter metrics

Returns:

  • (Hash)

    Metrics for the current quarter



248
249
250
# File 'lib/attio/resources/deal.rb', line 248

def current_quarter_metrics(**opts)
  metrics_for_period(Util::TimePeriod.current_quarter, **opts)
end

.find_by_owner(owner_id, **opts) ⇒ Attio::ListObject

Find deals by owner

Parameters:

  • owner_id (String)

    The workspace member ID

Returns:

  • (Attio::ListObject)

    List of deals owned by the member



163
164
165
166
167
168
169
170
171
172
# File 'lib/attio/resources/deal.rb', line 163

def find_by_owner(owner_id, **opts)
  list(**opts.merge(params: {
    filter: {
      owner: {
        target_object: "workspace_members",
        target_record_id: owner_id
      }
    }
  }))
end

.find_by_value_range(min: nil, max: nil, **opts) ⇒ Attio::ListObject

Find deals within a value range

Parameters:

  • min (Numeric) (defaults to: nil)

    Minimum value (optional)

  • max (Numeric) (defaults to: nil)

    Maximum value (optional)

Returns:

  • (Attio::ListObject)

    List of matching deals



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/attio/resources/deal.rb', line 124

def find_by_value_range(min: nil, max: nil, **opts)
  filters = []
  filters << {value: {"$gte": min}} if min
  filters << {value: {"$lte": max}} if max

  filter = if filters.length == 1
    filters.first
  elsif filters.length > 1
    {"$and": filters}
  else
    {}
  end

  list(**opts.merge(params: {filter: filter}))
end

.high_value(threshold = 50_000, **opts) ⇒ Array<Attio::Deal>

Get high-value deals above a threshold

Parameters:

  • threshold (Numeric) (defaults to: 50_000)

    The minimum value threshold (defaults to 50,000)

Returns:



273
274
275
# File 'lib/attio/resources/deal.rb', line 273

def high_value(threshold = 50_000, **opts)
  all(**opts).select { |deal| deal.amount > threshold }
end

.in_stage(stage_names:, **opts) ⇒ Attio::ListObject

Find deals by stage names

Parameters:

  • stage_names (Array<String>)

    Array of stage names to filter by

Returns:

  • (Attio::ListObject)

    List of matching deals



87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/attio/resources/deal.rb', line 87

def in_stage(stage_names:, **opts)
  # If only one stage, use simple equality
  filter = if stage_names.length == 1
    {stage: stage_names.first}
  else
    # Multiple stages need $or operator
    {
      "$or": stage_names.map { |stage| {stage: stage} }
    }
  end

  list(**opts.merge(params: {filter: filter}))
end

.last_30_days_metrics(**opts) ⇒ Hash

Get last 30 days metrics

Returns:

  • (Hash)

    Metrics for last 30 days



266
267
268
# File 'lib/attio/resources/deal.rb', line 266

def last_30_days_metrics(**opts)
  metrics_for_period(Util::TimePeriod.last_30_days, **opts)
end

.lost(**opts) ⇒ Attio::ListObject

Find lost deals using configured statuses

Returns:

  • (Attio::ListObject)

    List of lost deals



109
110
111
# File 'lib/attio/resources/deal.rb', line 109

def lost(**opts)
  in_stage(stage_names: Attio.configuration.lost_statuses, **opts)
end

.metrics_for_period(period, **opts) ⇒ Hash

Get metrics for any time period

Parameters:

Returns:

  • (Hash)

    Metrics for the period



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/attio/resources/deal.rb', line 196

def metrics_for_period(period, **opts)
  # Build date filter for stage.active_from
  # Note: We need to add a day to end_date to include all of that day
  # since stage.active_from includes time
  date_filter = {
    "stage" => {
      "active_from" => {
        "$gte" => period.start_date.strftime("%Y-%m-%d"),
        "$lte" => (period.end_date + 1).strftime("%Y-%m-%d")
      }
    }
  }

  # Fetch won deals closed in the period
  won_statuses = ::Attio.configuration.won_statuses
  won_conditions = won_statuses.map { |status| {"stage" => status} }
  won_filter = {
    "$and" => [
      ((won_conditions.size > 1) ? {"$or" => won_conditions} : won_conditions.first),
      date_filter
    ].compact
  }
  won_response = list(**opts.merge(params: {filter: won_filter}))

  # Fetch lost deals closed in the period
  lost_statuses = ::Attio.configuration.lost_statuses
  lost_conditions = lost_statuses.map { |status| {"stage" => status} }
  lost_filter = {
    "$and" => [
      ((lost_conditions.size > 1) ? {"$or" => lost_conditions} : lost_conditions.first),
      date_filter
    ].compact
  }
  lost_response = list(**opts.merge(params: {filter: lost_filter}))

  won_deals = won_response.data
  lost_deals = lost_response.data
  total_closed = won_deals.size + lost_deals.size

  {
    period: period.label,
    won_count: won_deals.size,
    won_amount: won_deals.sum(&:amount),
    lost_count: lost_deals.size,
    lost_amount: lost_deals.sum(&:amount),
    total_closed: total_closed,
    win_rate: (total_closed > 0) ? (won_deals.size.to_f / total_closed * 100).round(2) : 0.0
  }
end

.month_to_date_metrics(**opts) ⇒ Hash

Get month-to-date metrics

Returns:

  • (Hash)

    Metrics for month to date



260
261
262
# File 'lib/attio/resources/deal.rb', line 260

def month_to_date_metrics(**opts)
  metrics_for_period(Util::TimePeriod.month_to_date, **opts)
end

.open_deals(**opts) ⇒ Attio::ListObject

Find open deals (Lead + In Progress) using configured statuses

Returns:

  • (Attio::ListObject)

    List of open deals



115
116
117
118
# File 'lib/attio/resources/deal.rb', line 115

def open_deals(**opts)
  all_open_statuses = Attio.configuration.open_statuses + Attio.configuration.in_progress_statuses
  in_stage(stage_names: all_open_statuses, **opts)
end

.recently_created(days = 7, **opts) ⇒ Array<Attio::Deal>

Get recently created deals

Parameters:

  • days (Integer) (defaults to: 7)

    Number of days to look back (defaults to 7)

Returns:

  • (Array<Attio::Deal>)

    List of recently created deals



286
287
288
# File 'lib/attio/resources/deal.rb', line 286

def recently_created(days = 7, **opts)
  created_in_period(Util::TimePeriod.last_days(days), **opts)
end

.unassigned(**opts) ⇒ Array<Attio::Deal>

Get deals without owners

Returns:



279
280
281
# File 'lib/attio/resources/deal.rb', line 279

def unassigned(**opts)
  all(**opts).select { |deal| deal.owner.nil? }
end

.won(**opts) ⇒ Attio::ListObject

Find won deals using configured statuses

Returns:

  • (Attio::ListObject)

    List of won deals



103
104
105
# File 'lib/attio/resources/deal.rb', line 103

def won(**opts)
  in_stage(stage_names: Attio.configuration.won_statuses, **opts)
end

.year_to_date_metrics(**opts) ⇒ Hash

Get year-to-date metrics

Returns:

  • (Hash)

    Metrics for year to date



254
255
256
# File 'lib/attio/resources/deal.rb', line 254

def year_to_date_metrics(**opts)
  metrics_for_period(Util::TimePeriod.year_to_date, **opts)
end

Instance Method Details

#amountFloat

Get the monetary amount from the deal value

Returns:

  • (Float)

    The deal amount (0.0 if not set)



316
317
318
319
# File 'lib/attio/resources/deal.rb', line 316

def amount
  return 0.0 unless self[:value].is_a?(Hash)
  (self[:value]["currency_value"] || 0).to_f
end

#closed?Boolean

Check if the deal is closed (won or lost)

Returns:

  • (Boolean)

    True if deal is won or lost



540
541
542
# File 'lib/attio/resources/deal.rb', line 540

def closed?
  won? || lost?
end

#closed_atTime?

Get the timestamp when the deal was closed (won or lost)

Returns:

  • (Time, nil)

    The timestamp when deal was closed, or nil if still open



493
494
495
496
# File 'lib/attio/resources/deal.rb', line 493

def closed_at
  return nil unless won? || lost?
  status_changed_at
end

#companyHash?

Get the company reference

Returns:

  • (Hash, nil)

    The company reference



382
383
384
# File 'lib/attio/resources/deal.rb', line 382

def company
  self[:company]
end

#company_record(**opts) ⇒ Attio::Company?

Get the associated company record

Returns:



416
417
418
419
420
421
# File 'lib/attio/resources/deal.rb', line 416

def company_record(**opts)
  return nil unless company

  company_id = company.is_a?(Hash) ? company["target_record_id"] : company
  Company.retrieve(company_id, **opts) if company_id
end

#currencyString

Get the currency code

Returns:

  • (String)

    The currency code (defaults to "USD")



323
324
325
326
# File 'lib/attio/resources/deal.rb', line 323

def currency
  return "USD" unless self[:value].is_a?(Hash)
  self[:value]["currency_code"] || "USD"
end

#current_statusString?

Get the current status title (delegates to stage for simplicity)

Returns:

  • (String, nil)

    The current status title



445
446
447
# File 'lib/attio/resources/deal.rb', line 445

def current_status
  stage
end

#days_in_stageInteger

Get the number of days the deal has been in current stage

Returns:

  • (Integer)

    Number of days in current stage



525
526
527
528
# File 'lib/attio/resources/deal.rb', line 525

def days_in_stage
  return 0 unless status_changed_at
  ((Time.now - status_changed_at) / (24 * 60 * 60)).round
end

#enterprise?Boolean

Check if this is an enterprise deal

Returns:

  • (Boolean)

    True if amount > 100,000



507
508
509
# File 'lib/attio/resources/deal.rb', line 507

def enterprise?
  amount > 100_000
end

#formatted_amountString

Get formatted amount for display

Returns:

  • (String)

    The formatted currency amount



330
331
332
# File 'lib/attio/resources/deal.rb', line 330

def formatted_amount
  Util::CurrencyFormatter.format(amount, currency)
end

#lost?Boolean

Check if the deal is lost

Returns:

  • (Boolean)

    True if the deal is lost



478
479
480
481
482
# File 'lib/attio/resources/deal.rb', line 478

def lost?
  return false unless current_status

  Attio.configuration.lost_statuses.include?(current_status)
end

#mid_market?Boolean

Check if this is a mid-market deal

Returns:

  • (Boolean)

    True if amount is between 10,000 and 100,000



513
514
515
# File 'lib/attio/resources/deal.rb', line 513

def mid_market?
  amount.between?(10_000, 100_000)
end

#nameString?

Get the deal name

Returns:

  • (String, nil)

    The deal name



310
311
312
# File 'lib/attio/resources/deal.rb', line 310

def name
  self[:name]
end

#needs_attention?(stale_days = 30) ⇒ Boolean

Check if deal needs attention (stale and not closed)

Parameters:

  • stale_days (Integer) (defaults to: 30)

    Days to consider stale

Returns:

  • (Boolean)

    True if deal needs attention



571
572
573
# File 'lib/attio/resources/deal.rb', line 571

def needs_attention?(stale_days = 30)
  !closed? && stale?(stale_days)
end

#open?Boolean

Check if the deal is open

Returns:

  • (Boolean)

    True if the deal is open



461
462
463
464
465
466
# File 'lib/attio/resources/deal.rb', line 461

def open?
  return false unless current_status

  all_open_statuses = Attio.configuration.open_statuses + Attio.configuration.in_progress_statuses
  all_open_statuses.include?(current_status)
end

#ownerHash?

Get the owner reference

Returns:

  • (Hash, nil)

    The owner reference



376
377
378
# File 'lib/attio/resources/deal.rb', line 376

def owner
  self[:owner]
end

#owner_record(**opts) ⇒ Attio::WorkspaceMember?

Get the owner workspace member record

Returns:



425
426
427
428
429
430
431
432
433
434
# File 'lib/attio/resources/deal.rb', line 425

def owner_record(**opts)
  return nil unless owner

  owner_id = if owner.is_a?(Hash)
    owner["referenced_actor_id"] || owner["target_record_id"]
  else
    owner
  end
  WorkspaceMember.retrieve(owner_id, **opts) if owner_id
end

#raw_valueObject

Get the raw value data from the API

Returns:

  • (Object)

    The raw value data



344
345
346
# File 'lib/attio/resources/deal.rb', line 344

def raw_value
  self[:value]
end

#size_categorySymbol

Get deal size category

Returns:

  • (Symbol)

    :enterprise, :mid_market, or :small



558
559
560
561
562
563
564
565
566
# File 'lib/attio/resources/deal.rb', line 558

def size_category
  if enterprise?
    :enterprise
  elsif mid_market?
    :mid_market
  else
    :small
  end
end

#small?Boolean

Check if this is a small deal

Returns:

  • (Boolean)

    True if amount < 10,000



519
520
521
# File 'lib/attio/resources/deal.rb', line 519

def small?
  amount < 10_000
end

#stageString? Also known as: status

Get the normalized deal stage/status

Returns:

  • (String, nil)

    The deal stage title



350
351
352
353
354
355
356
# File 'lib/attio/resources/deal.rb', line 350

def stage
  stage_data = self[:stage]
  return nil unless stage_data.is_a?(Hash)

  # Attio always returns stage as a hash with nested status.title
  stage_data.dig("status", "title")
end

#stale?(days = 30) ⇒ Boolean

Check if the deal is stale (no activity for specified days)

Parameters:

  • days (Integer) (defaults to: 30)

    Number of days to consider stale (defaults to 30)

Returns:

  • (Boolean)

    True if deal is open and hasn't changed in specified days



533
534
535
536
# File 'lib/attio/resources/deal.rb', line 533

def stale?(days = 30)
  return false if closed?
  days_in_stage > days
end

#status_changed_atTime?

Get the timestamp when the status changed

Returns:

  • (Time, nil)

    The timestamp when status changed



451
452
453
454
455
456
457
# File 'lib/attio/resources/deal.rb', line 451

def status_changed_at
  return nil unless self[:stage].is_a?(Hash)

  # Attio returns active_from at the top level of the stage hash
  timestamp = self[:stage]["active_from"]
  timestamp ? Time.parse(timestamp) : nil
end

#summaryString

Get a simple summary of the deal

Returns:

  • (String)

    Summary string with name, amount, and stage



546
547
548
# File 'lib/attio/resources/deal.rb', line 546

def summary
  "#{name || "Unnamed Deal"}: #{formatted_amount} (#{stage || "No Stage"})"
end

#to_sString

Convert to string for display

Returns:

  • (String)

    The deal summary



552
553
554
# File 'lib/attio/resources/deal.rb', line 552

def to_s
  summary
end

#update_stage(new_stage, **opts) ⇒ Attio::Deal

Update the deal stage

Parameters:

  • new_stage (String)

    The new stage

Returns:



389
390
391
# File 'lib/attio/resources/deal.rb', line 389

def update_stage(new_stage, **opts)
  self.class.update(id, values: {stage: new_stage}, **opts)
end

#update_status(new_status, **opts) ⇒ Attio::Deal

Alias for update_stage (for compatibility)

Parameters:

  • new_status (String)

    The new status/stage

Returns:



396
397
398
# File 'lib/attio/resources/deal.rb', line 396

def update_status(new_status, **opts)
  update_stage(new_status, **opts)
end

#update_value(new_value, **opts) ⇒ Attio::Deal

Update the deal value

Parameters:

  • new_value (Numeric)

    The new value

Returns:



410
411
412
# File 'lib/attio/resources/deal.rb', line 410

def update_value(new_value, **opts)
  self.class.update(id, values: {value: new_value}, **opts)
end

#valueObject

Deprecated.

Use #amount for monetary values or #raw_value for raw API response

Get the raw deal value (for backward compatibility)

Returns:

  • (Object)

    The raw value from the API



337
338
339
340
# File 'lib/attio/resources/deal.rb', line 337

def value
  warn "[DEPRECATION] `value` is deprecated. Use `amount` for monetary values or `raw_value` for the raw API response." unless ENV["ATTIO_SUPPRESS_DEPRECATION"]
  amount
end

#velocityFloat?

Get deal velocity (amount per day if closed)

Returns:

  • (Float, nil)

    Amount per day or nil if not closed



577
578
579
580
581
582
# File 'lib/attio/resources/deal.rb', line 577

def velocity
  return nil unless closed? && closed_at && created_at

  days_to_close = ((closed_at - created_at) / (24 * 60 * 60)).round
  (days_to_close > 0) ? (amount / days_to_close).round(2) : amount
end

#won?Boolean

Check if the deal is won

Returns:

  • (Boolean)

    True if the deal is won



470
471
472
473
474
# File 'lib/attio/resources/deal.rb', line 470

def won?
  return false unless current_status

  Attio.configuration.won_statuses.include?(current_status)
end

#won_atTime?

Get the timestamp when the deal was won

Returns:

  • (Time, nil)

    The timestamp when deal was won, or nil if not won



486
487
488
489
# File 'lib/attio/resources/deal.rb', line 486

def won_at
  return nil unless won?
  status_changed_at
end