jbr 1.0.0 → 1.0.2

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: e80d8cc7babd6cf1085382e8edb6d1e680afada9eda3cfdf8e3372969a15177d
4
- data.tar.gz: dae5536d6035f67546136f12ffe1fc52d602bdffc8dd5eeba120fec2313c4cd7
3
+ metadata.gz: 9a1fb03ffa4daca5169eaad910f2827a11aee58c2ba49bf8bb7cc783e16cbb5b
4
+ data.tar.gz: ac95d6fa65c22bd24975c5ba559a1bda62dfd62fdf9f45c9875a60ea1c8e55c8
5
5
  SHA512:
6
- metadata.gz: f48b5bf3bf903d427172e3e1aedb004887ade4026049275fadfae98e60eff542d548176910a7e221c6c6c9cfd7ced652a7d0eaae6ea28bc57f7f5f1f06f8bc56
7
- data.tar.gz: 8cf8d948c812075b068479e25737a0d43999831f2dea87a018d6d62fe8c1049d96bb27076f88173ed91d4bd4ae1939669d768402ee9e8ce8e5c215c14014d909
6
+ metadata.gz: e3cc394c967fc81fb75ca6abd53e6f2d9f6777b61f44e492a96b44e2e06c091bc4b180b12cd7218578eef76c5368b76d9075830e55dbb9f7e8a793c8fef4a3d6
7
+ data.tar.gz: 13b17df1cd0f40caafd12d6cfcfc35cff33aa3580fbf12a69b28fdc7c279dda89b3bee03a35a86597dabd4cc27f7a7563c875211c53770684ff2a482c5af0814
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ## [1.0.2] - 2026-05-22
2
+
3
+ - [Fix] Ensure .find returns nil if the Jobber resource is not found
4
+
5
+ ## [1.0.0] - 2026-05-15
6
+
7
+ - Initial release: OAuth, Request, Client, Quote, Job, Invoice, Account classes
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 HouseAccount
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Jobber API Ruby client
2
+
3
+ ## Available methods
4
+
5
+ Initialize the authentication with a code:
6
+
7
+ ```ruby
8
+ oauth = Jbr::OAuth.create code: code, redirect_uri: jobber_callback_url
9
+ ```
10
+
11
+ Access OAuth attributes:
12
+
13
+ ```
14
+ oauth.access_token # => 'eyJhbGciOiJIUzI1NiJ'
15
+ oauth.refresh_token # => 'ea02775958c5fca28d'
16
+ oauth.expires_at # => 2026-05-22 14:32:53
17
+ oauth.account_id # => 'Z2lkOi8vSm9iYmV'
18
+ ````
19
+
20
+ Create a Jobber request, finding or creating a Client with a matching phone number:
21
+
22
+ ```ruby
23
+ request = oauth.requests.create first_name: 'Jane', last_name: 'Doe', phone: '5553335555',
24
+ email: 'jane@example.com', title: 'New Plumber Lead', instructions: 'Needs new faucet'
25
+ request.id # => 'Z2lkOi8vSm9iYmVyL'
26
+ request.client_id # => 'MwMTU0Mg'
27
+ ```
28
+
29
+ Fetches a quote from Jobber:
30
+
31
+ ```ruby
32
+ quote = oauth.quotes.find 'Z2lkOi8vS'
33
+ quote.id # => 'Z2lkOi8vS'
34
+ quote.request_id # => 'Z2lkOi8vSm9iYmVyL'
35
+ ```
36
+
37
+ Fetches a job from Jobber:
38
+
39
+ ```ruby
40
+ job = oauth.jobs.find 'Njc5MTk5'
41
+ job.id # => 'Z2lkOi8vS'
42
+ job.quote_id # => 'Z2lkOi8vS'
43
+ job.scheduled_at # => 2026-05-14 23:02:52
44
+ job.completed_at # => 2026-05-18 11:36:13
45
+ ```
46
+
47
+ Fetches a non-draft invoice from Jobber:
48
+
49
+ ```ruby
50
+ invoice = oauth.invoices.find 'MjU3ODA0'
51
+ invoice.id # => 'MjU3ODA0'
52
+ invoice.job_id # => 'Z2lkOi8vS'
53
+ invoice.total # => '40.30'
54
+ invoice.issued_date # => 2026-05-22
55
+ invoice.completed_at # => 2026-05-22 14:32:53
56
+ ```
57
+
58
+
59
+ Revoke authentication:
60
+
61
+ ```ruby
62
+ oauth.delete
63
+ ```
64
+
@@ -0,0 +1,11 @@
1
+ module Jbr
2
+ class Account < Resource
3
+ FIND = <<~GRAPHQL.freeze
4
+ { account { id } }
5
+ GRAPHQL
6
+
7
+ def id
8
+ @id ||= @oauth.query(FIND).dig 'account', 'id'
9
+ end
10
+ end
11
+ end
data/lib/jbr/client.rb ADDED
@@ -0,0 +1,54 @@
1
+ module Jbr
2
+ class Client < Resource
3
+ LOOKUP = <<~GRAPHQL.freeze
4
+ query($searchTerm: String!) {
5
+ clientPhones(searchTerm: $searchTerm) { nodes { client { id updatedAt } } }
6
+ }
7
+ GRAPHQL
8
+
9
+ CREATE = <<~GRAPHQL.freeze
10
+ mutation($input: ClientCreateInput!) {
11
+ clientCreate(input: $input) { client { id } userErrors { message } }
12
+ }
13
+ GRAPHQL
14
+
15
+ # Create a client instance with the provided attributes.
16
+ # @return [Client] itself
17
+ # @param params [Hash] the attributes of the client
18
+ # @option params [String] :first_name the client’s first name
19
+ # @option params [String] :last_name the client’s last name
20
+ # @option params [String] :phone the client’s phone number
21
+ # @option params [<String, nil>] :email the client’s email address
22
+ def create_with(params = {})
23
+ self.tap { @create_params = params }
24
+ end
25
+
26
+ def find_or_create_by(phone:)
27
+ find_by_phone(phone) || create
28
+ self
29
+ end
30
+
31
+ private
32
+
33
+ def find_by_phone(phone)
34
+ output = @oauth.query LOOKUP, variables: { searchTerm: phone }
35
+ recent = (output.dig('clientPhones', 'nodes') || []).max_by do |clients|
36
+ clients.dig('client', 'updatedAt') || ''
37
+ end
38
+ @id = recent&.dig 'client', 'id'
39
+ end
40
+
41
+ def create
42
+ output = @oauth.query CREATE, variables: { input: input }
43
+ @id = output.dig 'clientCreate', 'client', 'id'
44
+ end
45
+
46
+ def input
47
+ { firstName: @create_params[:first_name],
48
+ lastName: @create_params[:last_name],
49
+ phones: [{ number: @create_params[:phone], primary: true }],
50
+ emails: ([{ address: @create_params[:email], primary: true }] if @create_params[:email].present?)
51
+ }.compact
52
+ end
53
+ end
54
+ end
data/lib/jbr/error.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Jbr
2
+ Error = Class.new StandardError
3
+ end
@@ -0,0 +1,38 @@
1
+ module Jbr
2
+ class Invoice < Resource
3
+ FIND = <<~GRAPHQL.freeze
4
+ query($id: EncodedId!) {
5
+ invoice(id: $id) { id total invoiceStatus issuedDate
6
+ jobs { nodes { id completedAt } } }
7
+ }
8
+ GRAPHQL
9
+
10
+ attr_reader :job_id, :total
11
+
12
+ def find(id)
13
+ output = @oauth.query FIND, variables: { id: id }
14
+ return unless invoice = output['invoice']
15
+ return if invoice['invoiceStatus'].eql? 'draft'
16
+
17
+ @id = invoice['id']
18
+ @total = invoice['total']
19
+ @issued_date = invoice['issuedDate']
20
+
21
+ job = invoice.dig('jobs', 'nodes', 0) || {}
22
+ @job_id = job['id']
23
+ @completed_at = job['completedAt']
24
+
25
+ self
26
+ end
27
+
28
+ # @return [Date] the invoice issued date
29
+ def issued_date
30
+ Date.iso8601(@issued_date) if @issued_date
31
+ end
32
+
33
+ # @return [Time] the job completed time
34
+ def completed_at
35
+ Time.iso8601(@completed_at) if @completed_at
36
+ end
37
+ end
38
+ end
data/lib/jbr/job.rb ADDED
@@ -0,0 +1,32 @@
1
+ module Jbr
2
+ class Job < Resource
3
+ FIND = <<~GRAPHQL.freeze
4
+ query($id: EncodedId!) {
5
+ job(id: $id) { id quote { id } startAt completedAt }
6
+ }
7
+ GRAPHQL
8
+
9
+ attr_reader :quote_id
10
+
11
+ def find(id)
12
+ output = @oauth.query FIND, variables: { id: id }
13
+ return unless job = output['job']
14
+
15
+ @id = job['id']
16
+ @quote_id = job.dig 'quote', 'id'
17
+ @scheduled_at = job['startAt']
18
+ @completed_at = job['completedAt']
19
+ self
20
+ end
21
+
22
+ # @return [Time] the job scheduled time
23
+ def scheduled_at
24
+ Time.iso8601(@scheduled_at) if @scheduled_at
25
+ end
26
+
27
+ # @return [Time] the job completed time
28
+ def completed_at
29
+ Time.iso8601(@completed_at) if @completed_at
30
+ end
31
+ end
32
+ end
data/lib/jbr/oauth.rb ADDED
@@ -0,0 +1,84 @@
1
+ module Jbr
2
+ class OAuth
3
+ DISCONNECT_MUTATION = <<~GRAPHQL.freeze
4
+ mutation Disconnect {
5
+ appDisconnect {
6
+ app { name author }
7
+ userErrors { message }
8
+ }
9
+ }
10
+ GRAPHQL
11
+
12
+ def initialize(credentials = {})
13
+ @access_token = credentials[:access_token]
14
+ @refresh_token = credentials[:refresh_token]
15
+ @expires_at = credentials[:expires_at]
16
+ @account_id = credentials[:account_id]
17
+ @invalid_at = credentials[:invalid_at]
18
+ end
19
+
20
+ attr_reader :access_token, :refresh_token, :expires_at, :invalid_at
21
+ attr_accessor :account_id
22
+
23
+ def account = Account.new oauth: self
24
+ def clients = Client.new oauth: self
25
+ def invoices = Invoice.new oauth: self
26
+ def jobs = Job.new oauth: self
27
+ def quotes = Quote.new oauth: self
28
+ def requests = Request.new oauth: self
29
+
30
+ def query(statement, variables: {})
31
+ client.query statement, variables: variables
32
+ rescue GraphQL::Unauthorized => e
33
+ refresh ? retry : {}
34
+ end
35
+
36
+ # Delete a token. If the token is invalid, do nothing.
37
+ def delete
38
+ client.query DISCONNECT_MUTATION
39
+ rescue GraphQL::Unauthorized => e
40
+ end
41
+
42
+ def self.create(code:, redirect_uri:)
43
+ credentials = post code: code, redirect_uri: redirect_uri, grant_type: 'authorization_code'
44
+ new(credentials).tap { |oauth| oauth.account_id = oauth.account.id }
45
+ end
46
+
47
+ private
48
+
49
+ def refresh
50
+ output = self.class.post refresh_token: @refresh_token, grant_type: 'refresh_token'
51
+ @access_token = output[:access_token]
52
+ @refresh_token = output[:refresh_token]
53
+ @expires_at = output[:expires_at]
54
+ rescue Error => e
55
+ @invalid_at = Time.current
56
+ false
57
+ end
58
+
59
+ def self.post(params = {})
60
+ uri = URI 'https://api.getjobber.com/api/oauth/token'
61
+ response = Net::HTTP.post_form uri, params.merge(client_id: client_id, client_secret: client_secret)
62
+ raise Error, response.body unless response.is_a? Net::HTTPSuccess
63
+ output = JSON.parse(response.body)
64
+ { access_token: output['access_token'], refresh_token: output['refresh_token'],
65
+ expires_at: (Time.current + output.fetch('expires_in', 3600).to_i) }
66
+ end
67
+
68
+ def self.client_id = ENV['JOBBER_CLIENT_ID']
69
+
70
+ def self.client_secret = ENV['JOBBER_CLIENT_SECRET']
71
+
72
+ def client
73
+ GraphQL::Client.new endpoint: 'https://api.getjobber.com/api/graphql', token: @access_token, headers: headers
74
+ end
75
+
76
+ def headers = { 'X-JOBBER-GRAPHQL-VERSION' => '2026-04-22' }
77
+
78
+ def self.url_for(params = {})
79
+ uri = URI 'https://api.getjobber.com/api/oauth/authorize'
80
+ uri.query ||= params.merge(response_type: 'code', client_id: client_id).to_query
81
+ uri.to_s
82
+ end
83
+ end
84
+ end
data/lib/jbr/quote.rb ADDED
@@ -0,0 +1,20 @@
1
+ module Jbr
2
+ class Quote < Resource
3
+ FIND = <<~GRAPHQL.freeze
4
+ query($id: EncodedId!) {
5
+ quote(id: $id) { id request { id } }
6
+ }
7
+ GRAPHQL
8
+
9
+ attr_reader :request_id
10
+
11
+ def find(id)
12
+ output = @oauth.query FIND, variables: { id: id }
13
+ return unless quote = output['quote']
14
+
15
+ @id = quote['id']
16
+ @request_id = quote.dig 'request', 'id'
17
+ self
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ module Jbr
2
+ class Request < Resource
3
+ CREATE = <<~GRAPHQL.freeze
4
+ mutation($input: RequestCreateInput!) {
5
+ requestCreate(input: $input) { request { id } userErrors { message } }
6
+ }
7
+ GRAPHQL
8
+
9
+ attr_reader :client_id
10
+
11
+ # Create a lead in Jobber associated to a new or existing client, matched by phone.
12
+ # @return [String] the ID of the newly created lead.
13
+ # @param params [Hash] the attributes of the lead
14
+ # @option params [String] :first_name the client’s first name
15
+ # @option params [String] :last_name the client’s last name
16
+ # @option params [String] :phone the client’s phone number
17
+ # @option params [<String, nil>] :email the client’s email address
18
+ # @option params [String] :title the reason why the lead is created
19
+ # @option params [String] :instructions a comment about the lead
20
+ def create(params = {})
21
+ @client_id = @oauth.clients.create_with(params).find_or_create_by(phone: params[:phone]).id
22
+ input = { clientId: @client_id, title: params[:title], assessment: { instructions: params[:instructions] } }
23
+ output = @oauth.query CREATE, variables: { input: input }
24
+ @id = output.dig 'requestCreate', 'request', 'id'
25
+ self
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module Jbr
2
+ class Resource
3
+ def initialize(oauth:)
4
+ @oauth = oauth
5
+ end
6
+
7
+ attr_reader :id
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jbr
4
+ VERSION = '1.0.2'
5
+ end
data/lib/jbr.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+
6
+ require 'jbr/error'
7
+ require 'jbr/resource'
8
+ require 'jbr/request'
9
+ require 'jbr/oauth'
10
+
11
+ require 'jbr/account'
12
+ require 'jbr/client'
13
+ require 'jbr/invoice'
14
+ require 'jbr/job'
15
+ require 'jbr/quote'
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: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo
@@ -15,7 +15,21 @@ email:
15
15
  executables: []
16
16
  extensions: []
17
17
  extra_rdoc_files: []
18
- files: []
18
+ files:
19
+ - CHANGELOG.md
20
+ - LICENSE.txt
21
+ - README.md
22
+ - lib/jbr.rb
23
+ - lib/jbr/account.rb
24
+ - lib/jbr/client.rb
25
+ - lib/jbr/error.rb
26
+ - lib/jbr/invoice.rb
27
+ - lib/jbr/job.rb
28
+ - lib/jbr/oauth.rb
29
+ - lib/jbr/quote.rb
30
+ - lib/jbr/request.rb
31
+ - lib/jbr/resource.rb
32
+ - lib/jbr/version.rb
19
33
  homepage: https://github.com/HouseAccountEng/jbr
20
34
  licenses:
21
35
  - MIT