hcp 1.4.0 → 3.0.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +0 -1
  3. data/CHANGELOG.md +75 -0
  4. data/CLAUDE.md +34 -9
  5. data/README.md +99 -125
  6. data/lib/hcp/account.rb +25 -0
  7. data/lib/hcp/answer.rb +3 -11
  8. data/lib/hcp/client.rb +41 -0
  9. data/lib/hcp/collections/estimates.rb +22 -0
  10. data/lib/hcp/collections/jobs.rb +32 -0
  11. data/lib/hcp/collections/leads.rb +25 -0
  12. data/lib/hcp/collections/technicians.rb +23 -0
  13. data/lib/hcp/collections/visits.rb +80 -0
  14. data/lib/hcp/error.rb +1 -1
  15. data/lib/hcp/errors/throttled.rb +4 -0
  16. data/lib/hcp/event.rb +1 -6
  17. data/lib/hcp/resources/business.rb +11 -0
  18. data/lib/hcp/resources/customer.rb +9 -45
  19. data/lib/hcp/resources/estimate.rb +40 -46
  20. data/lib/hcp/resources/job.rb +58 -69
  21. data/lib/hcp/resources/lead.rb +26 -0
  22. data/lib/hcp/resources/line.rb +8 -0
  23. data/lib/hcp/resources/location.rb +7 -0
  24. data/lib/hcp/resources/quote.rb +28 -0
  25. data/lib/hcp/resources/technician.rb +7 -0
  26. data/lib/hcp/resources/visit.rb +45 -0
  27. data/lib/hcp/version.rb +3 -3
  28. data/lib/hcp.rb +22 -41
  29. metadata +30 -28
  30. data/lib/hcp/access.rb +0 -12
  31. data/lib/hcp/concerns/chainable.rb +0 -39
  32. data/lib/hcp/concerns/keyed.rb +0 -14
  33. data/lib/hcp/concerns/named.rb +0 -7
  34. data/lib/hcp/concerns/queryable.rb +0 -41
  35. data/lib/hcp/concerns/scheduled.rb +0 -16
  36. data/lib/hcp/concerns/statused.rb +0 -23
  37. data/lib/hcp/concerns/timestamped.rb +0 -10
  38. data/lib/hcp/errors/not_found.rb +0 -4
  39. data/lib/hcp/errors/too_many_requests.rb +0 -16
  40. data/lib/hcp/filter.rb +0 -36
  41. data/lib/hcp/key.rb +0 -24
  42. data/lib/hcp/lead/pipeline.rb +0 -36
  43. data/lib/hcp/lead.rb +0 -44
  44. data/lib/hcp/relation.rb +0 -76
  45. data/lib/hcp/request.rb +0 -31
  46. data/lib/hcp/resource.rb +0 -58
  47. data/lib/hcp/resources/address.rb +0 -24
  48. data/lib/hcp/resources/booking_window.rb +0 -52
  49. data/lib/hcp/resources/company.rb +0 -42
  50. data/lib/hcp/resources/employee.rb +0 -12
  51. data/lib/hcp/resources/estimate/option.rb +0 -26
  52. data/lib/hcp/resources/job/appointment.rb +0 -22
  53. data/lib/hcp/resources/job/invoice.rb +0 -21
  54. data/lib/hcp/resources/line_item.rb +0 -20
  55. data/lib/hcp/resources/note.rb +0 -6
  56. data/lib/hcp/resources/schedule.rb +0 -21
