hcp 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d1ffc074c8306b8bf82153790c5e205a5c3ee758606baac77e16fa79097a4f8
4
- data.tar.gz: 8414015f07d86c48e63fbeca9afc743462377618519314bff6f342528f9e42dd
3
+ metadata.gz: 6efa69ce3e07caf79d11e344137caf623856a6c4ea62bdda98730d852df80e5e
4
+ data.tar.gz: 10296089173df4f01acca8945fe39cdff3fdaa28f789730fe402b3322624a338
5
5
  SHA512:
6
- metadata.gz: f4b405e43222fc579c7fb54d33bd791270ed9eb283641fe26b59dd1ccfa2b269b3d3adac3f3e51ebb81cef1f0e36da3dd7492c9a06d152e7ee5568eaed64029b
7
- data.tar.gz: d6e498e41fe664170557f5a802a94b64fd84391609f6f06c6694663347bc1d5a5e6bfd65f0da9c4ef8b9582a1dace4a92e3983787741589381aaaf757c937fef
6
+ metadata.gz: 82f25858ff585025ee7f1efbd8d099114b18a07f2cac3fc8f2f20d4967b8fe46d68f61d5b922d7ebb80146645fb6d47aa4788df96feae706c8310e99c8992098
7
+ data.tar.gz: 0333e25614ba5be270978f6b5022ca27e2e4b1ead840e4804aa73b1287f57b5cbc8ee25bf8aa065061abb5e194c069ec62751e9ce281c8c665d9857bcc2578c4
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --readme README.md
2
+ --markup markdown
3
+ --no-private
4
+ lib/hcp/resource.rb
5
+ lib/**/*.rb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.3.0] - 2026-08-25
4
+
5
+ - [Feature] Read customers, estimates, jobs and job appointments as an Active Record relation:
6
+ `Hcp::Job.all`, `.where`, `.order`, `.limit`, `.includes`, `.find`, `.count` and `.first`,
7
+ walked lazily a page at a time.
8
+ - [Feature] Set the API key once, with `Hcp.key` or `HCP_KEY`, and pass `company_id:` per call.
9
+ - [Feature] Narrow a list by a range — `where(scheduled_at: ..2.days.ago)` — and refuse a
10
+ condition, an order or an expansion Housecall Pro does not take, which it answers by
11
+ ignoring rather than by refusing.
12
+ - [Feature] Raise `Hcp::NotFound` where Housecall Pro has no such record, and
13
+ `Hcp::TooManyRequests`, carrying `reset_at`, where it refuses one for rate. Both descend
14
+ from `Hcp::Error`, so an existing rescue still catches them.
15
+ - [Breaking change] `Hcp::Lead#lead_for`, `#customer_for` and `#uri` are now private, and
16
+ neither `Hcp::Lead` nor `Hcp::Lead::Pipeline` inherits from `Hcp::Resource`, which is now a
17
+ record rather than a holder of credentials. Nothing documented ever called them.
18
+
19
+ ## [1.2.4] - 2026-06-17
20
+
21
+ - [Fix] Set event.type when params is nil
22
+
1
23
  ## [1.2.3] - 2026-06-15
2
24
 
3
25
  - [New] Parse estimate_id for :estimate_sent event
data/README.md CHANGED
@@ -1,20 +1,133 @@
1
1
  # The Housecall Pro API Ruby client
2
2
 
3
- ## Available methods
3
+ Every method in this gem is one Housecall Pro endpoint. Where Housecall Pro has no endpoint,
4
+ this gem has no method — so a resource answers `update` only where Housecall Pro takes a `PUT`,
5
+ and a job, which it takes none for, answers none.
4
6
 
5
- Initialize and create a Lead:
7
+ This release reads. Writing — beyond the leads that were here before it — comes next.
8
+
9
+ ## How to install
10
+
11
+ ```sh
12
+ gem install hcp
13
+ ```
14
+
15
+ Or, in a Gemfile, pinned to the current major:
6
16
 
7
17
  ```ruby
8
- lead = Hcp::Lead.new key:, company_id:
9
- lead.create name: 'Alice', phone: '8008008000', email: 'alice@example.com',
10
- address: { street: '7111 Melrose Ave', city: 'Los Angeles', state: 'CA', zip: '90046' },
11
- note: 'Very interested in buying', source: 'The Lead Generator'
12
- ````
18
+ gem 'hcp', '~> 1.3'
19
+ ```
20
+
21
+ `~> major.minor` means `bundle update` never crosses a breaking change.
22
+
23
+ ## The key, and the location
24
+
25
+ Set the key once. Left unset, the gem reads `HCP_KEY` from the environment:
26
+
27
+ ```ruby
28
+ Hcp.key = ENV['HCP_KEY']
29
+ ```
30
+
31
+ An account with more than one location passes `company_id:` per call, since one process may
32
+ serve several:
33
+
34
+ ```ruby
35
+ Hcp::Job.find id, company_id: company_id
36
+ Hcp::Job.all(company_id: company_id).where(work_status: :scheduled)
37
+ ```
38
+
39
+ ## Reading
40
+
41
+ A list is walked lazily, a page read only once the one before it runs out, so asking for three
42
+ records costs one request rather than all of them:
43
+
44
+ ```ruby
45
+ Hcp::Job.all
46
+ Hcp::Job.find id
47
+ Hcp::Job.limit 50
48
+ Hcp::Job.order updated_at: :desc
49
+ Hcp::Job.where scheduled_at: ..2.days.ago
50
+ Hcp::Job.where(customer_id: id).order(created_at: :asc).limit 10
51
+ Hcp::Job.all.count
52
+ ```
53
+
54
+ A condition written as a range is sent as the two ends Housecall Pro takes, and an end left
55
+ open is left out:
56
+
57
+ ```ruby
58
+ Hcp::Job.where scheduled_at: 2.days.ago.. # scheduled_start_min
59
+ Hcp::Job.where scheduled_at: ..2.days.ago # scheduled_start_max
60
+ Hcp::Job.where scheduled_at: 1.week.ago..Time.now
61
+ ```
62
+
63
+ Housecall Pro narrows a list by one set of words and answers with another, so this gem speaks
64
+ the narrower's words on both sides: `where(work_status: :in_progress)` and `job.work_status`
65
+ both say `:in_progress`.
66
+
67
+ Nothing comes back unasked. `includes` asks for what Housecall Pro otherwise leaves out:
13
68
 
