jbr 2.0.0 → 2.1.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: 6c3baeab4ed52ddaf9e86a1086b34b84da47a9bce32a46e05036145dc1c9ca86
4
- data.tar.gz: e0c89dd53a6e64a15bba3b748b1689f2cc063030ba64ed13b18364d1768a6f99
3
+ metadata.gz: 6938d7f413fe243d95247911154afacb7ec9098710f784b9032bbf5bf3d60e37
4
+ data.tar.gz: 7b1b8c0f7b3b339ec96dd1f3343b75528a2a78755c752a0f779218746953b541
5
5
  SHA512:
6
- metadata.gz: cd207ad99f2769ae55bcf2a4f5b149118bafa49f6638d18a4e4a9661ff5729cb1fd1398b67af68cfb17dd251a851b46a350835621a34ed3f888f3676e774d621
7
- data.tar.gz: d8dc7694dff3aa8ef51480ab51500c94903cf0118f6d075e9b8ada9fae761dcb7f91f133a9eb7ee4894c33a18fab59645ae138ccd4aec367fa5c0ccbf50c0a9f
6
+ metadata.gz: 9f4b95151294dc8d1119bc51d5fe63544ca66a0d7073b65e1de0e6b315e9d7cffea572d21567aa05c94a9df5692e0460aa454f531e1a86967209b6e3f81c2d11
7
+ data.tar.gz: f1b7ed20d0eddab211e5a0ca5a68fbf6bfd25762ba20ccde8d76349e6a1fa986bc6dbc3cc062cced2947a8617fdd69cec3e33dcf8031158c24294fd0f8aaaff2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [2.1.0] - 2026-08-07
2
+
3
+ - [New] Fetch the visits an account has scheduled from now on, with oauth.visits.upcoming.
4
+ Each carries its job, its times, whether it takes the whole day, whether the client
5
+ confirmed it, and the address of the property it happens at, in the same fields
6
+ Jbr::Property takes. It answers an Enumerator, so a page is read only once the one
7
+ before it runs out
8
+ - [New] Read a property's latitude and longitude, beside its street rather than under a
9
+ hash of their own. Every query that reads a property asks for them
10
+
1
11
  ## [2.0.0] - 2026-08-07
2
12
 
3
13
  - [Fix] Require nothing but the standard library: to_query, present?, pluck,
data/README.md CHANGED
@@ -86,6 +86,26 @@ 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.address # => { street: '1 Main St', city: 'Raleigh', state: 'NC', zip: '27601',
102
+ # latitude: 35.77, longitude: -78.63 }
103
+ visit.starts_at # => 2026-08-09 14:00:00
104
+ visit.ends_at # => 2026-08-09 16:00:00
105
+ visit.all_day? # => false
106
+ visit.client_confirmed? # => true
107
+ ```
108
+
89
109
  ### Events
90
110
 
91
111
  Parse the payload of a Jobber event webhook:
@@ -145,6 +165,17 @@ Mock successfully fetching a job:
145
165
  Jbr.mock.job = { id: 'job-01', quote_id: 'quote-01', scheduled_at: Date.tomorrow.noon }
146
166
  ```
147
167
 
168
+ ### Visits
169
+
170
+ Mock successfully fetching upcoming visits:
171
+
172
+ ```ruby
173
+ Jbr.mock.visits = [ { id: 'visit-01', title: 'Furnace tune-up', job_id: 'job-01',
174
+ address: { street: '1 Main St', city: 'Raleigh', state: 'NC', zip: '27601' },
175
+ starts_at: Date.tomorrow.noon, ends_at: Date.tomorrow.end_of_day,
176
+ all_day: false, client_confirmed: true } ]
177
+ ```
178
+
148
179
  ### Invoices
149
180
 
150
181
  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 { street1 city province postalCode } }} }