@@ -0,0 +1,25 @@
1
+ module Hcp
2
+ # The leads of one location: who asked a Housecall Pro user for work, before there is a job.
3
+ class Leads < Company::Leads
4
+ # @param client [Client] how to reach Housecall Pro as the location.
5
+ def initialize(client:)
6
+ @client = client
7
+ end
8
+
9
+ # Opens a lead, and a customer with it, on Housecall Pro. A lead has no title of its own, so
10
+ # the description heads the note and the notes follow it.
11
+ # @return [Lead] lead as Housecall Pro filed it, with its customer's ID beside it.
12
+ def create(name:, surname:, phone:, email:, address:, description:, notes:, source:)
13
+ customer = { first_name: name, last_name: surname, email: email, mobile_number: phone,
14
+ lead_source: source, }
15
+ body = { customer: customer.compact_blank, address: address, lead_source: source,
16
+ note: [ description, notes ].compact_blank.join("\n"), }
17
+ Lead.new node: @client.post('leads', body.compact_blank), client: @client
18
+ end
19
+
20
+ # Reaches the network for nothing: the lead is named, and moved once it is asked to be.
21
+ # @param id [String] ID Housecall Pro files the lead under.
22
+ # @return [Lead] the lead, able to move through the pipeline.
23
+ def find(id) = Lead.new node: { id: id }, client: @client
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Hcp
2
+ # The employees of one location, walked a page at a time. Housecall Pro lists the active
3
+ # ones and no others, so a crew that has left the business is not among them.
4
+ class Technicians < Company::Collection
5
+ # Employees a page: the most Housecall Pro answers with, and more than it refuses.
6
+ PAGE = 200
7
+
8
+ # @param client [Client] how to reach Housecall Pro as the location.
9
+ def initialize(client:)
10
+ @client = client
11
+ end
12
+
13
+ # Nothing is read until the walk starts, and a page only once the one before it runs out.
14
+ # @yield [Technician] each employee, in the order Housecall Pro lists them.
15
+ def each
16
+ (1..).each do |page|
17
+ body = @client.get 'employees', page: page, page_size: PAGE
18
+ body.fetch('employees').each { |node| yield Technician.new node: node }
19
+ break if page >= body.fetch('total_pages')
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,80 @@
1
+ module Hcp
2
+ # The visits of one location: the appointments Housecall Pro files inside jobs, and the slot
3
+ # it files on each estimate. They are read off the work booked across a window, a page of it
4
+ # at a time, and each kind costs a list of its own.
5
+ class Visits < Company::Visits
6
+ # @param client [Client] how to reach Housecall Pro as the location.
7
+ # @param from [Time, nil] the moment the window opens, or nothing for every visit there was.
8
+ # @param to [Time, nil] the moment the window closes, or nothing for every visit to come.
9
+ # @param technician [Company::Technician, nil] whose work to ask for, or nothing for all.
10
+ # @param jobs [Boolean] whether to read the stops of jobs.
11
+ # @param leads [Boolean] whether to read the stops of estimates.
12
+ def initialize(client:, from: nil, to: nil, technician: nil, jobs: true, leads: true)
13
+ @client = client
14
+ @from = from
15
+ @to = to
16
+ @technician = technician
17
+ @jobs = jobs
18
+ @leads = leads
19
+ end
20
+
21
+ # @param from [Time, nil] the moment the window opens, or nothing for every visit there was.
22
+ # @param to [Time, nil] the moment the window closes, or nothing for every visit to come.
23
+ # @return [Visits] the same list, narrowed to the visits booked to start between the two.
24
+ def between(from, to) = with from: from, to: to
25
+
26
+ # Housecall Pro narrows both lists by who is assigned to the work, so the window is asked
27
+ # for as this technician's; a job's stop is the whole crew's until somebody is dispatched
28
+ # to it, so the stops they are not on are let go once the work comes back.
29
+ # @param technician [Company::Technician] whoever the work is booked for.
30
+ # @return [Company::Selection] the same list, narrowed to the stops they are booked for.
31
+ def assigned_to(technician)
32
+ theirs = with technician: technician
33
+ Company::Selection.new(collection: theirs) do |visit|
34
+ visit.technicians.any? { |each| each.id == technician.id }
35
+ end
36
+ end
37
+
38
+ # @return [Visits] the same list, read off the jobs alone: one request rather than two.
39
+ def for_jobs = with leads: false
40
+
41
+ # @return [Visits] the same list, read off the estimates alone: one request rather than two.
42
+ def for_leads = with jobs: false
43
+
44
+ # Work booked across the window carries every stop in it, so each list is read once and
45
+ # what was called off keeps its stops to itself.
46
+ # @yield [Visit] each visit in the window, the jobs' stops before the estimates'.
47
+ def each(&)
48
+ jobs.each { |job| stops job, & } if @jobs
49
+ estimates.each { |estimate| stops estimate, & } if @leads
50
+ end
51
+
52
+ private
53
+
54
+ def with(**changed)
55
+ self.class.new(**{ client: @client, from: @from, to: @to, technician: @technician,
56
+ jobs: @jobs, leads: @leads }.merge(changed))
57
+ end
58
+
59
+ def stops(work)
60
+ return if work.canceled?
61
+
62
+ work.visits.each { |visit| yield visit if window.cover? visit.starts_at }
63
+ end
64
+
65
+ def window = @from..@to
66
+
67
+ def crew = { employee_ids: (@technician && [ @technician.id ]) }
68
+
69
+ def jobs
70
+ bounds = { scheduled_end_min: @from&.utc&.iso8601, scheduled_start_max: @to&.utc&.iso8601 }
71
+ params = bounds.merge(crew).compact.merge expand: [ 'appointments' ]
72
+ Jobs.new client: @client, params: params
73
+ end
74
+
75
+ def estimates
76
+ bounds = { scheduled_start_min: @from&.utc&.iso8601, scheduled_start_max: @to&.utc&.iso8601 }
77
+ Estimates.new client: @client, params: bounds.merge(crew).compact
78
+ end
79
+ end
80
+ end
data/lib/hcp/error.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Hcp
2
2
  # Raised where Housecall Pro would not answer.