14
- Change the status of a lead in the pipeline:
69
+ ```ruby
70
+ Hcp::Job.includes(:appointments).limit 20
71
+ ```
72
+
73
+ A condition, an order or an expansion Housecall Pro does not take is refused here rather than
74
+ sent — it answers an unknown condition by ignoring it and handing back the whole account:
75
+
76
+ ```ruby
77
+ Hcp::Job.where bogus: 1
78
+ # => Hcp::Error: bogus is not one of: scheduled_at, ends_at, customer_id, ...
79
+ ```
80
+
81
+ ### What each resource reads
82
+
83
+ ```ruby
84
+ job = Hcp::Job.find id
85
+ job.description, job.work_status, job.total_amount, job.invoice_number
86
+ job.customer.name, job.address.city, job.assigned_employees, job.notes, job.tags
87
+ job.schedule.starts_at, job.schedule.time_zone, job.completed_at
88
+ job.appointments, job.line_items, job.invoices
89
+
90
+ customer = Hcp::Customer.where(q: 'Ada').first
91
+ customer.name, customer.email, customer.phone, customer.kind, customer.addresses
92
+
93
+ estimate = Hcp::Estimate.find id
94
+ estimate.estimate_number, estimate.options.map(&:total_amount)
95
+ ```
96
+
97
+ Housecall Pro counts money in cents; this gem reads it in dollars, as a `BigDecimal`.
98
+
99
+ ## Errors
100
+
101
+ Everything descends from `Hcp::Error`, so one rescue still catches the lot.
102
+
103
+ ```ruby
104
+ Hcp::NotFound # Housecall Pro has no record under that ID
105
+ Hcp::TooManyRequests # refused for rate; #reset_at says when it lifts
106
+ ```
107
+
108
+ Nothing here sleeps. A caller told to come back later has a queue that can bring the whole job
109
+ back, which is worth more than a worker asleep holding a connection open.
110
+
111
+ ## Leads
112
+
113
+ Opening a lead, and moving one through the pipeline, are unchanged:
15
114
 
16
115
  ```ruby
116
+ lead = Hcp::Lead.new key:, company_id:
117
+ lead.create name: 'Ada', phone: '5550000001', email: 'ada@example.com',
118
+ address: { street: '1 Example Street', city: 'Springfield', state: 'CA', zip: '90210' },
119
+ note: 'Very interested in buying', source: 'The Lead Generator'
120
+
17
121
  pipeline = Hcp::Lead::Pipeline.new id:, key:, company_id:
18
122
  pipeline.update status_name: 'Won'
19
123
  ```
20
124
 
