jbr 3.13.0 → 5.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -0
  3. data/CHANGELOG.md +139 -0
  4. data/README.md +302 -167
  5. data/lib/graphql/client.rb +2 -6
  6. data/lib/graphql/error.rb +10 -0
  7. data/lib/graphql/throttled.rb +0 -9
  8. data/lib/jbr/account.rb +48 -15
  9. data/lib/jbr/authorizing.rb +43 -0
  10. data/lib/jbr/booking.rb +56 -0
  11. data/lib/jbr/collection.rb +7 -0
  12. data/lib/jbr/collections/customers.rb +54 -0
  13. data/lib/jbr/collections/invoices.rb +18 -0
  14. data/lib/jbr/{jobs.rb → collections/jobs.rb} +8 -8
  15. data/lib/jbr/collections/leads.rb +30 -0
  16. data/lib/jbr/collections/locations.rb +47 -0
  17. data/lib/jbr/collections/quotes.rb +18 -0
  18. data/lib/jbr/collections/technicians.rb +17 -0
  19. data/lib/jbr/collections/visits.rb +61 -0
  20. data/lib/jbr/error.rb +1 -1
  21. data/lib/jbr/errors/throttled.rb +6 -0
  22. data/lib/jbr/includable.rb +10 -8
  23. data/lib/jbr/listable.rb +17 -35
  24. data/lib/jbr/logger.rb +13 -0
  25. data/lib/jbr/mock/account.rb +28 -7
  26. data/lib/jbr/mock/invoice.rb +7 -11
  27. data/lib/jbr/mock/invoices.rb +7 -0
  28. data/lib/jbr/mock/jobs.rb +10 -6
  29. data/lib/jbr/mock/leads.rb +7 -0
  30. data/lib/jbr/mock/quote.rb +3 -8
  31. data/lib/jbr/mock/quotes.rb +7 -0
  32. data/lib/jbr/mock/technicians.rb +12 -0
  33. data/lib/jbr/mock/visit.rb +9 -16
  34. data/lib/jbr/mock/visits.rb +39 -6
  35. data/lib/jbr/mock.rb +17 -4
  36. data/lib/jbr/phone.rb +5 -8
  37. data/lib/jbr/querying.rb +68 -0
  38. data/lib/jbr/reader.rb +9 -0
  39. data/lib/jbr/reading.rb +15 -0
  40. data/lib/jbr/refreshing.rb +0 -8
  41. data/lib/jbr/resources/customer.rb +23 -0
  42. data/lib/jbr/resources/invoice.rb +29 -0
  43. data/lib/jbr/resources/job.rb +20 -0
  44. data/lib/jbr/resources/lead.rb +7 -0
  45. data/lib/jbr/resources/line.rb +15 -0
  46. data/lib/jbr/resources/location.rb +31 -0
  47. data/lib/jbr/resources/quote.rb +15 -0
  48. data/lib/jbr/resources/technician.rb +19 -0
  49. data/lib/jbr/resources/visit.rb +30 -0
  50. data/lib/jbr/scheduled.rb +36 -0
  51. data/lib/jbr/version.rb +1 -1
  52. data/lib/jbr.rb +45 -45
  53. metadata +46 -28
  54. data/lib/jbr/client.rb +0 -98
  55. data/lib/jbr/cliental.rb +0 -14
  56. data/lib/jbr/invoice.rb +0 -43
  57. data/lib/jbr/itemized.rb +0 -9
  58. data/lib/jbr/job.rb +0 -40
  59. data/lib/jbr/line_item.rb +0 -44
  60. data/lib/jbr/mock/client.rb +0 -17
  61. data/lib/jbr/mock/job.rb +0 -31
  62. data/lib/jbr/mock/line_item.rb +0 -16
  63. data/lib/jbr/mock/oauth.rb +0 -22
  64. data/lib/jbr/mock/property.rb +0 -19
  65. data/lib/jbr/mock/request.rb +0 -12
  66. data/lib/jbr/mock/url.rb +0 -9
  67. data/lib/jbr/mocking.rb +0 -30
  68. data/lib/jbr/named.rb +0 -9
  69. data/lib/jbr/oauth.rb +0 -94
  70. data/lib/jbr/properted.rb +0 -17
  71. data/lib/jbr/property.rb +0 -93
  72. data/lib/jbr/quote.rb +0 -25
  73. data/lib/jbr/request.rb +0 -37
  74. data/lib/jbr/resource.rb +0 -26
  75. data/lib/jbr/retriable.rb +0 -20
  76. data/lib/jbr/url.rb +0 -16
  77. data/lib/jbr/visit.rb +0 -24
  78. data/lib/jbr/visits.rb +0 -36
  79. /data/lib/jbr/{refused.rb → errors/refused.rb} +0 -0