3
- Error = Class.new StandardError
3
+ Error = Class.new Company::Error
4
4
  end
@@ -0,0 +1,4 @@
1
+ module Hcp
2
+ # Raised where Housecall Pro refuses a request for rate, and answers it again a little later.
3
+ Throttled = Class.new Company::Throttled
4
+ end
data/lib/hcp/event.rb CHANGED
@@ -8,13 +8,10 @@ module Hcp
8
8
  TIMESTAMP_HEADER = 'Api-Timestamp'
9
9
 
10
10
  # @param params [Hash] the payload for an event webhook.
11
- def initialize(params = {})
11
+ def initialize(params)
12
12
  @params = params
13
13
  end
14
14
 
15
- # @return [Symbol] the type of event, e.g.: :lead_converted, :job_created, :invoice_sent.
16
- def type = @params.fetch(:event, '').gsub('.', '_').to_sym
17
-
18
15
  # @return [String] unique identifier of the lead in a :lead_converted event.
19
16
  def lead_id = @params.dig :lead, :id
20
17
 
@@ -50,10 +47,8 @@ module Hcp
50
47
 
51
48
  private
52
49
 
53
- # @return [Hash] the latest conversion, applies to 'lead.converted' events.
54
50
  def conversion = @params.dig(:lead, :conversions).last
55
51
 
56
- # @return [Symbol] the type of resource affected by the event, can be :lead or :job.
57
52
  def resource_type = @params[:event].split('.').first.to_sym
58
53
  end
59
54
  end
@@ -0,0 +1,11 @@
1
+ module Hcp
2
+ # The business behind a key: the account, or one location of a franchise.
3
+ class Business < Company::Business
4
+ # The node keys Housecall Pro spells otherwise than the vocabulary.
5
+ def self.keys = { phone: :phone_number }
6
+
7
+ private
8
+
9
+ def below = records Business, :locations
10
+ end
11
+ end
@@ -1,52 +1,16 @@
1
1
  module Hcp
2
2
  # Whoever the work is for.
