hcp 2.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/CLAUDE.md +21 -0
- data/README.md +63 -7
- data/lib/hcp/account.rb +3 -0
- data/lib/hcp/collections/estimates.rb +22 -0
- data/lib/hcp/collections/technicians.rb +23 -0
- data/lib/hcp/collections/visits.rb +55 -14
- data/lib/hcp/resources/estimate.rb +43 -0
- data/lib/hcp/resources/job.rb +4 -0
- data/lib/hcp/resources/technician.rb +7 -0
- data/lib/hcp/resources/visit.rb +32 -8
- data/lib/hcp/version.rb +1 -1
- data/lib/hcp.rb +4 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d401d8b087f9b1c05637aaad2f69c6f343ff3d8d50809c02d1b12f6d3fa2889
|
|
4
|
+
data.tar.gz: e58432a01d9530029c912b92451c9e94101dd8de0982ca072fb540572d14cbfb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9254b6d9c951494705a671b09484a8bef8586ca085e5a75b3101e66811179cdadd1239e6dd3598d80b9aa097bc0b6a9e7deb982351abc74c48de6f224a7320d8
|
|
7
|
+
data.tar.gz: 0afdf26a35fa1a7c36e8862d272e39590b02f1d7462393bc972379c6a51a32d7f210c02ff9a0788ef0b973bb02259063d489503abc2bbab43ddfe487b1fe5fc1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [3.0.1] - 2026-09-15
|
|
4
|
+
|
|
5
|
+
- [Fix] `visit.anytime?` answers false on an estimate's slot rather than nil. Housecall Pro
|
|
6
|
+
books an estimate for an hour and has no anytime to answer with, so the slot named none --
|
|
7
|
+
and a caller storing what a visit reads had a nil where it expected a yes or a no.
|
|
8
|
+
|
|
9
|
+
## [3.0.0] - 2026-09-15
|
|
10
|
+
|
|
11
|
+
- [Breaking change] The vocabulary is `company` 2.1: a `Company::Selection` takes its rule as a
|
|
12
|
+
block, a visit answers a lead as well as a job, and it says where it is. The pin is `~> 2.1`.
|
|
13
|
+
|
|
14
|
+
- [Breaking change] `account.visits` is every stop booked, not only a job's: an estimate is
|
|
15
|
+
work still being looked at, so its slot is a visit too, and the list now reads `/jobs` and
|
|
16
|
+
`/estimates` rather than `/jobs` alone. `visit.job` is nil on an estimate's slot and
|
|
17
|
+
`visit.lead` is the `Hcp::Estimate`; `visit.description` is nil there, Housecall Pro giving
|
|
18
|
+
an estimate no words of its own. A caller that wants what it had before asks
|
|
19
|
+
`account.visits.for_jobs`, which is the one request it always was, and `for_leads` is the
|
|
20
|
+
estimates alone.
|
|
21
|
+
|
|
22
|
+
- [Feature] `Hcp::Estimate`, a `Company::Lead`: `id`, `customer`, `location`, `technicians` and
|
|
23
|
+
the one slot it is booked for. Housecall Pro expands an estimate with `attachments` and not
|
|
24
|
+
with appointments, so the estimate's own `schedule` is the slot and answers to its ID.
|
|
25
|
+
|
|
26
|
+
- [Feature] `Hcp::Visit#location`, where the stop is: an appointment has no address of its own
|
|
27
|
+
and takes the job's, an estimate's slot takes the estimate's. Both nodes are read in full
|
|
28
|
+
already, so it costs nothing and is always there.
|
|
29
|
+
|
|
30
|
+
- [Feature] `Hcp::Visit#lead`, and `technicians` reading an estimate's `assigned_employees`
|
|
31
|
+
where a job's stop reads its dispatch.
|
|
32
|
+
|
|
33
|
+
- [Feature] `account.technicians` walks the location's employees a page at a time, each an
|
|
34
|
+
`Hcp::Technician` reading `id`, `name` off `first_name` and `surname` off `last_name`.
|
|
35
|
+
|
|
36
|
+
- [Feature] `job.technicians` is the crew a job is assigned to, and `visit.technicians` whoever
|
|
37
|
+
an appointment was dispatched to -- or, where it was dispatched to nobody, the job's whole
|
|
38
|
+
crew, which is how Housecall Pro draws it.
|
|
39
|
+
|
|
40
|
+
- [Feature] `account.visits.between(from, to).assigned_to(technician)` is one technician's
|
|
41
|
+
week. The window goes to Housecall Pro as `employee_ids`, so only their jobs come back, and
|
|
42
|
+
the stops of those jobs they are not on are let go as the jobs arrive.
|
|
43
|
+
|
|
3
44
|
## [2.0.0] - 2026-09-09
|
|
4
45
|
|
|
5
46
|
- [Breaking change] The key is one account's: `Hcp::Account.new(key:, company_id:)`, a
|
data/CLAUDE.md
CHANGED
|
@@ -42,6 +42,27 @@ documented shape. What has been found so far:
|
|
|
42
42
|
`Estimate not found` for an option's, so an option is found by listing
|
|
43
43
|
`GET /estimates?customer_id=` and searching the options. Probed live 2026-09-09.
|
|
44
44
|
|
|
45
|
+
- `employee_ids` narrows both `/jobs` and `/estimates` by assigned pro, and `/estimates` takes
|
|
46
|
+
the same `scheduled_start_min`/`scheduled_start_max` window as `/jobs`. Over one year on a
|
|
47
|
+
real account: 15 jobs to 5, and 40 estimates to 2. Worth writing down because the spec gives
|
|
48
|
+
the parameter no description at all on `/jobs` -- only the `/estimates` twin documents it.
|
|
49
|
+
`GET /employees` answers `first_name` and `last_name`. Probed live 2026-09-15.
|
|
50
|
+
|
|
51
|
+
Read off the spec but **not probed**, so treat as claims rather than findings:
|
|
52
|
+
|
|
53
|
+
- `GET /estimates`' `expand` enum is `attachments` alone -- there is no `appointments` on an
|
|
54
|
+
estimate, so its `schedule` is the one slot it occupies. Its `work_status` carries `canceled`.
|
|
55
|
+
- `GET /routes` is the only endpoint that groups a day's work, and is unusable as one: it takes
|
|
56
|
+
a single `date` and `per_page` rather than a range, refuses a Company API Key, and answers
|
|
57
|
+
`event_ids` and `estimate_ids` as bare strings, so the times still cost `/events` and
|
|
58
|
+
`/estimates`.
|
|
59
|
+
- `GET /events` -- time blocked out on the calendar -- takes no date and no employee filter at
|
|
60
|
+
all, so a week of it can only be paged in full. Its `recurrence_rule` is not expanded.
|
|
61
|
+
- `POST /estimates` takes `customer_id`, not a customer, so booking one needs `GET /customers`
|
|
62
|
+
(search by `q`) and `POST /customers` first. The arrival window is spelled three ways:
|
|
63
|
+
`arrival_window_in_minutes` on create, `arrival_window_minutes` on an appointment,
|
|
64
|
+
`arrival_window` on a schedule read back.
|
|
65
|
+
|
|
45
66
|
A company-scoped key refuses `X-Company-Id` with a 401 on every endpoint, so the header can only
|
|
46
67
|
be exercised with an application key. `GET /company` answers `locations` only to the latter, and
|
|
47
68
|
nests: a location holds locations of its own, several levels deep.
|
data/README.md
CHANGED
|
@@ -13,7 +13,7 @@ gem install hcp
|
|
|
13
13
|
Or, in a Gemfile, pinned to the current major:
|
|
14
14
|
|
|
15
15
|
```ruby
|
|
16
|
-
gem 'hcp', '~>
|
|
16
|
+
gem 'hcp', '~> 3.0'
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
`~> major.minor` means `bundle update` never crosses a breaking change.
|
|
@@ -84,18 +84,74 @@ A customer's `name` is their first name, or the business's where a person has no
|
|
|
84
84
|
|
|
85
85
|
## Visits
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
A visit is any booked time, and Housecall Pro books it two ways. Work already won is a job,
|
|
88
|
+
and Housecall Pro calls its stops appointments and files them inside it. Work still being
|
|
89
|
+
looked at is an estimate, which it schedules the same way but hangs no appointments under, so
|
|
90
|
+
an estimate holds the one slot. Both are read off the work booked across the window, a page of
|
|
91
|
+
it at a time; work called off keeps its stops to itself, and a list nothing narrows walks
|
|
92
|
+
every job and estimate there was.
|
|
91
93
|
|
|
92
94
|
```ruby
|
|
93
95
|
account.visits.upcoming(2.weeks).each do |visit|
|
|
94
|
-
visit.id, visit.
|
|
95
|
-
visit.
|
|
96
|
+
visit.id, visit.starts_at, visit.ends_at, visit.anytime?
|
|
97
|
+
visit.description # => what the job is called, or nil: an estimate has no words of its own
|
|
98
|
+
visit.location # => where the stop is, whatever it was booked for
|
|
99
|
+
visit.job # => the Hcp::Job the stop belongs to, or nil where an estimate does
|
|
100
|
+
visit.lead # => the Hcp::Estimate it belongs to, or nil where a job does
|
|
101
|
+
visit.technicians # => the Hcp::Technicians the stop is booked for
|
|
96
102
|
end
|
|
97
103
|
```
|
|
98
104
|
|
|
105
|
+
The two cost a list each, so a caller that wants one kind asks for it and spends one request:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
account.visits.upcoming(2.weeks).for_jobs # => only the appointments, one request
|
|
109
|
+
account.visits.upcoming(2.weeks).for_leads # => only the estimates' slots, one request
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A stop says where it is without being asked what it was booked for: an appointment has no
|
|
113
|
+
address of its own and takes the job's, and an estimate's slot takes the estimate's. A caller
|
|
114
|
+
reading a schedule never reaches through `visit.job` for an address.
|
|
115
|
+
|
|
116
|
+
An estimate reads as the lead it is -- `id`, `customer`, `location` -- because its other half,
|
|
117
|
+
the price, is already `Hcp::Quote`: Housecall Pro files the visit and the prices as one record
|
|
118
|
+
and the vocabulary reads them as two.
|
|
119
|
+
|
|
120
|
+
`account.visits.create` is named by the vocabulary and not answered here yet; it raises
|
|
121
|
+
`NotImplementedError`. Booking one means `POST /estimates`, which takes a `customer_id` rather
|
|
122
|
+
than a customer, so it needs `GET /customers` and `POST /customers` first, and all three want
|
|
123
|
+
probing before they are written against.
|
|
124
|
+
|
|
125
|
+
## The schedule
|
|
126
|
+
|
|
127
|
+
The crew are Housecall Pro's employees, and the active ones are walked a page at a time:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
account.technicians.each do |technician|
|
|
131
|
+
technician.id, technician.name, technician.surname
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
One technician's week is the visits in it narrowed to them, which is how a schedule reads:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
monday = Date.today.beginning_of_week.in_time_zone
|
|
139
|
+
account.visits.between(monday, monday + 1.week).assigned_to(technician).each do |visit|
|
|
140
|
+
visit.starts_at, visit.ends_at, visit.job.location.street
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Housecall Pro narrows both lists by who is assigned to the work, so the window is asked for as
|
|
145
|
+
that technician's and nobody else's comes back -- verified against a real account, where a year
|
|
146
|
+
of work went from 15 jobs to 5 and from 40 estimates to 2. It narrows no further: a job's appointments are
|
|
147
|
+
dispatched to some of its crew or to none of it, so a stop dispatched to nobody is the whole
|
|
148
|
+
crew's, and the stops the technician is not on are let go once the jobs arrive. Asking for the
|
|
149
|
+
week and asking for the technician narrow the same list, in either order.
|
|
150
|
+
|
|
151
|
+
What Housecall Pro schedules elsewhere is still not here: time blocked out on the calendar is
|
|
152
|
+
filed under `/events`, which takes no date and no employee to narrow by, so a week of it cannot
|
|
153
|
+
be asked for -- only paged in full.
|
|
154
|
+
|
|
99
155
|
## Errors
|
|
100
156
|
|
|
101
157
|
Everything descends from `Hcp::Error`, which descends from `Company::Error`, so one rescue
|
data/lib/hcp/account.rb
CHANGED
|
@@ -16,6 +16,9 @@ module Hcp
|
|
|
16
16
|
# @return [Visits] visits of the location, read off its jobs.
|
|
17
17
|
def visits = Visits.new client: @client
|
|
18
18
|
|
|
19
|
+
# @return [Technicians] employees of the location, walked a page at a time.
|
|
20
|
+
def technicians = Technicians.new client: @client
|
|
21
|
+
|
|
19
22
|
# @return [Leads] leads of the location, to open and to move.
|
|
20
23
|
def leads = Leads.new client: @client
|
|
21
24
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Hcp
|
|
2
|
+
# The estimates of one location, walked a page at a time. Housecall Pro narrows them by the
|
|
3
|
+
# same words it narrows jobs by, so a window and a crew cost what they cost on a job.
|
|
4
|
+
class Estimates < Company::Collection
|
|
5
|
+
# @param client [Client] how to reach Housecall Pro as the location.
|
|
6
|
+
# @param params [Hash] what the list is narrowed to, as Housecall Pro filters estimates.
|
|
7
|
+
def initialize(client:, params: {})
|
|
8
|
+
@client = client
|
|
9
|
+
@params = params
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Nothing is read until the walk starts, and a page only once the one before it runs out.
|
|
13
|
+
# @yield [Estimate] each estimate in the window, in the order Housecall Pro lists them.
|
|
14
|
+
def each
|
|
15
|
+
(1..).each do |page|
|
|
16
|
+
body = @client.get 'estimates', @params.merge(page: page, page_size: Jobs::PAGE)
|
|
17
|
+
body.fetch('estimates').each { |node| yield Estimate.new node: node }
|
|
18
|
+
break if page >= body.fetch('total_pages')
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
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
|
|
@@ -1,39 +1,80 @@
|
|
|
1
1
|
module Hcp
|
|
2
|
-
# The visits of one location
|
|
3
|
-
#
|
|
4
|
-
|
|
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
|
|
5
6
|
# @param client [Client] how to reach Housecall Pro as the location.
|
|
6
7
|
# @param from [Time, nil] the moment the window opens, or nothing for every visit there was.
|
|
7
8
|
# @param to [Time, nil] the moment the window closes, or nothing for every visit to come.
|
|
8
|
-
|
|
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)
|
|
9
13
|
@client = client
|
|
10
14
|
@from = from
|
|
11
15
|
@to = to
|
|
16
|
+
@technician = technician
|
|
17
|
+
@jobs = jobs
|
|
18
|
+
@leads = leads
|
|
12
19
|
end
|
|
13
20
|
|
|
14
21
|
# @param from [Time, nil] the moment the window opens, or nothing for every visit there was.
|
|
15
22
|
# @param to [Time, nil] the moment the window closes, or nothing for every visit to come.
|
|
16
23
|
# @return [Visits] the same list, narrowed to the visits booked to start between the two.
|
|
17
|
-
def between(from, to) =
|
|
24
|
+
def between(from, to) = with from: from, to: to
|
|
18
25
|
|
|
19
|
-
#
|
|
20
|
-
# a
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 }
|
|
27
35
|
end
|
|
28
36
|
end
|
|
29
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
|
+
|
|
30
52
|
private
|
|
31
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
|
+
|
|
32
65
|
def window = @from..@to
|
|
33
66
|
|
|
67
|
+
def crew = { employee_ids: (@technician && [ @technician.id ]) }
|
|
68
|
+
|
|
34
69
|
def jobs
|
|
35
70
|
bounds = { scheduled_end_min: @from&.utc&.iso8601, scheduled_start_max: @to&.utc&.iso8601 }
|
|
36
|
-
|
|
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
|
|
37
78
|
end
|
|
38
79
|
end
|
|
39
80
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Hcp
|
|
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 the
|
|
24
|
+
# estimate's own, and books it for an hour rather than for any time in a day.
|
|
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, anytime: false, start_time: @node.dig(:schedule, :scheduled_start),
|
|
40
|
+
end_time: @node.dig(:schedule, :scheduled_end) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/hcp/resources/job.rb
CHANGED
|
@@ -38,6 +38,10 @@ module Hcp
|
|
|
38
38
|
# @return [Array<Visit>] stops the job is booked as, empty where none came back.
|
|
39
39
|
def visits = Array(@node.dig :schedule, :appointments).map { Visit.new node: it, job: self }
|
|
40
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
|
+
|
|
41
45
|
# @return [Boolean] whether the customer or the pro called the job off.
|
|
42
46
|
def canceled? = attribute(:work_status).to_s.end_with? 'canceled'
|
|
43
47
|
|
|
@@ -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
|
data/lib/hcp/resources/visit.rb
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
module Hcp
|
|
2
|
-
# One stop
|
|
3
|
-
# and says what the
|
|
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
4
|
class Visit < Company::Visit
|
|
5
5
|
# The node keys Housecall Pro spells otherwise than the vocabulary.
|
|
6
6
|
def self.keys = { starts_at: :start_time, ends_at: :end_time }
|
|
7
7
|
|
|
8
|
-
# @param node [Hash]
|
|
9
|
-
# @param job [Job] job the
|
|
10
|
-
|
|
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)
|
|
11
12
|
super node: node
|
|
12
13
|
@job = job
|
|
14
|
+
@lead = lead
|
|
13
15
|
end
|
|
14
16
|
|
|
15
|
-
# @return [Job] job the stop belongs to,
|
|
17
|
+
# @return [Job, nil] job the stop belongs to, nil where the stop is an estimate's.
|
|
16
18
|
attr_reader :job
|
|
17
19
|
|
|
18
|
-
# @return [
|
|
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
|
|
20
44
|
end
|
|
21
45
|
end
|
data/lib/hcp/version.rb
CHANGED
data/lib/hcp.rb
CHANGED
|
@@ -26,12 +26,16 @@ require 'hcp/collections/leads'
|
|
|
26
26
|
|
|
27
27
|
# Every record before the one that reads it beside itself, and the job before its list.
|
|
28
28
|
require 'hcp/resources/customer'
|
|
29
|
+
require 'hcp/resources/technician'
|
|
29
30
|
require 'hcp/resources/location'
|
|
30
31
|
require 'hcp/resources/line'
|
|
31
32
|
require 'hcp/resources/quote'
|
|
32
33
|
require 'hcp/resources/visit'
|
|
33
34
|
require 'hcp/resources/job'
|
|
35
|
+
require 'hcp/resources/estimate'
|
|
34
36
|
require 'hcp/collections/jobs'
|
|
37
|
+
require 'hcp/collections/estimates'
|
|
35
38
|
require 'hcp/collections/visits'
|
|
39
|
+
require 'hcp/collections/technicians'
|
|
36
40
|
require 'hcp/account'
|
|
37
41
|
require 'hcp/event'
|
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:
|
|
4
|
+
version: 3.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Baccigalupo
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1
|
|
32
|
+
version: '2.1'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '1
|
|
39
|
+
version: '2.1'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: minitest
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -137,19 +137,23 @@ files:
|
|
|
137
137
|
- lib/hcp/account.rb
|
|
138
138
|
- lib/hcp/answer.rb
|
|
139
139
|
- lib/hcp/client.rb
|
|
140
|
+
- lib/hcp/collections/estimates.rb
|
|
140
141
|
- lib/hcp/collections/jobs.rb
|
|
141
142
|
- lib/hcp/collections/leads.rb
|
|
143
|
+
- lib/hcp/collections/technicians.rb
|
|
142
144
|
- lib/hcp/collections/visits.rb
|
|
143
145
|
- lib/hcp/error.rb
|
|
144
146
|
- lib/hcp/errors/throttled.rb
|
|
145
147
|
- lib/hcp/event.rb
|
|
146
148
|
- lib/hcp/resources/business.rb
|
|
147
149
|
- lib/hcp/resources/customer.rb
|
|
150
|
+
- lib/hcp/resources/estimate.rb
|
|
148
151
|
- lib/hcp/resources/job.rb
|
|
149
152
|
- lib/hcp/resources/lead.rb
|
|
150
153
|
- lib/hcp/resources/line.rb
|
|
151
154
|
- lib/hcp/resources/location.rb
|
|
152
155
|
- lib/hcp/resources/quote.rb
|
|
156
|
+
- lib/hcp/resources/technician.rb
|
|
153
157
|
- lib/hcp/resources/visit.rb
|
|
154
158
|
- lib/hcp/version.rb
|
|
155
159
|
homepage: https://github.com/claudiob/hcp
|