data/lib/jbr/mock/job.rb DELETED
@@ -1,31 +0,0 @@
1
- module Jbr
2
- # A job that reads from {Jbr.mock} instead of Jobber.
3
- class Mock::Job < Job
4
- # @return [Object, nil] the values the app asked for.
5
- def id = @node[:id]
6
-
7
- def title = @node[:title]
8
-
9
- def instructions = @node[:instructions]
10
-
11
- def status = @node[:status]
12
-
13
- def quote_id = @node[:quote_id]
14
-
15
- def total = @node[:total]
16
-
17
- def quote_total = @node[:quote_total]
18
-
19
- def created_at = @node[:created_at]
20
-
21
- def client = Mock::Client.new(node: @node.fetch(:client, {}))
22
-
23
- def property = Mock::Property.new(node: @node.fetch(:property, {}))
24
-
25
- def line_items = Mock::LineItem.from(@node.fetch(:line_items, []))
26
-
27
- def scheduled_at = @node[:scheduled_at]
28
-
29
- def completed_at = @node[:completed_at]
30
- end
31
- end
@@ -1,16 +0,0 @@
1
- module Jbr
2
- # A line of a job that reads from {Jbr.mock} instead of Jobber.
3
- class Mock::LineItem < LineItem
4
- # @return [Object, nil] the values the app asked for. The quantity is still read whole,
5
- # so an app that mocks `3.0` of a thing sees the `3` a page would show.
6
- def id = @node[:id]
7
-
8
- def quantity = whole @node[:quantity]
9
-
10
- def name = @node[:name]
11
-
12
- def description = @node[:description]
13
-
14
- def amount = @node[:amount]
15
- end
16
- end
@@ -1,22 +0,0 @@
1
- module Jbr
2
- # Credentials that answer from {Jbr.mock} instead of Jobber.
3
- class Mock::OAuth < OAuth
4
- # The mocked resources these credentials read and write.
5
- def invoices = Mock::Invoice.new(oauth: self)
6
- def jobs = Mock::Jobs.new(oauth: self)
7
- def quotes = Mock::Quote.new(oauth: self)
8
- def requests = Mock::Request.new(oauth: self)
9
- def account = Mock::Account.new oauth: self
10
- def visits = Mock::Visits.new(oauth: self)
11
-
12
- # Revoking a mocked token asks nobody.
13
- def delete; end
14
-
15
- # @return [Hash] canned credentials, unless the app asked for a refusal.
16
- def self.post(_)
17
- raise Error, Jbr.mock.oauth_error if Jbr.mock.oauth_error
18
-
19
- { access_token: 'mock-token', refresh_token: 'mock-token', expires_at: (Time.now + 3600) }
20
- end
21
- end
22
- end
@@ -1,19 +0,0 @@
1
- module Jbr
2
- # A property that reads from {Jbr.mock} instead of Jobber.
3
- class Mock::Property < Property
4
- # @return [Object, nil] the values the app asked for.
5
- def id = @node[:id]
6
-
7
- def street = @node[:street]
8
-
9
- def city = @node[:city]
10
-
11
- def zip = @node[:zip]
12
-
13
- def latitude = @node[:latitude]
14
-
15
- def longitude = @node[:longitude]
16
-
17
- def client = Mock::Client.new(node: @node.fetch(:client, {}))
18
- end
19
- end
@@ -1,12 +0,0 @@
1
- module Jbr
2
- # A request that records what {Jbr.mock} was told to answer.
3
- class Mock::Request < Request
4
- # @return [Mock::Request] itself, carrying the mocked IDs.
5
- def create(_)
6
- @id = Jbr.mock.request[:id]
7
- @client_id = Jbr.mock.request[:client_id]
8
-
9
- self
10
- end
11
- end
12
- end
data/lib/jbr/mock/url.rb DELETED
@@ -1,9 +0,0 @@
1
- module Jbr
2
- # The authorize URL an app under test asked for.
3
- class Mock::URL < URL
4
- # @return [String] whatever {Jbr.mock} was told to answer.
5
- def self.for(_)
6
- Jbr.mock.oauth_url
7
- end
8
- end
9
- end
data/lib/jbr/mocking.rb DELETED
@@ -1,30 +0,0 @@
1
- module Jbr
2
- # The switch an app under test throws to answer Jobber without a network. Touching
3
- # {#mock} once turns every entry point below over to its Mock counterpart.
4
- module Mocking
5
- # @return [Mock] the answers this process gives, created on first use.
6
- def mock
7
- @mock ||= Jbr::Mock.new
8
- end
9
-
10
- # @param params [Hash] the +code+ and +redirect_uri+ to exchange.
11
- # @return [OAuth] credentials for the account that authorized the app.
12
- def create_oauth(params = {})
13
- (@mock ? Mock::OAuth : OAuth).create(**params)
14
- end
15
-
16
- # @param params [Hash] the +redirect_uri+ and +state+ to come back with.
17
- # @return [String] the URL a Jobber user authorizes the app on.
18
- def oauth_url_for(params = {})
19
- (@mock ? Mock::URL : URL).for(**params)
20
- end
21
-
22
- # @param params [Hash] credentials already on file.
23
- # @return [OAuth] those credentials, ready to read and write with.
24
- def oauth_for(params = {})
25
- (@mock ? Mock::OAuth : OAuth).new(params)
26
- end
27
- end
28
-
29
- extend Mocking
30
- end
data/lib/jbr/named.rb DELETED
@@ -1,9 +0,0 @@
1
- module Jbr
2
- # Extends a record Jobber lets go untitled: a visit, a job, anything somebody may have
3
- # opened without ever typing a name for it.
4
- module Named
5
- # Untitled happens often enough, and something has to stand in for it on a list.
6
- # @return [String] the title, or the ID Jobber files the record under. Never empty.
7
- def name = title.presence || id
8
- end
9
- end
data/lib/jbr/oauth.rb DELETED
@@ -1,94 +0,0 @@
1
- module Jbr
2
- # Credentials for one Jobber account, and the gateway to all they read or write.
3
- class OAuth
4
- include Refreshing
5
-
6
- # The mutation that revokes the app on the account.
7
- DISCONNECT_MUTATION = <<~GRAPHQL
8
- mutation Disconnect {
9
- appDisconnect {
10
- app { name author }
11
- userErrors { message }
12
- }
13
- }
14
- GRAPHQL
15
-
16
- # @param credentials [Hash] the tokens, their expiry, the account, when it went bad, and
17
- # the `store:` these are kept in, where processes share them. See {Refreshing}.
18
- def initialize(credentials = {})
19
- @access_token = credentials[:access_token]
20
- @refresh_token = credentials[:refresh_token]
21
- @expires_at = credentials[:expires_at]
22
- @account_id = credentials[:account_id]
23
- @invalid_at = credentials[:invalid_at]
24
- @store = credentials[:store]
25
- end
26
-
27
- # The credentials as Jobber last gave them, plus the moment a refusal to refresh landed.
28
- attr_reader :access_token, :refresh_token, :expires_at, :invalid_at
29
- # @return [String, nil] the account these credentials reach.
30
- attr_accessor :account_id
31
-
32
- # The resources these credentials read and write.
33
- def account = Account.new oauth: self
34
- def clients = Client.new oauth: self
35
- def invoices = Invoice.new oauth: self
36
- def jobs = Jobs.new oauth: self
37
- def quotes = Quote.new oauth: self
38
- def requests = Request.new oauth: self
39
- def visits = Visits.new oauth: self
40
-
41
- # Run a statement, refreshing a stale token. Nothing here ever sleeps: where Jobber holds
42
- # the app to a limit it says so, and a caller asking from a background job has a queue that
43
- # will bring the whole job back later — which is worth more than a worker asleep holding a
44
- # transaction open.
45
- # @return [Hash] the data Jobber answered, or empty when the credentials are dead.
46
- def query(statement, variables: {})
47
- client.query statement, variables: variables
48
- rescue GraphQL::Unauthorized
49
- refresh ? retry : {}
50
- rescue GraphQL::Throttled => error
51
- raise Retriable.new(error.message, error.cost)
52
- rescue GraphQL::Error => error
53
- # The transport's own class never leaves the gem: a caller told to rescue `Jbr::Error`
54
- # was not catching a throttle, a 500 or an unreadable answer, and had its own job blow
55
- # up instead of hearing that Jobber would not answer.
56
- raise Error, error.message
57
- end
58
-
59
- # Delete a token. If the token is invalid, do nothing.
60
- def delete
61
- client.query DISCONNECT_MUTATION
62
- rescue GraphQL::Unauthorized => e
63
- end
64
-
65
- # Exchange an authorization code for credentials, then learn their account.
66
- # @param code [String] the code Jobber sent to the redirect URI.
67
- # @param redirect_uri [String] the URI the code came back to.
68
- # @return [OAuth] the new credentials.
69
- def self.create(code:, redirect_uri:)
70
- credentials = post code: code, redirect_uri: redirect_uri, grant_type: 'authorization_code'
71
- new(credentials).tap { |oauth| oauth.account_id = oauth.account.id }
72
- end
73
-
74
- # @return [String, nil] The client ID to interact with the API.
75
- def self.client_id = ENV['JOBBER_CLIENT_ID']
76
-
77
- # @return [String, nil] The client secret to interact with the API.
78
- def self.client_secret = ENV['JOBBER_CLIENT_SECRET']
79
-
80
- # Exchange a code or a refresh token for credentials.
81
- def self.post(params = {})
82
- Token.post params.merge(client_id: client_id, client_secret: client_secret)
83
- end
84
-
85
- private
86
-
87
- def client
88
- GraphQL::Client.new endpoint: 'https://api.getjobber.com/api/graphql',
89
- token: @access_token, headers: headers
90
- end
91
-
92
- def headers = { 'X-JOBBER-GRAPHQL-VERSION' => '2026-04-22' }
93
- end
94
- end
data/lib/jbr/properted.rb DELETED
@@ -1,17 +0,0 @@
1
- module Jbr
2
- # Extends a record that stands at a property: a visit, a job, anything Jobber files
3
- # somewhere.
4
- module Properted
5
- # What to ask for where a record names the place the work happens at.
6
- # @param client [Boolean] whether the place's own client comes back with it, which spares
7
- # a second query for somebody Jobber already knows the place belongs to.
8
- def self.selection(client: false)
9
- [ 'property { id address {', Property::SELECTION, '}',
10
- (Cliental::SELECTION if client), '}',
11
- ].compact.join ' '
12
- end
13
-
14
- # @return [Property] where the work happens, and who Jobber holds the place for.
15
- def property = Property.new node: @node.fetch('property', {})
16
- end
17
- end
data/lib/jbr/property.rb DELETED
@@ -1,93 +0,0 @@
1
- module Jbr
2
- # Where the work happens: one address on a client's file.
3
- class Property < Resource
4
- include Cliental
5
-
6
- # The mutation that adds a property to a client already on file.
7
- CREATE = <<~GRAPHQL
8
- mutation propertyCreateMutation($clientId: EncodedId!, $input: PropertyCreateInput!) {
9
- propertyCreate(clientId: $clientId, input: $input) {
10
- properties { id }
11
- userErrors { message }
12
- }
13
- }
14
- GRAPHQL
15
-
16
- # What Jobber calls each address field, against what a caller passes.
17
- FIELDS = { street1: :street, city: :city, province: :state, postalCode: :zip }
18
-
19
- # The two Jobber tucks inside the address and a property carries beside the street.
20
- COORDINATES = %i[latitude longitude]
21
-
22
- # The address as Jobber answers it, asked for wherever a property is read.
23
- SELECTION = "#{FIELDS.keys.join ' '} coordinates { #{COORDINATES.join ' '} }"
24
-
25
- # Where the place is, in the fields a caller passes to open one.
26
- # @return [Hash] any of :street, :city, :state, :zip, :latitude and :longitude.
27
- def address = Property.fields_from @node['address']
28
-
29
- # @return [String, nil] the street the work happens on.
30
- def street = address[:street]
31
-
32
- # @return [String, nil] the town the work happens in.
33
- def city = address[:city]
34
-
35
- # @return [String, nil] the postal code the work happens in.
36
- def zip = address[:zip]
37
-
38
- # @return [Float, nil] how far north the work happens.
39
- def latitude = address[:latitude]
40
-
41
- # @return [Float, nil] how far east the work happens.
42
- def longitude = address[:longitude]
43
-
44
- # The fields a match is made on. City and state are written but never matched:
45
- # Jobber holds whatever was typed, so "NC" and "North Carolina" -- or "Winston Salem"
46
- # and "Winston-Salem" -- would read as two homes. The ZIP already places the home.
47
- MATCHED = %i[street1 postalCode]
48
-
49
- # The address as Jobber takes it, from the fields a caller passes.
50
- # @param fields [Hash] any of :street, :city, :state and :zip.
51
- # @return [Hash] the address, without the fields the caller left out.
52
- def self.address_from(fields = {})
53
- FIELDS.to_h { |jobber, ours| [ jobber, fields[ours] ] }.compact_blank
54
- end
55
-
56
- # The fields a caller reads, from the address as Jobber holds it.
57
- # @param address [Hash, nil] the address Jobber answered, if it answered one.
58
- # @return [Hash] any of :street, :city, :state, :zip, :latitude and :longitude,
59
- # without the ones Jobber left out.
60
- def self.fields_from(address)
61
- address ||= {}
62
- coordinates = address['coordinates'] || {}
63
- FIELDS.to_h { |jobber, ours| [ ours, address[jobber.to_s] ] }.
64
- merge(COORDINATES.to_h { |ours| [ ours, coordinates[ours.to_s] ] }).compact_blank
65
- end
66
-
67
- # Reach the property at an address, adding one when none of the client's matches.
68
- # @param client_id [String] the client the property belongs to.
69
- # @param address [Hash] the fields the work happens at.
70
- # @param existing [Array<Hash>] the properties already on the client's file.
71
- # @return [String, nil] the property ID.
72
- def find_or_create_for(client_id:, address:, existing: [])
73
- wanted = self.class.address_from address
74
- match = existing.find { |property| same_address? wanted, property['address'] }
75
- return match['id'] if match
76
-
77
- output = @oauth.query CREATE, variables: {
78
- clientId: client_id, input: { properties: [ { address: wanted } ] },
79
- }
80
- (output&.dig('propertyCreate', 'properties')&.first || {})['id']
81
- end
82
-
83
- private
84
-
85
- # Field by field, because Jobber answers every field it was asked for, nil included,
86
- # while a caller's address carries only what they had -- an absent field and a nil
87
- # one are the same address. All of {MATCHED} has to agree: a home with no street
88
- # parsed must not match the one house on the client's file that does have one.
89
- def same_address?(wanted, address)
90
- MATCHED.all? { |field| wanted[field] == (address || {})[field.to_s] }
91
- end
92
- end
93
- end
data/lib/jbr/quote.rb DELETED
@@ -1,25 +0,0 @@
1
- module Jbr
2
- # A price a Jobber user sent to their client.
3
- class Quote < Resource
4
- # The query that reads one quote and the request it came from.
5
- FIND = <<~GRAPHQL
6
- query($id: EncodedId!) {
7
- quote(id: $id) { id request { id } }
8
- }
9
- GRAPHQL
10
-
11
- # @return [String, nil] the ID of the request the quote answers.
12
- attr_reader :request_id
13
-
14
- # @param id [String] the Jobber ID of the quote.
15
- # @return [Quote, nil] itself, or nil when Jobber has no such quote.
16
- def find(id)
17
- output = @oauth.query FIND, variables: { id: id }
18
- return unless quote = output['quote']
19
-
20
- @id = quote['id']
21
- @request_id = quote.dig 'request', 'id'
22
- self
23
- end
24
- end
25
- end
data/lib/jbr/request.rb DELETED
@@ -1,37 +0,0 @@
1
- module Jbr
2
- # A lead on a Jobber user's board: work someone asked them for.
3
- class Request < Resource
4
- # The mutation that opens a request against a client and a property.
5
- CREATE = <<~GRAPHQL
6
- mutation($input: RequestCreateInput!) {
7
- requestCreate(input: $input) { request { id property { id } } userErrors { message } }
8
- }
9
- GRAPHQL
10
-
11
- # @return [String, nil] the client the request was opened against.
12
- attr_reader :client_id
13
-
14
- # Create a lead in Jobber associated to a new or existing client, matched by phone.
15
- # @return [String] the ID of the newly created lead.
16
- # @param params [Hash] the attributes of the lead
17
- # @option params [String] :first_name the client’s first name
18
- # @option params [String] :last_name the client’s last name
19
- # @option params [String] :phone the client’s phone number
20
- # @option params [<String, nil>] :email the client’s email address
21
- # @option params [String] :title the reason why the lead is created
22
- # @option params [String] :instructions a comment about the lead
23
- def create(params = {})
24
- client = @oauth.clients.create_with(params).find_or_create_by(phone: params[:phone])
25
- @client_id = client.id
26
- @property_id = client.property_id
27
-
28
- input = {
29
- clientId: @client_id, title: params[:title], propertyId: @property_id,
30
- assessment: { instructions: params[:instructions] },
31
- }
32
- output = @oauth.query CREATE, variables: { input: input }
33
- @id = output.dig 'requestCreate', 'request', 'id'
34
- self
35
- end
36
- end
37
- end
data/lib/jbr/resource.rb DELETED
@@ -1,26 +0,0 @@
1
- module Jbr
2
- # What every Jobber resource shares: the node Jobber answered with, the credentials it was
3
- # read through, and what a list of them was asked and narrowed to.
4
- class Resource
5
- # @param oauth [OAuth] the credentials to reach Jobber with, where reaching it is needed.
6
- # @param node [Hash] the record as Jobber answered it.
7
- # @param includes [Hash] what a list was asked to bring back beside its records.
8
- # @param filter [Hash] what a list was narrowed to, in the shape Jobber filters by.
9
- def initialize(oauth: nil, node: {}, includes: {}, filter: nil)
10
- @oauth = oauth
11
- @node = node
12
- @id = node['id']
13
- @includes = includes
14
- @filter = filter
15
- end
16
-
17
- # @return [String, nil] the Jobber ID, from the node it came in or once one was created.
18
- attr_reader :id
19
-
20
- private
21
-
22
- # @return [Time, nil] what Jobber answered under a key, as a time. An empty answer is no
23
- # answer: Time.iso8601 raises on one, where nothing at all it simply has none of.
24
- def time(key) = (Time.iso8601 @node[key] if @node[key].present?)
25
- end
26
- end
data/lib/jbr/retriable.rb DELETED
@@ -1,20 +0,0 @@
1
- module Jbr
2
- # Jobber refusing a query over what it costs rather than over anything about the query. The
3
- # bucket it is priced against refills, so the same question asked later is answered.
4
- class Retriable < Error
5
- # @param message [String] what Jobber said, with the numbers it said it with.
6
- # @param cost [Hash] the `requestedQueryCost` and the `throttleStatus` beside it.
7
- def initialize(message, cost = {})
8
- super message
9
- status = cost['throttleStatus'].to_h
10
- @cost = cost['requestedQueryCost']
11
- @available = status['currentlyAvailable']
12
- @maximum = status['maximumAvailable']
13
- @restore_rate = status['restoreRate']
14
- end
15
-
16
- # What the query would have cost, the budget it was priced against, and how fast that
17
- # budget refills. A cost above the maximum is one no waiting will pay for.
18
- attr_reader :cost, :available, :maximum, :restore_rate
19
- end
20
- end
data/lib/jbr/url.rb DELETED
@@ -1,16 +0,0 @@
1
- module Jbr
2
- # The page a Jobber user authorizes the app on.
3
- class URL
4
- # @param params [Hash] the +redirect_uri+ and +state+ to come back with.
5
- # @return [String] the URL to send the user to.
6
- def self.for(params = {})
7
- uri = URI 'https://api.getjobber.com/api/oauth/authorize'
8
- uri.query = URI.encode_www_form params.merge(response_type: 'code', client_id: client_id)
9
- uri.to_s
10
- end
11
-
12
- # @return [String, nil] the client ID of the app being authorized.
13
- def self.client_id = ENV['JOBBER_CLIENT_ID']
14
- private_class_method :client_id
15
- end
16
- end
data/lib/jbr/visit.rb DELETED
@@ -1,24 +0,0 @@
1
- module Jbr
2
- # One stop at a property: when the work on a job is scheduled to happen.
3
- class Visit < Resource
4
- include Cliental, Named, Properted
5
-
6
- # @return [String, nil] what the visit is called.
7
- def title = @node['title']
8
-
9
- # @return [String, nil] the ID of the job the visit belongs to.
10
- def job_id = @node.dig 'job', 'id'
11
-
12
- # @return [Boolean, nil] whether the visit takes the whole day rather than an hour of it.
13
- def all_day? = @node['allDay']
14
-
15
- # @return [Boolean, nil] whether the client has confirmed the visit.
16
- def client_confirmed? = @node['clientConfirmed']
17
-
18
- # @return [Time, nil] the visit start time
19
- def starts_at = time 'startAt'
20
-
21
- # @return [Time, nil] the visit end time
22
- def ends_at = time 'endAt'
23
- end
24
- end
data/lib/jbr/visits.rb DELETED
@@ -1,36 +0,0 @@
1
- module Jbr
2
- # The visits on a Jobber account, oldest first, walked a page at a time.
3
- class Visits < Resource
4
- include Includable, Listable
5
-
6
- # What a visit answers with, before anything it was asked to bring back with it.
7
- FIELDS = 'id title startAt endAt allDay clientConfirmed job { id }'
8
-
9
- # Shadows Enumerable#find on purpose, the way jobs do: a visit is reached by the ID Jobber
10
- # files it under, not by asking every visit on the account whether it is the one.
11
- # @param id [String] the Jobber ID of the visit.
12
- # @return [Visit, nil] nil when Jobber has no visit under that ID.
13
- def find(id)
14
- node = @oauth.query(one, variables: { id: id })['visit']
15
- Visit.new node: node if node
16
- end
17
-
18
- private
19
-
20
- def page = paged "#{FIELDS} #{selections}", PAGE
21
-
22
- def one
23
- <<~GRAPHQL
24
- query($id: EncodedId!) {
25
- visit(id: $id) { #{FIELDS} #{selections} }
26
- }
27
- GRAPHQL
28
- end
29
-
30
- def field = 'visits'
31
-
32
- def filtered = 'VisitFilterAttributes'
33
-
34
- def item(node) = Visit.new node: node
35
- end
36
- end
File without changes