3
- class Customer < Resource
4
- extend Queryable
5
- include Named, Timestamped
3
+ class Customer < Company::Customer
4
+ # The node keys Housecall Pro spells otherwise than the vocabulary.
5
+ def self.keys = { name: :first_name, surname: :last_name }
6
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] }
7
+ # The numbers Housecall Pro holds for a customer, the one they are reached on first.
8
+ NUMBERS = %i[mobile_number home_number work_number]
9
9
 
10
- # The conditions Housecall Pro takes more than one of, and refuses one of.
11
- MANY = %i[location_ids]
10
+ # @return [String, nil] first name, or the business's name where a person has none.
11
+ def name = super.presence || attribute(:company).presence
12
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'
13
+ # @return [String, nil] ten digits they are reached on, nil where none can be dialed.
14
+ def phone = NUMBERS.lazy.filter_map { |number| Company::Phone.from @node[number] }.first
51
15
  end
52
16
  end
@@ -1,49 +1,43 @@
1
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'
2
+ # Work a Housecall Pro user was asked to look at and price, which the vocabulary reads as the
3
+ # lead it is: a stop to go and see it, before there is a job. Housecall Pro schedules it the
4
+ # way it schedules a job but hangs no appointments under it, so it holds the one slot.
5
+ class Estimate < Company::Lead
6
+ # @return [Customer, nil] customer the estimate was opened for.
7
+ def customer = record Customer, :customer
8
+
9
+ # Housecall Pro files the address and the customer side by side, the way it does on a job.
10
+ # @return [Location, nil] where the work would happen, nil where it is booked nowhere.
11
+ def location
12
+ address = @node[:address]
13
+ Location.new node: address.merge(customer: @node[:customer]) if address[:id].present?
14
+ end
15
+
16
+ # @return [Array<Technician>] employees the estimate is assigned to.
17
+ def technicians = records Technician, :assigned_employees
18
+
19
+ # Housecall Pro gives an estimate no words of its own: what it says is said by its options.
20
+ # @return [nil] nothing, so a stop of one goes undescribed.
21
+ def description = nil
22
+
23
+ # Housecall Pro files no ID on the slot, there being only ever the one, so it answers to
24
+ # the estimate's own.
25
+ # @return [Array<Visit>] the one slot the estimate is booked for, empty where it has none.
26
+ def visits
27
+ booked = @node.dig :schedule, :scheduled_start
28
+ return [] if booked.blank?
29
+
30
+ [ Visit.new(node: slot, lead: self) ]
31
+ end
32
+
33
+ # @return [Boolean] whether the customer or the pro called the estimate off.
34
+ def canceled? = attribute(:work_status).to_s.end_with? 'canceled'
35
+
36
+ private
37
+
38
+ def slot
39
+ { id: id, start_time: @node.dig(:schedule, :scheduled_start),
40
+ end_time: @node.dig(:schedule, :scheduled_end) }
41
+ end
48
42
  end
49
43
  end
@@ -1,73 +1,62 @@
1
1
  module Hcp
