jbr 2.0.0 → 2.2.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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +35 -0
- data/lib/jbr/client.rb +1 -1
- data/lib/jbr/mock/oauth.rb +1 -0
- data/lib/jbr/mock/visit.rb +32 -0
- data/lib/jbr/mock.rb +1 -1
- data/lib/jbr/oauth.rb +2 -2
- data/lib/jbr/property.rb +17 -0
- data/lib/jbr/version.rb +1 -1
- data/lib/jbr/visit.rb +96 -0
- data/lib/jbr.rb +4 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2e03e9b80223954e92497d75f84d2f6be78f23fadcbcfd7d82ba95e83ebacec
|
|
4
|
+
data.tar.gz: aac7754bb691e5b45ec8284a289eceacfc9afc961cbb71e6ffedc2f92a5fd2a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a962bbdc7d20e9549fb10d55a5c52ddb1de7ce7c92a9da20e61162f040a8b2da35c2c6daa545c4ef67e36b04eec0085fa03faddf10db6435674390c20ddacde
|
|
7
|
+
data.tar.gz: e565f59d6f14284fd3919a048f770e060588be06850d225947b4552d88b1201ea7149afec5fe733f57d78afaa1809ea762f62377a7596729917381ecd7d65e7f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [2.2.0] - 2026-08-07
|
|
2
|
+
|
|
3
|
+
- [New] Carry the client a visit is for -- id, first name, last name, phone and email --
|
|
4
|
+
and the ID of the property it happens at, so an app can file a visit against the people
|
|
5
|
+
and places it already knows
|
|
6
|
+
|
|
7
|
+
## [2.1.0] - 2026-08-07
|
|
8
|
+
|
|
9
|
+
- [New] Fetch the visits an account has scheduled from now on, with oauth.visits.upcoming.
|
|
10
|
+
Each carries its job, its times, whether it takes the whole day, whether the client
|
|
11
|
+
confirmed it, and the address of the property it happens at, in the same fields
|
|
12
|
+
Jbr::Property takes. It answers an Enumerator, so a page is read only once the one
|
|
13
|
+
before it runs out
|
|
14
|
+
- [New] Read a property's latitude and longitude, beside its street rather than under a
|
|
15
|
+
hash of their own. Every query that reads a property asks for them
|
|
16
|
+
|
|
1
17
|
## [2.0.0] - 2026-08-07
|
|
2
18
|
|
|
3
19
|
- [Fix] Require nothing but the standard library: to_query, present?, pluck,
|
data/README.md
CHANGED
|
@@ -86,6 +86,29 @@ invoice.issued_at # => 2026-05-22 12:12:53
|
|
|
86
86
|
invoice.completed_at # => 2026-05-22 14:32:53
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### Visits
|
|
90
|
+
|
|
91
|
+
Fetch the visits scheduled from now on, oldest first. Jobber is asked for a page at a time,
|
|
92
|
+
and only once the page before it runs out, so `first` costs one request where `to_a` costs
|
|
93
|
+
as many as the account has pages:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
visits = oauth.visits.upcoming # => an Enumerator, nothing fetched yet
|
|
97
|
+
visit = visits.first
|
|
98
|
+
visit.id # => 'Z2lkOi8vS'
|
|
99
|
+
visit.title # => 'Furnace tune-up'
|
|
100
|
+
visit.job_id # => 'Z2lkOi8vS'
|
|
101
|
+
visit.property_id # => 'Z2lkOi8vS'
|
|
102
|
+
visit.client # => { id: 'Z2lkOi8vS', first_name: 'Jane', last_name: 'Doe',
|
|
103
|
+
# phone: '5553335555', email: 'jane@example.com' }
|
|
104
|
+
visit.address # => { street: '1 Main St', city: 'Raleigh', state: 'NC', zip: '27601',
|
|
105
|
+
# latitude: 35.77, longitude: -78.63 }
|
|
106
|
+
visit.starts_at # => 2026-08-09 14:00:00
|
|
107
|
+
visit.ends_at # => 2026-08-09 16:00:00
|
|
108
|
+
visit.all_day? # => false
|
|
109
|
+
visit.client_confirmed? # => true
|
|
110
|
+
```
|
|
111
|
+
|
|
89
112
|
### Events
|
|
90
113
|
|
|
91
114
|
Parse the payload of a Jobber event webhook:
|
|
@@ -145,6 +168,18 @@ Mock successfully fetching a job:
|
|
|
145
168
|
Jbr.mock.job = { id: 'job-01', quote_id: 'quote-01', scheduled_at: Date.tomorrow.noon }
|
|
146
169
|
```
|
|
147
170
|
|
|
171
|
+
### Visits
|
|
172
|
+
|
|
173
|
+
Mock successfully fetching upcoming visits:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
Jbr.mock.visits = [ { id: 'visit-01', title: 'Furnace tune-up', job_id: 'job-01',
|
|
177
|
+
property_id: 'property-01', client: { id: 'client-01', first_name: 'Jane' },
|
|
178
|
+
address: { street: '1 Main St', city: 'Raleigh', state: 'NC', zip: '27601' },
|
|
179
|
+
starts_at: Date.tomorrow.noon, ends_at: Date.tomorrow.end_of_day,
|
|
180
|
+
all_day: false, client_confirmed: true } ]
|
|
181
|
+
```
|
|
182
|
+
|
|
148
183
|
### Invoices
|
|
149
184
|
|
|
150
185
|
Mock successfully fetching an invoice:
|
data/lib/jbr/client.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Jbr
|
|
|
5
5
|
LOOKUP = <<~GRAPHQL
|
|
6
6
|
query($searchTerm: String!) {
|
|
7
7
|
clientPhones(first: 1, searchTerm: $searchTerm) { nodes {
|
|
8
|
-
client { id updatedAt clientProperties { nodes { id address {
|
|
8
|
+
client { id updatedAt clientProperties { nodes { id address { #{Property::SELECTION} } }} }
|
|
9
9
|
} }
|
|
10
10
|
}
|
|
11
11
|
GRAPHQL
|
data/lib/jbr/mock/oauth.rb
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Jbr
|
|
2
|
+
# A visit that reads from {Jbr.mock} instead of Jobber.
|
|
3
|
+
class Mock::Visit < Visit
|
|
4
|
+
# @return [Enumerator<Mock::Visit>] the visits the app asked for.
|
|
5
|
+
def upcoming
|
|
6
|
+
Enumerator.new do |yielder|
|
|
7
|
+
Jbr.mock.visits.each { |visit| yielder << self.class.new(oauth: @oauth, node: visit) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @return [Object, nil] the values the app asked for.
|
|
12
|
+
def id = @node[:id]
|
|
13
|
+
|
|
14
|
+
def title = @node[:title]
|
|
15
|
+
|
|
16
|
+
def job_id = @node[:job_id]
|
|
17
|
+
|
|
18
|
+
def property_id = @node[:property_id]
|
|
19
|
+
|
|
20
|
+
def client = @node.fetch :client, {}
|
|
21
|
+
|
|
22
|
+
def address = @node.fetch :address, {}
|
|
23
|
+
|
|
24
|
+
def all_day? = @node[:all_day]
|
|
25
|
+
|
|
26
|
+
def client_confirmed? = @node[:client_confirmed]
|
|
27
|
+
|
|
28
|
+
def starts_at = @node[:starts_at]
|
|
29
|
+
|
|
30
|
+
def ends_at = @node[:ends_at]
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/jbr/mock.rb
CHANGED
|
@@ -2,6 +2,6 @@ module Jbr
|
|
|
2
2
|
# What an app under test wants Jobber to answer.
|
|
3
3
|
class Mock
|
|
4
4
|
# The canned answers, each read by the matching Mock resource.
|
|
5
|
-
attr_accessor :quote, :job, :invoice, :request, :oauth_url, :oauth_error
|
|
5
|
+
attr_accessor :quote, :job, :invoice, :request, :visits, :oauth_url, :oauth_error
|
|
6
6
|
end
|
|
7
7
|
end
|
data/lib/jbr/oauth.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
module Jbr
|
|
2
|
-
# Credentials for one Jobber account, and the gateway to
|
|
3
|
-
# with them. An expired access token is refreshed and the call retried once.
|
|
2
|
+
# Credentials for one Jobber account, and the gateway to all they read or write.
|
|
4
3
|
class OAuth
|
|
5
4
|
# The mutation that revokes the app on the account.
|
|
6
5
|
DISCONNECT_MUTATION = <<~GRAPHQL
|
|
@@ -33,6 +32,7 @@ module Jbr
|
|
|
33
32
|
def jobs = Job.new oauth: self
|
|
34
33
|
def quotes = Quote.new oauth: self
|
|
35
34
|
def requests = Request.new oauth: self
|
|
35
|
+
def visits = Visit.new oauth: self
|
|
36
36
|
|
|
37
37
|
# Run a statement, refreshing the access token once if Jobber says it expired.
|
|
38
38
|
# @param statement [String] the query or mutation to run.
|
data/lib/jbr/property.rb
CHANGED
|
@@ -14,6 +14,12 @@ module Jbr
|
|
|
14
14
|
# What Jobber calls each address field, against what a caller passes.
|
|
15
15
|
FIELDS = { street1: :street, city: :city, province: :state, postalCode: :zip }
|
|
16
16
|
|
|
17
|
+
# The two Jobber tucks inside the address and a property carries beside the street.
|
|
18
|
+
COORDINATES = %i[latitude longitude]
|
|
19
|
+
|
|
20
|
+
# The address as Jobber answers it, asked for wherever a property is read.
|
|
21
|
+
SELECTION = "#{FIELDS.keys.join ' '} coordinates { #{COORDINATES.join ' '} }"
|
|
22
|
+
|
|
17
23
|
# The fields a match is made on. City and state are written but never matched:
|
|
18
24
|
# Jobber holds whatever was typed, so "NC" and "North Carolina" -- or "Winston Salem"
|
|
19
25
|
# and "Winston-Salem" -- would read as two homes. The ZIP already places the home.
|
|
@@ -26,6 +32,17 @@ module Jbr
|
|
|
26
32
|
FIELDS.to_h { |jobber, ours| [ jobber, fields[ours] ] }.compact
|
|
27
33
|
end
|
|
28
34
|
|
|
35
|
+
# The fields a caller reads, from the address as Jobber holds it.
|
|
36
|
+
# @param address [Hash, nil] the address Jobber answered, if it answered one.
|
|
37
|
+
# @return [Hash] any of :street, :city, :state, :zip, :latitude and :longitude,
|
|
38
|
+
# without the ones Jobber left out.
|
|
39
|
+
def self.fields_from(address)
|
|
40
|
+
address ||= {}
|
|
41
|
+
coordinates = address['coordinates'] || {}
|
|
42
|
+
FIELDS.to_h { |jobber, ours| [ ours, address[jobber.to_s] ] }.
|
|
43
|
+
merge(COORDINATES.to_h { |ours| [ ours, coordinates[ours.to_s] ] }).compact
|
|
44
|
+
end
|
|
45
|
+
|
|
29
46
|
# Reach the property at an address, adding one when none of the client's matches.
|
|
30
47
|
# @param client_id [String] the client the property belongs to.
|
|
31
48
|
# @param address [Hash] the fields the work happens at.
|
data/lib/jbr/version.rb
CHANGED
data/lib/jbr/visit.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Jbr
|
|
2
|
+
# One stop at a property: when the work on a job is scheduled to happen.
|
|
3
|
+
class Visit < Resource
|
|
4
|
+
# What Jobber calls each field of the client the work is for, against what a caller reads.
|
|
5
|
+
CLIENT_FIELDS = { id: :id, firstName: :first_name, lastName: :last_name,
|
|
6
|
+
phone: :phone, email: :email,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
# The query that reads a page of visits starting after a moment, oldest first. Forty a
|
|
10
|
+
# page, not a hundred: Jobber prices a query by its page size and refuses the wider one.
|
|
11
|
+
UPCOMING = <<~GRAPHQL
|
|
12
|
+
query($after: String, $from: ISO8601DateTime!) {
|
|
13
|
+
visits(first: 40, after: $after, filter: { startAt: { after: $from } }) {
|
|
14
|
+
nodes {
|
|
15
|
+
id title startAt endAt allDay clientConfirmed
|
|
16
|
+
job { id }
|
|
17
|
+
client { #{CLIENT_FIELDS.keys.join ' '} }
|
|
18
|
+
property { id address { #{Property::SELECTION} } }
|
|
19
|
+
}
|
|
20
|
+
pageInfo { hasNextPage endCursor }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
GRAPHQL
|
|
24
|
+
|
|
25
|
+
# @param oauth [OAuth] the credentials to reach Jobber with.
|
|
26
|
+
# @param node [Hash] the visit as Jobber answered it.
|
|
27
|
+
def initialize(oauth:, node: {})
|
|
28
|
+
super oauth: oauth
|
|
29
|
+
@node = node
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The visits scheduled from now on, oldest first. Nothing is read until the enumerator
|
|
33
|
+
# is walked, and a page is read only once the one before it runs out.
|
|
34
|
+
# @return [Enumerator<Visit>] the account's upcoming visits.
|
|
35
|
+
def upcoming
|
|
36
|
+
Enumerator.new do |yielder|
|
|
37
|
+
nodes.each { |node| yielder << self.class.new(oauth: @oauth, node: node) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [String, nil] the Jobber ID of the visit.
|
|
42
|
+
def id = @node['id']
|
|
43
|
+
|
|
44
|
+
# @return [String, nil] what the visit is called.
|
|
45
|
+
def title = @node['title']
|
|
46
|
+
|
|
47
|
+
# @return [String, nil] the ID of the job the visit belongs to.
|
|
48
|
+
def job_id = @node.dig 'job', 'id'
|
|
49
|
+
|
|
50
|
+
# @return [Boolean, nil] whether the visit takes the whole day rather than an hour of it.
|
|
51
|
+
def all_day? = @node['allDay']
|
|
52
|
+
|
|
53
|
+
# @return [Boolean, nil] whether the client has confirmed the visit.
|
|
54
|
+
def client_confirmed? = @node['clientConfirmed']
|
|
55
|
+
|
|
56
|
+
# @return [String, nil] the ID of the property the work happens at.
|
|
57
|
+
def property_id = @node.dig 'property', 'id'
|
|
58
|
+
|
|
59
|
+
# Who the work is for, in the fields a client is created with.
|
|
60
|
+
# @return [Hash] any of :id, :first_name, :last_name, :phone and :email.
|
|
61
|
+
def client
|
|
62
|
+
CLIENT_FIELDS.to_h { |jobber, ours| [ ours, @node.dig('client', jobber.to_s) ] }.compact
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Where the work happens, in the fields {Property} carries.
|
|
66
|
+
# @return [Hash] any of :street, :city, :state, :zip, :latitude and :longitude.
|
|
67
|
+
def address = Property.fields_from @node.dig('property', 'address')
|
|
68
|
+
|
|
69
|
+
# @return [Time, nil] the visit start time
|
|
70
|
+
def starts_at
|
|
71
|
+
Time.iso8601(@node['startAt']) if @node['startAt']
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [Time, nil] the visit end time
|
|
75
|
+
def ends_at
|
|
76
|
+
Time.iso8601(@node['endAt']) if @node['endAt']
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# The moment is stamped once, before the first page: read per page, it would slide
|
|
82
|
+
# forward and drop a visit that started while the pages were being walked.
|
|
83
|
+
def nodes
|
|
84
|
+
Enumerator.new do |yielder|
|
|
85
|
+
from, after = Time.now.iso8601, nil
|
|
86
|
+
loop do
|
|
87
|
+
page = @oauth.query(UPCOMING, variables: { after: after, from: from }).fetch 'visits', {}
|
|
88
|
+
page.fetch('nodes', []).each { |node| yielder << node }
|
|
89
|
+
break unless page.dig 'pageInfo', 'hasNextPage'
|
|
90
|
+
|
|
91
|
+
after = page.dig 'pageInfo', 'endCursor'
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/jbr.rb
CHANGED
|
@@ -14,11 +14,13 @@ require 'jbr/request'
|
|
|
14
14
|
require 'jbr/oauth'
|
|
15
15
|
|
|
16
16
|
require 'jbr/account'
|
|
17
|
+
# Property comes before Client and Visit: their queries read its fields as they load.
|
|
18
|
+
require 'jbr/property'
|
|
17
19
|
require 'jbr/client'
|
|
18
20
|
require 'jbr/invoice'
|
|
19
21
|
require 'jbr/job'
|
|
20
|
-
require 'jbr/property'
|
|
21
22
|
require 'jbr/quote'
|
|
23
|
+
require 'jbr/visit'
|
|
22
24
|
|
|
23
25
|
require 'jbr/mock/oauth'
|
|
24
26
|
require 'jbr/mock/quote'
|
|
@@ -27,6 +29,7 @@ require 'jbr/mock/invoice'
|
|
|
27
29
|
require 'jbr/mock/request'
|
|
28
30
|
require 'jbr/mock/account'
|
|
29
31
|
require 'jbr/mock/url'
|
|
32
|
+
require 'jbr/mock/visit'
|
|
30
33
|
|
|
31
34
|
require 'jbr/mocking'
|
|
32
35
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jbr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Baccigalupo
|
|
@@ -108,6 +108,7 @@ files:
|
|
|
108
108
|
- lib/jbr/mock/quote.rb
|
|
109
109
|
- lib/jbr/mock/request.rb
|
|
110
110
|
- lib/jbr/mock/url.rb
|
|
111
|
+
- lib/jbr/mock/visit.rb
|
|
111
112
|
- lib/jbr/mocking.rb
|
|
112
113
|
- lib/jbr/oauth.rb
|
|
113
114
|
- lib/jbr/property.rb
|
|
@@ -116,6 +117,7 @@ files:
|
|
|
116
117
|
- lib/jbr/resource.rb
|
|
117
118
|
- lib/jbr/url.rb
|
|
118
119
|
- lib/jbr/version.rb
|
|
120
|
+
- lib/jbr/visit.rb
|
|
119
121
|
homepage: https://github.com/HouseAccountEng/jbr
|
|
120
122
|
licenses:
|
|
121
123
|
- MIT
|