125
+ ## Webhooks
126
+
127
+ `Hcp::Event` reads a webhook payload, and reaches the network for nothing:
128
+
129
+ ```ruby
130
+ event = Hcp::Event.new params
131
+ event.type # :job_scheduled
132
+ event.job_id, event.customer_id, event.scheduled_at, event.invoice_amount
133
+ ```
data/lib/hcp/answer.rb ADDED
@@ -0,0 +1,35 @@
1
+ module Hcp
2
+ # What Housecall Pro answered: the body it carries, or the refusal it stands for.
3
+ class Answer
4
+ # The header Housecall Pro names the epoch a rate limit lifts at.
5
+ RESET_HEADER = 'RateLimit-Reset'
6
+
7
+ # @param response [Net::HTTPResponse] what Housecall Pro sent back.
8
+ def initialize(response)
9
+ @response = response
10
+ end
11
+
12
+ # @return [Hash] the record or page Housecall Pro answered with.
13
+ def body
14
+ case @response
15
+ when Net::HTTPSuccess then parsed
16
+ when Net::HTTPNotFound then raise NotFound, message
17
+ when Net::HTTPTooManyRequests then raise TooManyRequests.new(message, reset_at)
18
+ else raise Error, message
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # Housecall Pro writes a refusal three ways: the message under an `error`, the message as
25
+ # the `error`, and the message on its own.
26
+ def message
27
+ error = parsed['error'] || parsed['message']
28
+ error.is_a?(Hash) ? error['message'] : error
29
+ end
30
+
31
+ def reset_at = (Time.at Integer(@response[RESET_HEADER]) if @response[RESET_HEADER])
32
+
33
+ def parsed = @parsed ||= JSON(@response.body)
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ module Hcp
2
+ # Extends a list with the chaining that narrows, orders and cuts it.
3
+ module Chainable
4
+ # A condition Housecall Pro does not take is one it ignores, answering the whole account
5
+ # rather than a page of it, so an unknown one is refused here where it can still be named.
6
+ # @return [Relation] the same list, narrowed by these conditions.
7
+ def where(**conditions)
8
+ only conditions.keys, @type.filters.keys
9
+ narrowed conditions: @conditions.merge(conditions)
10
+ end
11
+
12
+ # @return [Relation] the same list, in this order.
13
+ def order(**sorts)
14
+ only sorts.keys, @type.sorts
15
+ narrowed sorts: sorts
16
+ end
17
+
18
+ # @return [Relation] the same list, stopping after this many records.
19
+ def limit(count) = narrowed limit: count
20
+
21
+ # @return [Relation] the same list, asking Housecall Pro for these beside each record.
22
+ def includes(*names)
23
+ only names, @type.expands
24
+ narrowed expands: @expands | names
25
+ end
26
+
27
+ private
28
+
29
+ def only(names, allowed)
30
+ unknown = names - allowed
31
+ raise Error, "#{unknown.first} is not one of: #{allowed.join ', '}" if unknown.any?
32
+ end
33
+
34
+ def narrowed(conditions: @conditions, sorts: @sorts, limit: @limit, expands: @expands)
35
+ Relation.new type: @type, path: @path, company_id: @company_id, conditions: conditions,
36
+ sorts: sorts, limit: limit, expands: expands
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,14 @@
1
+ module Hcp
2
+ # Extends a class handed its own key, as everything was before the key became global.
3
+ module Keyed
4
+ private
5
+
6
+ def headers
7
+ {
8
+ 'Authorization' => "Token #{@key}",
9
+ 'Content-Type' => 'application/json',
10
+ 'X-Company-Id' => @company_id,
11
+ }.compact
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ module Hcp
2
+ # Extends a record Housecall Pro holds a person's names for.
3
+ module Named
4
+ # @return [String] what they are called, by whichever of their names Housecall Pro holds.
5
+ def name = [ @node['first_name'], @node['last_name'] ].compact_blank.join ' '
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ module Hcp
2
+ # Extends a resource with the entry points that read a list of it, or one record.
3
+ module Queryable
4
+ # @param company_id [String, nil] the location to read as, where the account has several.
5
+ # @return [Relation] every record of this kind the location holds.
6
+ def all(company_id: nil) = Relation.new type: self, company_id: company_id
7
+
8
+ # @return [Relation] the records matching these conditions.
9
+ def where(company_id: nil, **conditions) = all(company_id: company_id).where(**conditions)
10
+
11
+ # @return [Relation] the records in this order.
12
+ def order(company_id: nil, **sorts) = all(company_id: company_id).order(**sorts)
13
+
14
+ # @return [Relation] at most this many records.
15
+ def limit(count, company_id: nil) = all(company_id: company_id).limit(count)
16
+
17
+ # @return [Relation] the records, with these brought back beside each of them.
18
+ def includes(*names, company_id: nil) = all(company_id: company_id).includes(*names)
19
+
20
+ # @return [Integer] how many records of this kind the location holds.
21
+ def count(company_id: nil) = all(company_id: company_id).count
22
+
23
+ # @return [Resource, nil] the first record, or nil where the location holds none.
24
+ def first(company_id: nil) = all(company_id: company_id).first
25
+
26
+ # @return [Resource] the record Housecall Pro files under this ID.
27
+ def find(id, company_id: nil) = all(company_id: company_id).find(id)
28
+
29
+ # @return [Hash] what a list of these may be narrowed by.
30
+ def filters = self::FILTERS
31
+
32
+ # @return [Array<Symbol>] the conditions Housecall Pro takes more than one of.
33
+ def many = self::MANY
34
+
35
+ # @return [Array<Symbol>] what Housecall Pro will put a list of these in order of.
36
+ def sorts = self::SORTS
37
+
38
+ # @return [Array<Symbol>] what these bring back beside themselves where asked to.
39
+ def expands = self::EXPANDS
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ module Hcp
2
+ # Extends a record Housecall Pro books work for and stamps as the work happens.
3
+ module Scheduled
4
+ # @return [Schedule, nil] when the work is booked for.
5
+ def schedule = record Schedule, 'schedule'
6
+
7
+ # @return [Time, nil] when the pro said they were on their way.
8
+ def on_my_way_at = time 'work_timestamps', 'on_my_way_at'
9
+
10
+ # @return [Time, nil] when the work started.
11
+ def started_at = time 'work_timestamps', 'started_at'
12
+
13
+ # @return [Time, nil] when the work was finished.
14
+ def completed_at = time 'work_timestamps', 'completed_at'
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ module Hcp
2
+ # Extends a record Housecall Pro moves through a workflow.
3
+ module Statused
4
+ # Housecall Pro narrows a list by one set of words and answers with another, so a caller
5
+ # who filters by :in_progress reads :in_progress back rather than 'in progress'.
6
+ STATUSES = {
7
+ 'needs scheduling' => :unscheduled,
8
+ 'scheduled' => :scheduled,
9
+ 'in progress' => :in_progress,
10
+ 'complete rated' => :completed,
11
+ 'complete unrated' => :completed,
12
+ 'user canceled' => :canceled,
13
+ 'pro canceled' => :canceled,
14
+ }
15
+
16
+ # @return [Symbol, nil] where Housecall Pro files the record in its own workflow.
17
+ def work_status = STATUSES[@node['work_status']]
18
+
19
+ # A rating is not a status, so it reads beside one rather than inside it.
20
+ # @return [Boolean] whether the customer rated the work.
21
+ def rated? = @node['work_status'] == 'complete rated'
22
+ end
23
+ end
@@ -0,0 +1,10 @@
1
+ module Hcp
2
+ # Extends a record Housecall Pro stamps with when it was opened and last changed.
3
+ module Timestamped
4
+ # @return [Time, nil] when the record was opened.
5
+ def created_at = time 'created_at'
6
+
7
+ # @return [Time, nil] when the record last changed.
8
+ def updated_at = time 'updated_at'
9
+ end
10
+ end
data/lib/hcp/error.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  module Hcp
2
+ # Raised where Housecall Pro would not answer.
2
3
  Error = Class.new StandardError
3
4
  end
@@ -0,0 +1,4 @@
1
+ module Hcp
2
+ # Raised where Housecall Pro has no record under the ID it was asked for.
3
+ NotFound = Class.new Error
4
+ end
@@ -0,0 +1,16 @@
1
+ module Hcp
2
+ # Raised where Housecall Pro refuses a request for rate.
3
+ class TooManyRequests < Error
4
+ # @param message [String] what Housecall Pro said.
5
+ # @param reset_at [Time, nil] when it will answer again.
6
+ def initialize(message, reset_at)
7
+ super message
8
+ @reset_at = reset_at
9
+ end
10
+
11
+ # Nothing here sleeps: a caller with a queue can bring the whole job back, which is worth
12
+ # more than a worker asleep holding a connection open.
13
+ # @return [Time, nil] when the limit lifts, where Housecall Pro said.
14
+ attr_reader :reset_at
15
+ end
16
+ end
data/lib/hcp/event.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  module Hcp
2
+ # What Housecall Pro sent a webhook to say, read without reaching the network.
2
3
  class Event