2
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'
3
+ class Job < Company::Job
4
+ # The node keys Housecall Pro spells otherwise than the vocabulary.
5
+ def self.keys = { amount: :total_amount }
6
+
7
+ # Housecall Pro nests when the work is booked and when it was done, and files the address
8
+ # and the customer side by side; the node is read flat, the way the vocabulary reads one.
9
+ # @param node [Hash] job as Housecall Pro answered it.
10
+ # @param client [Client] how to reach Housecall Pro for what the job holds elsewhere.
11
+ def initialize(node: {}, client:)
12
+ node = node.with_indifferent_access
13
+ super node: node.merge(scheduled_at: node.dig(:schedule, :scheduled_start),
14
+ completed_at: node.dig(:work_timestamps, :completed_at), location: location_from(node))
15
+ @client = client
16
+ end
17
+
18
+ # Housecall Pro files the notes on a job one by one, which read as a list, one to a line.
19
+ # @return [String, nil] every note's content, `- ` before each, nil where none was written.
20
+ def notes = Array(super).map { |note| "- #{note[:content]}" }.join("\n").presence
21
+
22
+ # Housecall Pro counts in cents, and a caller reads dollars.
23
+ # @return [BigDecimal, nil] what the job comes to.
24
+ def amount = (cents = super) && cents / 100
25
+
26
+ # Housecall Pro files a job's lines under their own endpoint rather than beside the job,
27
+ # so they are read once, on the first ask.
28
+ # @return [Array<Line>] lines the job is billed as.
29
+ def lines
30
+ @lines ||= @client.get("jobs/#{id}/line_items").fetch('data').map { Line.new node: it }
31
+ end
32
+
33
+ # @return [Location, nil] where the work happens, nil where the job is booked nowhere.
34
+ def location = record Location, :location
35
+
36
+ # Housecall Pro nests the appointments under the schedule, and only where the list was
37
+ # asked to bring them.
38
+ # @return [Array<Visit>] stops the job is booked as, empty where none came back.
39
+ def visits = Array(@node.dig :schedule, :appointments).map { Visit.new node: it, job: self }
40
+
41
+ # @return [Array<Technician>] employees the job is assigned to, empty where it is assigned
42
+ # to nobody.
43
+ def technicians = records Technician, :assigned_employees
44
+
45
+ # @return [Boolean] whether the customer or the pro called the job off.
46
+ def canceled? = attribute(:work_status).to_s.end_with? 'canceled'
47
+
48
+ # Housecall Pro names the estimate option a job was created from beside the job, and the
49
+ # option is priced among the customer's estimates, so the quote takes the customer along.
50
+ # @return [Quote, nil] option the job was won with, nil where it was won without one.
51
+ def quote
52
+ option_id = @node[:original_estimate_id]
53
+ Quote.new node: { id: option_id, customer: @node[:customer] }, client: @client if option_id
54
+ end
55
+
56
+ private
57
+
58
+ def location_from(node)
59
+ node[:address].merge customer: node[:customer] if node.dig(:address, :id).present?
60
+ end
72
61
  end
73
62
  end