8
+ client { id updatedAt clientProperties { nodes { id address { #{Property::SELECTION} } }} }
9
9
  } }
10
10
  }
11
11
  GRAPHQL
@@ -7,6 +7,7 @@ module Jbr
7
7
  def quotes = Mock::Quote.new(oauth: self)
8
8
  def requests = Mock::Request.new(oauth: self)
9
9
  def account = Mock::Account.new oauth: self
10
+ def visits = Mock::Visit.new(oauth: self)
10
11
 
11
12
  # Revoking a mocked token asks nobody.
12
13
  def delete; end
@@ -0,0 +1,28 @@
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 address = @node.fetch :address, {}
19
+
20
+ def all_day? = @node[:all_day]
21
+
22
+ def client_confirmed? = @node[:client_confirmed]
23
+
24
+ def starts_at = @node[:starts_at]
25
+
26
+ def ends_at = @node[:ends_at]
27
+ end
28
+ 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 everything read or written
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
@@ -1,5 +1,5 @@
1
1
  # A Ruby client for the Jobber API.
2
2
  module Jbr
3
3
  # The version of this gem.
4
- VERSION = '2.0.0'
4
+ VERSION = '2.1.0'
5
5
  end
data/lib/jbr/visit.rb ADDED
@@ -0,0 +1,81 @@
1
+ module Jbr
2
+ # One stop at a property: when the work on a job is scheduled to happen.
3
+ class Visit < Resource
4
+ # The query that reads a page of visits starting after a moment, oldest first. Forty a
5
+ # page, not a hundred: Jobber prices a query by its page size and refuses the wider one.
6
+ UPCOMING = <<~GRAPHQL
7
+ query($after: String, $from: ISO8601DateTime!) {
8
+ visits(first: 40, after: $after, filter: { startAt: { after: $from } }) {
9
+ nodes {
10
+ id title startAt endAt allDay clientConfirmed
11
+ job { id }
12
+ property { address { #{Property::SELECTION} } }
13
+ }
14
+ pageInfo { hasNextPage endCursor }
15
+ }
16
+ }
17
+ GRAPHQL
18
+
19
+ # @param oauth [OAuth] the credentials to reach Jobber with.
20
+ # @param node [Hash] the visit as Jobber answered it.
21
+ def initialize(oauth:, node: {})
22
+ super oauth: oauth
23
+ @node = node
24
+ end
25
+
26
+ # The visits scheduled from now on, oldest first. Nothing is read until the enumerator
27
+ # is walked, and a page is read only once the one before it runs out.
28
+ # @return [Enumerator<Visit>] the account's upcoming visits.
29
+ def upcoming
30
+ Enumerator.new do |yielder|
31
+ nodes.each { |node| yielder << self.class.new(oauth: @oauth, node: node) }
32
+ end
33
+ end
34
+
35
+ # @return [String, nil] the Jobber ID of the visit.
36
+ def id = @node['id']
37
+
38
+ # @return [String, nil] what the visit is called.
39
+ def title = @node['title']
40
+
41
+ # @return [String, nil] the ID of the job the visit belongs to.
42
+ def job_id = @node.dig 'job', 'id'
43
+
44
+ # @return [Boolean, nil] whether the visit takes the whole day rather than an hour of it.
45
+ def all_day? = @node['allDay']
46
+
47
+ # @return [Boolean, nil] whether the client has confirmed the visit.
48
+ def client_confirmed? = @node['clientConfirmed']
49
+
50
+ # Where the work happens, in the fields {Property} carries.
51
+ # @return [Hash] any of :street, :city, :state, :zip, :latitude and :longitude.
52
+ def address = Property.fields_from @node.dig('property', 'address')
53
+
54
+ # @return [Time, nil] the visit start time
55
+ def starts_at
56
+ Time.iso8601(@node['startAt']) if @node['startAt']
57
+ end
58
+
59
+ # @return [Time, nil] the visit end time
60
+ def ends_at
61
+ Time.iso8601(@node['endAt']) if @node['endAt']
62
+ end
63
+
64
+ private
65
+
66
+ # The moment is stamped once, before the first page: read per page, it would slide
67
+ # forward and drop a visit that started while the pages were being walked.
68
+ def nodes
69
+ Enumerator.new do |yielder|
70
+ from, after = Time.now.iso8601, nil
71
+ loop do
72
+ page = @oauth.query(UPCOMING, variables: { after: after, from: from }).fetch 'visits', {}
73
+ page.fetch('nodes', []).each { |node| yielder << node }
74
+ break unless page.dig 'pageInfo', 'hasNextPage'
75
+
76
+ after = page.dig 'pageInfo', 'endCursor'
77
+ end
78
+ end
79
+ end
80
+ end
81
+ 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.0.0
4
+ version: 2.1.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