3
4
  # @see https://docs.housecallpro.com/docs/housecall-public-api/46e9e1be07621-webhooks
4
5
  SIGNATURE_HEADER = 'Api-Signature'
@@ -12,7 +13,7 @@ module Hcp
12
13
  end
13
14
 
14
15
  # @return [Symbol] the type of event, e.g.: :lead_converted, :job_created, :invoice_sent.
15
- def type = @params[:event].gsub('.', '_').to_sym
16
+ def type = @params.fetch(:event, '').gsub('.', '_').to_sym
16
17
 
17
18
  # @return [String] unique identifier of the lead in a :lead_converted event.
18
19
  def lead_id = @params.dig :lead, :id
data/lib/hcp/filter.rb ADDED
@@ -0,0 +1,36 @@
1
+ module Hcp
2
+ # One condition a list is narrowed by, as the parameters Housecall Pro takes it as.
3
+ class Filter
4
+ # @param bounds [Array<Symbol>] the parameter for each end of a range, or the single one.
5
+ # @param value [Object] a range, or the one value to match.
6
+ def initialize(bounds:, value:)
7
+ @bounds = bounds
8
+ @value = value
9
+ end
10
+
11
+ # @return [Hash] the parameters to send, without the end the caller left open.
12
+ def params
13
+ return { @bounds.first => @value } unless @value.is_a? Range
14
+
15
+ refuse
16
+ low, high = @bounds
17
+ { low => stamp(@value.begin), high => stamp(@value.end) }.compact
18
+ end
19
+
20
+ private
21
+
22
+ # Housecall Pro reads both ends inclusively, so an excluded end would come back anyway.
23
+ def refuse
24
+ raise Error, "#{@bounds.first} takes one value, not a range" if @bounds.one?
25
+ raise Error, "#{@bounds.last} cannot exclude its end" if @value.exclude_end? && @value.end
26
+ end
27
+
28
+ def stamp(value)
29
+ case value
30
+ when Time then value.utc.iso8601
31
+ when Date then value.iso8601
32
+ else value
33
+ end
34
+ end
35
+ end
36
+ end
data/lib/hcp/key.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Hcp
2
+ class << self
3
+ # The Housecall Pro API key every request is read with.
4
+ attr_writer :key
5
+
6
+ # @return [String, nil] the key this module was given, or the one the environment carries.
7
+ def key = @key || ENV['HCP_KEY']
8
+ end
9
+ end
@@ -1,15 +1,18 @@
1
1
  module Hcp
2
- class Lead::Pipeline < Resource
2
+ # Where a Housecall Pro user files a lead while they are still chasing it.
3
+ class Lead::Pipeline
4
+ include Keyed
5
+
3
6
  def initialize(id: nil, key:, company_id:)
4
7
  @id = id
5
8
  @key = key
6
9
  @company_id = company_id
7
10
  end
8
11
 
12
+ # Moves the lead to the status going by this name.
13
+ # @param status_name [String] the status as the account names it, such as 'Won'.
9
14
  def update(status_name:)
10
15
  status = find_status_by name: status_name
11
- raise Error, "Status #{status_name} not found for lead #{@id}" unless status
12
-
13
16
  payload = { resource_type: 'lead', resource_id: @id, status_id: status['id'] }.to_json
14
17
  response = Net::HTTP.put uri, payload, headers
15
18
  raise Error, response.body unless response.is_a? Net::HTTPSuccess
data/lib/hcp/lead.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  module Hcp
2
- class Lead < Resource
3
- attr_reader :customer_id
2
+ # Somebody who asked a Housecall Pro user for work, before there is a job.
3
+ class Lead
4
+ include Keyed
5
+
6
+ attr_reader :id, :customer_id
4
7
 
5
8
  def initialize(id: nil, customer_id: nil, key:, company_id:)
6
9
  @id = id
@@ -9,6 +12,8 @@ module Hcp
9
12
  @customer_id = customer_id
10
13
  end
11
14
 
15
+ # Opens the lead on Housecall Pro, and keeps what it filed the lead and customer under.
16
+ # @param params [Hash] :name, :email, :phone, :address, :note and :source.
12
17
  def create(params = {})
13
18
  response = Net::HTTP.post uri, lead_for(params).to_json, headers
14
19
  raise Error, response.body unless response.is_a? Net::HTTPSuccess
@@ -18,10 +23,12 @@ module Hcp
18
23
  raise Error, error
19
24
  end
20
25
 
26
+ private
27
+
21
28
  def lead_for(params = {})
22
29
  {
23
30
  customer: customer_for(params), address: params[:address],
24
- lead_source: params[:source], note: params[:note]
31
+ lead_source: params[:source], note: params[:note],
25
32
  }.compact_blank
26
33
  end
27
34
 