@@ -0,0 +1,26 @@
1
+ module Hcp
2
+ # Somebody who asked a Housecall Pro user for work, and where they sit in the pipeline.
3
+ class Lead < Company::Lead
4
+ # @param node [Hash] lead as Housecall Pro answered it.
5
+ # @param client [Client] how to reach Housecall Pro as the lead's location.
6
+ def initialize(node: {}, client:)
7
+ super node: node
8
+ @client = client
9
+ end
10
+
11
+ # Moves the lead to the status going by this name. Housecall Pro moves a lead by status ID
12
+ # and names them only by their words, so the status is looked up first.
13
+ # @param status_name [String] status as the account names it, such as 'Won'.
14
+ def update(status_name:)
15
+ status = statuses.find { |each| each['name'] == status_name } || unknown(status_name)
16
+ @client.put 'pipeline/statuses',
17
+ resource_type: 'lead', resource_id: id, status_id: status['id']
18
+ end
19
+
20
+ private
21
+
22
+ def statuses = @client.get('pipeline/statuses', resource_type: 'lead').fetch 'statuses'
23
+
24
+ def unknown(name) = raise Error, "Status #{name} not found for lead #{id}"
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ module Hcp
2
+ # One line of what a job comes to.
3
+ class Line < Company::Line
4
+ # Housecall Pro counts in cents, and a caller reads dollars.
5
+ # @return [BigDecimal, nil] what the line comes to.
6
+ def amount = (cents = super) && cents / 100
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Hcp
2
+ # Where the work happens: the address Housecall Pro books a job at.
3
+ class Location < Company::Location
4
+ # @return [Customer, nil] whose place it is, as Housecall Pro filed them beside the job.
5
+ def customer = record Customer, :customer
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ module Hcp
2
+ # A price a Housecall Pro user sent, which Housecall Pro calls an estimate option: a job names
3
+ # the option it was created from, and an estimate holds one option or several.
4
+ class Quote < Company::Quote
5
+ # @param node [Hash] option as Housecall Pro named it beside a job: its ID, and the customer.
6
+ # @param client [Client] how to reach Housecall Pro for the rest of it.
7
+ def initialize(node: {}, client:)
8
+ super node: node
9
+ @client = client
10
+ end
11
+
12
+ # Housecall Pro reads an estimate by its own ID and not by an option's, so the option is
13
+ # found among the customer's estimates, read once on the first ask.
14
+ # @return [BigDecimal, nil] what the option comes to in dollars, nil where none is found.
15
+ def amount = option && BigDecimal(option['total_amount'].to_s) / 100
16
+
17
+ private
18
+
19
+ def option = options.find { |each| each['id'] == id }
20
+
21
+ def options = estimates.flat_map { |estimate| estimate['options'] }
22
+
23
+ def estimates
24
+ @estimates ||= @client.get('estimates', customer_id: @node.dig(:customer, :id),
25
+ page_size: Jobs::PAGE).fetch 'estimates'
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,7 @@
1
+ module Hcp
2
+ # A person the business sends out, whom Housecall Pro calls an employee.
3
+ class Technician < Company::Technician
4
+ # The node keys Housecall Pro spells otherwise than the vocabulary.
5
+ def self.keys = { name: :first_name, surname: :last_name }
6
+ end
7
+ end
@@ -0,0 +1,45 @@
1
+ module Hcp
2
+ # One stop, which Housecall Pro hangs under a job as an appointment and under an estimate as
3
+ # the estimate's own slot: it happens where the work does and says what the work says.
4
+ class Visit < Company::Visit
5
+ # The node keys Housecall Pro spells otherwise than the vocabulary.
6
+ def self.keys = { starts_at: :start_time, ends_at: :end_time }
7
+
8
+ # @param node [Hash] slot as Housecall Pro answered it under the work it belongs to.
9
+ # @param job [Job, nil] job the slot sits under, nil where an estimate does.
10
+ # @param lead [Estimate, nil] estimate the slot sits under, nil where a job does.
11
+ def initialize(node: {}, job: nil, lead: nil)
12
+ super node: node
13
+ @job = job
14
+ @lead = lead
15
+ end
16
+
17
+ # @return [Job, nil] job the stop belongs to, nil where the stop is an estimate's.
18
+ attr_reader :job
19
+
20
+ # @return [Estimate, nil] estimate the stop belongs to, nil where the stop is a job's.
21
+ attr_reader :lead
22
+
23
+ # @return [String, nil] what the work is called: a stop has no words of its own.
24
+ def description = work.description
25
+
26
+ # An appointment has no address of its own and an estimate's slot is the estimate's, so a
27
+ # stop is where its work is.
28
+ # @return [Location, nil] where the stop happens, nil where the work is booked nowhere.
29
+ def location = work.location
30
+
31
+ # Housecall Pro dispatches a job's stop to some of its crew, or to none of it, and a stop
32
+ # dispatched to nobody is the whole crew's. An estimate dispatches nothing and has one crew.
33
+ # @return [Array<Technician>] whoever the stop is booked for.
34
+ def technicians = dispatched.presence || work.technicians
35
+
36
+ private
37
+
38
+ def work = @job || @lead
39
+
40
+ def dispatched
41
+ ids = Array attribute(:dispatched_employees_ids)
42
+ work.technicians.select { |technician| ids.include? technician.id }
43
+ end
44
+ end
45
+ end
data/lib/hcp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
- # Ruby client for the Housecall Pro API.
1
+ # The Housecall Pro API, answered in the vocabulary of the company gem.
2
2
  module Hcp
3
- # Current version of the gem.
4
- VERSION = '1.4.0'
3
+ # The version of this gem, as RubyGems knows it.
4
+ VERSION = '3.0.0'
5
5
  end