@@ -0,0 +1,76 @@
1
+ module Hcp
2
+ # Every record of one kind a Housecall Pro account holds, walked a page at a time.
3
+ class Relation
4
+ include Chainable, Enumerable
5
+
6
+ # Records a page: the most Housecall Pro answers with, and more than it refuses.
7
+ PAGE = 200
8
+
9
+ # @param type [Class] what each record is read as.
10
+ # @param path [String] where Housecall Pro keeps them, under the host.
11
+ # @param company_id [String, nil] the location to read them as.
12
+ def initialize(type:, path: nil, company_id: nil, conditions: {}, sorts: {}, limit: nil,
13
+ expands: [])
14
+ @type = type
15
+ @path = path || type.path
16
+ @company_id = company_id
17
+ @conditions = conditions
18
+ @sorts = sorts
19
+ @limit = limit
20
+ @expands = expands
21
+ end
22
+
23
+ # Nothing is read until the walk starts, and a page only once the one before it runs out.
24
+ # @return [Enumerator, Relation] every record the list holds, or the list once walked.
25
+ def each(&block)
26
+ return walk unless block
27
+
28
+ walk.each(&block)
29
+ self
30
+ end
31
+
32
+ # Shadows Enumerable#find the way Active Record does: a record is reached by the ID
33
+ # Housecall Pro files it under, not by asking every record whether it is the one.
34
+ # @param id [String] the Housecall Pro ID.
35
+ # @return [Resource] the record, raising Hcp::NotFound where there is none.
36
+ def find(id) = record_for read("#{@path}/#{id}")
37
+
38
+ # A page of one carries the total beside it, so this is one small read however long the
39
+ # list is.
40
+ # @return [Integer] how many records the list holds.
41
+ def count = @count ||= [ read(@path, page_size: 1)['total_items'], @limit ].compact.min
42
+
43
+ private
44
+
45
+ def walk
46
+ Enumerator.new do |yielder|
47
+ seen = 0
48
+ (1..).each do |page|
49
+ body = read @path, page: page, page_size: [ @limit, PAGE ].compact.min
50
+ body.fetch(@type.key).each do |node|
51
+ yielder << record_for(node)
52
+ break if (seen += 1) == @limit
53
+ end
54
+ break if seen == @limit || page >= body.fetch('total_pages', page)
55
+ end
56
+ end
57
+ end
58
+
59
+ def read(path, paging = {})
60
+ Request.new(path: path, params: params.merge(paging), company_id: @company_id).body
61
+ end
62
+
63
+ def params
64
+ @conditions.flat_map { |name, value| filter(name, value).to_a }.to_h.
65
+ merge sort_by: @sorts.keys.first, sort_direction: @sorts.values.first,
66
+ expand: @expands.presence
67
+ end
68
+
69
+ def filter(name, value)
70
+ bounds = @type.filters.fetch name
71
+ Filter.new(bounds: bounds, value: @type.many.include?(name) ? Array(value) : value).params
72
+ end
73
+
74
+ def record_for(node) = @type.new node: node, company_id: @company_id
75
+ end
76
+ end
@@ -0,0 +1,31 @@
1
+ module Hcp
2
+ # One read from Housecall Pro.
3
+ class Request
4
+ # Where Housecall Pro answers.
5
+ HOST = 'https://api.housecallpro.com'
6
+
7
+ # @param path [String] what to read, under the host.
8
+ # @param params [Hash] the query to read it with.
9
+ # @param company_id [String, nil] the location to read it as.
10
+ def initialize(path:, params: {}, company_id: nil)
11
+ @path = path
12
+ @params = params
13
+ @company_id = company_id
14
+ end
15
+
16
+ # @return [Hash] what Housecall Pro answered.
17
+ def body = Answer.new(Net::HTTP.get_response(uri, headers)).body
18
+
19
+ private
20
+
21
+ def uri = URI [ "#{HOST}/#{@path}", @params.compact.to_query ].compact_blank.join '?'
22
+
23
+ def headers
24
+ {
25
+ 'Authorization' => "Token #{Hcp.key}",
26
+ 'Content-Type' => 'application/json',
27
+ 'X-Company-Id' => @company_id,
28
+ }.compact
29
+ end
30
+ end
31
+ end
data/lib/hcp/resource.rb CHANGED
@@ -1,15 +1,58 @@
1
1
  module Hcp
2
+ # A record as Housecall Pro answered it.
2
3
  class Resource
3
- attr_reader :id
4
+ class << self
5
+ # Declares a reader for what Housecall Pro answers under a name.
6
+ # @!macro [attach] attribute
7
+ # @!method $1
8
+ # @return [String, nil] the $1, as Housecall Pro answers it.
9
+ def attribute(name, key = name) = define_method(name) { @node[key.to_s] }
10
+
11
+ # Declares a reader for a moment Housecall Pro stamps.
12
+ # @!macro [attach] timestamp
13
+ # @!method $1
14
+ # @return [Time, nil] the moment Housecall Pro answers as $1.
15
+ def timestamp(name, key = name) = define_method(name) { time key.to_s }
16
+
17
+ # Declares a reader for a sum Housecall Pro counts in cents.
18
+ # @!macro [attach] amount
19
+ # @!method $1
20
+ # @return [BigDecimal, nil] the $1, which Housecall Pro counts in cents.
21
+ def amount(name, key = name) = define_method(name) { money key.to_s }
22
+ end
23
+
24
+ # @param node [Hash] the record as Housecall Pro answered it.
25
+ # @param company_id [String, nil] the location it was read as.
26
+ def initialize(node: {}, company_id: nil)
27
+ @node = node
28
+ @company_id = company_id
29
+ end
30
+
31
+ # @return [String] the ID Housecall Pro files the record under.
32
+ def id = @node['id']
4
33
 
5
34
  private
6
35
 
7
- def headers
8
- {
9
- 'Authorization' => "Token #{@key}",
10
- 'Content-Type' => 'application/json',
11
- 'X-Company-Id' => @company_id,
12
- }.compact
36
+ # Built from the record's own path rather than its class, so a nested collection sits
37
+ # under its parent however deep Housecall Pro put it.
38
+ def nested(type, segment)
39
+ Relation.new type: type, path: "#{path}/#{segment}", company_id: @company_id
40
+ end
41
+
42
+ def path = "#{self.class.path}/#{id}"
43
+
44
+ def time(*keys)
45
+ value = @node.dig(*keys)
46
+ Time.iso8601 value if value.present?
47
+ end
48
+
49
+ # Housecall Pro counts money in cents, and a caller reads it in dollars.
50
+ def money(key) = (BigDecimal(@node[key].to_s) / 100 if @node[key])
51
+
52
+ def record(type, key) = (type.new node: @node[key], company_id: @company_id if @node[key])
53
+
54
+ def records(type, key)
55
+ Array(@node[key]).map { |node| type.new node: node, company_id: @company_id }
13
56
  end
14
57
  end
15
58
  end
@@ -0,0 +1,23 @@
1
+ module Hcp
2
+ # Where a customer is billed, or where the work happens.
3
+ class Address < Resource
4
+ # What Housecall Pro calls a page of a customer's addresses.
5
+ def self.key = 'addresses'
6
+
7
+ attribute :street
8
+ attribute :street_line_2
9
+ attribute :city
10
+ attribute :state
11
+ attribute :zip
12
+ attribute :country
13
+
14
+ # @return [Float, nil] how far north the address is, where Housecall Pro placed it.
15
+ attribute :latitude
16
+
17
+ # @return [Float, nil] how far east the address is, where Housecall Pro placed it.
18
+ attribute :longitude
19
+
20
+ # @return [Symbol, nil] :billing or :service.
21
+ def type = @node['type']&.to_sym
22
+ end
23
+ end
@@ -0,0 +1,52 @@
1
+ module Hcp
2
+ # Whoever the work is for.
3
+ class Customer < Resource
4
+ extend Queryable
5
+ include Named, Timestamped
6
+
7
+ # A customer is narrowed by one free-text search rather than by named fields.
8
+ FILTERS = { q: %i[q], location_ids: %i[location_ids] }
9
+
10
+ # The conditions Housecall Pro takes more than one of, and refuses one of.
11
+ MANY = %i[location_ids]
12
+
13
+ # What Housecall Pro will put a list of customers in order of.
14
+ SORTS = %i[created_at updated_at]
15
+
16
+ # What a customer brings back beside themselves where they are asked to.
17
+ EXPANDS = %i[attachments do_not_service]
18
+
19
+ # Where Housecall Pro keeps them.
20
+ def self.path = 'customers'
21
+
22
+ # What Housecall Pro calls a page of them.
23
+ def self.key = 'customers'
24
+
25
+ attribute :first_name
26
+ attribute :last_name
27
+ attribute :email
28
+
29
+ # @return [String, nil] the business the customer is, where they are one.
30
+ attribute :company
31
+
32
+ # @return [String, nil] where the customer came from.
33
+ attribute :lead_source
34
+
35
+
36
+ # Housecall Pro holds three numbers, and a caller wants whichever one there is.
37
+ # @return [String, nil] the number the customer is reached on first.
38
+ def phone = @node['mobile_number'] || @node['home_number'] || @node['work_number']
39
+
40
+ # @return [Symbol, nil] :homeowner, or whatever else Housecall Pro takes the customer for.
41
+ def kind = @node['kind']&.to_sym
42
+
43
+ # @return [Boolean] whether the customer agreed to hear from the pro.
44
+ def notifications_enabled? = @node['notifications_enabled']
45
+
46
+ # @return [Array<String>] what the customer is tagged with.
47
+ def tags = Array(@node['tags'])
48
+
49
+ # @return [Relation] every address the customer is billed or served at.
50
+ def addresses = nested Address, 'addresses'
51
+ end
52
+ end
@@ -0,0 +1,12 @@
1
+ module Hcp
2
+ # A pro on a Housecall Pro account.
3
+ class Employee < Resource
4
+ include Named, Timestamped
5
+
6
+ attribute :email
7
+ attribute :role
8
+
9
+ # @return [String, nil] the number the pro is reached on.
10
+ attribute :phone, :mobile_number
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ module Hcp
2
+ # One of the things a customer was offered on an estimate, priced on its own.
3
+ class Estimate::Option < Resource
4
+ include Timestamped
5
+
6
+ attribute :name
7
+ attribute :option_number
8
+
9
+ # @return [String, nil] whether the customer has answered, and how.
10
+ attribute :approval_status
11
+
12
+ # @return [String, nil] where Housecall Pro files the option in its own workflow.
13
+ attribute :status
14
+
15
+ # @return [String, nil] what the pro said when they sent it.
16
+ attribute :message_from_pro
17
+
18
+ amount :total_amount
19
+
20
+ # @return [Array<String>] what the option is tagged with.
21
+ def tags = Array(@node['tags'])
22
+
23
+ # @return [Array<Note>] what the pros wrote on the option.
24
+ def notes = records Note, 'notes'
25
+ end
26
+ end
@@ -0,0 +1,49 @@
1
+ module Hcp
2
+ # Work a Housecall Pro user offered to do, priced as one option or several.
3
+ class Estimate < Resource
4
+ extend Queryable
5
+ include Scheduled, Statused, Timestamped
6
+
7
+ # What a list of estimates may be narrowed by, against the parameters Housecall Pro takes.
8
+ FILTERS = {
9
+ scheduled_at: %i[scheduled_start_min scheduled_start_max],
10
+ ends_at: %i[scheduled_end_min scheduled_end_max],
11
+ customer_id: %i[customer_id],
12
+ employee_ids: %i[employee_ids],
13
+ work_status: %i[work_status],
14
+ location_ids: %i[location_ids],
15
+ }
16
+
17
+ # The conditions Housecall Pro takes more than one of, and refuses one of.
18
+ MANY = %i[employee_ids work_status location_ids]
19
+
20
+ # What Housecall Pro will put a list of estimates in order of.
21
+ SORTS = %i[created_at updated_at id]
22
+
23
+ # What an estimate brings back beside itself where it is asked to.
24
+ EXPANDS = %i[attachments]
25
+
26
+ # Where Housecall Pro keeps them.
27
+ def self.path = 'estimates'
28
+
29
+ # What Housecall Pro calls a page of them.
30
+ def self.key = 'estimates'
31
+
32
+ attribute :estimate_number
33
+
34
+ # @return [String, nil] where the work came from.
35
+ attribute :lead_source
36
+
37
+ # @return [Customer, nil] whose estimate it is.
38
+ def customer = record Customer, 'customer'
39
+
40
+ # @return [Address, nil] where the work would happen.
41
+ def address = record Address, 'address'
42
+
43
+ # @return [Array<Employee>] the pros the estimate is assigned to.
44
+ def assigned_employees = records Employee, 'assigned_employees'
45
+
46
+ # @return [Array<Estimate::Option>] what the customer was offered, priced.
47
+ def options = records Estimate::Option, 'options'
48
+ end
49
+ end
@@ -0,0 +1,22 @@
1
+ module Hcp
2
+ # One visit a job is booked for, where the work takes more than a single trip.
3
+ class Job::Appointment < Resource
4
+ # What Housecall Pro calls a list of them.
5
+ def self.key = 'appointments'
6
+
7
+ # @return [Time, nil] when the visit is booked to start.
8
+ timestamp :starts_at, :start_time
9
+
10
+ # @return [Time, nil] when the visit is booked to end.
11
+ timestamp :ends_at, :end_time
12
+
13
+ # @return [Integer, nil] how many minutes wide the arrival window is.
14
+ attribute :arrival_window, :arrival_window_minutes
15
+
16
+ # @return [Boolean] whether the visit is booked for a day rather than for a time.
17
+ def anytime? = @node['anytime']
18
+
19
+ # @return [Array<String>] the IDs of the pros sent on the visit.
20
+ def dispatched_employee_ids = Array(@node['dispatched_employees_ids'])
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ module Hcp
2
+ # A bill raised for a job.
3
+ class Job::Invoice < Resource
4
+ # What Housecall Pro calls a list of them.
5
+ def self.key = 'invoices'
6
+
7
+ attribute :invoice_number
8
+
9
+ # @return [String, nil] where Housecall Pro files the invoice in its own workflow.
10
+ attribute :status
11
+
12
+ amount :amount
13
+ amount :due_amount
14
+
15
+ # @return [Time, nil] when the invoice was sent.
16
+ timestamp :sent_at
17
+
18
+ # @return [Time, nil] when the invoice was paid.
19
+ timestamp :paid_at
20
+ end
21
+ end
@@ -0,0 +1,73 @@
1
+ module Hcp
2
+ # Work a Housecall Pro user accepted: what it is, who it is for, and what it comes to.
3
+ class Job < Resource
4
+ extend Queryable
5
+ include Scheduled, Statused, Timestamped
6
+
7
+ # What a list of jobs may be narrowed by, against the parameters Housecall Pro takes.
8
+ FILTERS = {
9
+ scheduled_at: %i[scheduled_start_min scheduled_start_max],
10
+ ends_at: %i[scheduled_end_min scheduled_end_max],
11
+ customer_id: %i[customer_id],
12
+ employee_ids: %i[employee_ids],
13
+ work_status: %i[work_status],
14
+ location_ids: %i[location_ids],
15
+ }
16
+
17
+ # The conditions Housecall Pro takes more than one of, and refuses one of.
18
+ MANY = %i[employee_ids work_status location_ids]
19
+
20
+ # What Housecall Pro will put a list of jobs in order of.
21
+ SORTS = %i[created_at updated_at invoice_number id description work_status]
22
+
23
+ # What a job brings back beside itself where it is asked to.
24
+ EXPANDS = %i[attachments appointments]
25
+
26
+ # Where Housecall Pro keeps them.
27
+ def self.path = 'jobs'
28
+
29
+ # What Housecall Pro calls a page of them.
30
+ def self.key = 'jobs'
31
+
32
+ attribute :invoice_number
33
+ attribute :description
34
+ attribute :lead_source
35
+
36
+ # @return [String, nil] the ID of the estimate the job was won with.
37
+ attribute :original_estimate_id
38
+
39
+ amount :subtotal
40
+ amount :total_amount
41
+ amount :outstanding_balance
42
+
43
+ # @return [Time, nil] when the job was locked against further changes.
44
+ timestamp :locked_at
45
+
46
+ # @return [Time, nil] when the customer canceled the job.
47
+ timestamp :canceled_at
48
+
49
+ # @return [Array<String>] what the job is tagged with.
50
+ def tags = Array(@node['tags'])
51
+
52
+ # @return [Customer, nil] whose job it is.
53
+ def customer = record Customer, 'customer'
54
+
55
+ # @return [Address, nil] where the work happens.
56
+ def address = record Address, 'address'
57
+
58
+ # @return [Array<Note>] what the pros wrote on the job.
59
+ def notes = records Note, 'notes'
60
+
61
+ # @return [Array<Employee>] the pros the job is assigned to.
62
+ def assigned_employees = records Employee, 'assigned_employees'
63
+
64
+ # @return [Relation] the visits the job is booked for.
65
+ def appointments = nested Job::Appointment, 'appointments'
66
+
67
+ # @return [Relation] what the job is billed as.
68
+ def line_items = nested LineItem, 'line_items'
69
+
70
+ # @return [Relation] the invoices raised for the job.
71
+ def invoices = nested Job::Invoice, 'invoices'
72
+ end
73
+ end
@@ -0,0 +1,20 @@
1
+ module Hcp
2
+ # One line of what a job or an estimate option comes to.
3
+ class LineItem < Resource
4
+ # Housecall Pro answers a job's line items under `data` rather than under their own name.
5
+ def self.key = 'data'
6
+
7
+ attribute :name
8
+ attribute :description
9
+
10
+ # @return [Float, nil] how many of it, to two decimal places.
11
+ attribute :quantity
12
+
13
+ amount :unit_price
14
+ amount :unit_cost
15
+ amount :amount
16
+
17
+ # @return [Symbol, nil] :materials, :labor, or one of the gratuities and discounts.
18
+ def kind = @node['kind']&.tr(' ', '_')&.to_sym
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ module Hcp
2
+ # Something a pro wrote on a job or on an estimate option.
3
+ class Note < Resource
4
+ attribute :content
5
+ end
6
+ end
@@ -0,0 +1,21 @@
1
+ module Hcp
2
+ # When work is booked for, and how long a window the customer was given.
3
+ class Schedule < Resource
4
+ # @return [Time, nil] when the work is booked to start.
5
+ timestamp :starts_at, :scheduled_start
6
+
7
+ # @return [Time, nil] when the work is booked to end.
8
+ timestamp :ends_at, :scheduled_end
9
+
10
+ # @return [String, nil] the IANA zone to show a customer a time in.
11
+ attribute :time_zone
12
+
13
+ # @return [Integer, nil] how many minutes wide the arrival window is.
14
+ attribute :arrival_window
15
+
16
+ # Housecall Pro leaves these out unless a list was asked to bring them back, so a job read
17
+ # without `includes(:appointments)` has none of them rather than none at all.
18
+ # @return [Array<Job::Appointment>] the visits the work is booked for.
19
+ def appointments = records Job::Appointment, 'appointments'
20
+ end
21
+ end
data/lib/hcp/version.rb CHANGED
@@ -1,7 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
1
  # Ruby client for the Housecall Pro API.
4
2
  module Hcp
5
3
  # Current version of the gem.
6
- VERSION = '1.2.3'
4
+ VERSION = '1.3.0'
7
5
  end
data/lib/hcp.rb CHANGED
@@ -1,13 +1,53 @@
1
- # frozen_string_literal: true
2
-
1
+ require 'bigdecimal'
2
+ require 'date'
3
3
  require 'json'
4
4
  require 'net/http'
5
+ require 'time'
5
6
 
7
+ # Only the Active Support files whose methods are used, rather than the whole of it: a name
8
+ # Housecall Pro holds nothing for arrives as readily empty as null, and its queries carry
9
+ # arrays in the bracketed form `to_query` writes.
6
10
  require 'active_support/core_ext/enumerable'
7
11
  require 'active_support/core_ext/object/blank'
12
+ require 'active_support/core_ext/object/to_query'
8
13
 
14
+ require 'hcp/version'
9
15
  require 'hcp/error'
16
+ require 'hcp/errors/not_found'
17
+ require 'hcp/errors/too_many_requests'
18
+ require 'hcp/key'
19
+
20
+ # Answer before Request, which reads one, and Filter before Relation, which writes them.
21
+ require 'hcp/answer'
22
+ require 'hcp/request'
23
+ require 'hcp/filter'
24
+ require 'hcp/concerns/chainable'
25
+ require 'hcp/relation'
26
+ require 'hcp/concerns/queryable'
10
27
  require 'hcp/resource'
28
+
29
+ # Every concern before the records including it, and Schedule before Scheduled, which reads one.
30
+ require 'hcp/concerns/named'
31
+ require 'hcp/concerns/timestamped'
32
+ require 'hcp/concerns/statused'
33
+ require 'hcp/resources/schedule'
34
+ require 'hcp/concerns/scheduled'
35
+
36
+ require 'hcp/resources/address'
37
+ require 'hcp/resources/employee'
38
+ require 'hcp/resources/line_item'
39
+ require 'hcp/resources/note'
40
+ require 'hcp/resources/customer'
41
+
42
+ # A job before the collections named under it, which its constant has to exist for.
43
+ require 'hcp/resources/job'
44
+ require 'hcp/resources/job/appointment'
45
+ require 'hcp/resources/job/invoice'
46
+
47
+ require 'hcp/resources/estimate'
48
+ require 'hcp/resources/estimate/option'
49
+
50
+ require 'hcp/concerns/keyed'
11
51
  require 'hcp/lead'
12
52
  require 'hcp/lead/pipeline'
13
53
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo
@@ -23,6 +23,90 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop-rails-omakase
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: simplecov
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: webmock
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: yard
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
26
110
  description: Housecall Pro API
27
111
  email:
28
112
  - claudiob@users.noreply.github.com
@@ -30,23 +114,49 @@ executables: []
30
114
  extensions: []
31
115
  extra_rdoc_files: []
32
116
  files:
117
+ - ".yardopts"
33
118
  - CHANGELOG.md
34
119
  - LICENSE.txt
35
120
  - README.md
36
121
  - lib/hcp.rb
122
+ - lib/hcp/answer.rb
123
+ - lib/hcp/concerns/chainable.rb
124
+ - lib/hcp/concerns/keyed.rb
125
+ - lib/hcp/concerns/named.rb
126
+ - lib/hcp/concerns/queryable.rb
127
+ - lib/hcp/concerns/scheduled.rb
128
+ - lib/hcp/concerns/statused.rb
129
+ - lib/hcp/concerns/timestamped.rb
37
130
  - lib/hcp/error.rb
131
+ - lib/hcp/errors/not_found.rb
132
+ - lib/hcp/errors/too_many_requests.rb
38
133
  - lib/hcp/event.rb
134
+ - lib/hcp/filter.rb
135
+ - lib/hcp/key.rb
39
136
  - lib/hcp/lead.rb
40
137
  - lib/hcp/lead/pipeline.rb
138
+ - lib/hcp/relation.rb
139
+ - lib/hcp/request.rb
41
140
  - lib/hcp/resource.rb
141
+ - lib/hcp/resources/address.rb
142
+ - lib/hcp/resources/customer.rb
143
+ - lib/hcp/resources/employee.rb
144
+ - lib/hcp/resources/estimate.rb
145
+ - lib/hcp/resources/estimate/option.rb
146
+ - lib/hcp/resources/job.rb
147
+ - lib/hcp/resources/job/appointment.rb
148
+ - lib/hcp/resources/job/invoice.rb
149
+ - lib/hcp/resources/line_item.rb
150
+ - lib/hcp/resources/note.rb
151
+ - lib/hcp/resources/schedule.rb
42
152
  - lib/hcp/version.rb
43
153
  homepage: https://github.com/claudiob/hcp
44
154
  licenses:
45
155
  - MIT
46
156
  metadata:
47
157
  homepage_uri: https://github.com/claudiob/hcp
48
- source_code_uri: https://github.com/claudiob/hcp
49
- changelog_uri: https://github.com/claudiob/hcp
158
+ changelog_uri: https://github.com/claudiob/hcp/blob/main/CHANGELOG.md
159
+ documentation_uri: https://rubydoc.info/gems/hcp
50
160
  rdoc_options: []
51
161
  require_paths:
52
162
  - lib