jbr 0.1.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: bc9bc9183104444f2ae6ef18f456a4053272f4b18a9d69cfbdc8a203528b9ccb
4
- data.tar.gz: 2d72fd9fad3344c30ab9db19accecd336ec83a6b3b030c875ccd00d103a45dd3
3
+ metadata.gz: 9a1fb03ffa4daca5169eaad910f2827a11aee58c2ba49bf8bb7cc783e16cbb5b
4
+ data.tar.gz: ac95d6fa65c22bd24975c5ba559a1bda62dfd62fdf9f45c9875a60ea1c8e55c8
5
5
  SHA512:
6
- metadata.gz: 496ecd2ea07ac0f14dfe83b74f2a5ceb318cd7d261f604f351702a20ef2aa2f942ae26dfb9f90a89b5dbb5f708efbbe58947c56cc4fb53d10d0ccd63b1f96f31
7
- data.tar.gz: 221b746aba0ac320f6757462b8600ebdac914ccb943f5988d3a69791f47050aa79a66b7795f37201345b0fff8237b19fe7f5a9077dcc70df55ebc3a318490eac
6
+ metadata.gz: e3cc394c967fc81fb75ca6abd53e6f2d9f6777b61f44e492a96b44e2e06c091bc4b180b12cd7218578eef76c5368b76d9075830e55dbb9f7e8a793c8fef4a3d6
7
+ data.tar.gz: 13b17df1cd0f40caafd12d6cfcfc35cff33aa3580fbf12a69b28fdc7c279dda89b3bee03a35a86597dabd4cc27f7a7563c875211c53770684ff2a482c5af0814
data/CHANGELOG.md CHANGED
@@ -1,5 +1,7 @@
1
- ## [Unreleased]
1
+ ## [1.0.2] - 2026-05-22
2
2
 
3
- ## [0.1.0] - 2026-05-15
3
+ - [Fix] Ensure .find returns nil if the Jobber resource is not found
4
4
 
5
- - Initial release
5
+ ## [1.0.0] - 2026-05-15
6
+
7
+ - Initial release: OAuth, Request, Client, Quote, Job, Invoice, Account classes
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2026 claudiob
3
+ Copyright (c) 2026 HouseAccount
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,39 +1,64 @@
1
- # Jbr
1
+ # Jobber API Ruby client
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ ## Available methods
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/jbr`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ Initialize the authentication with a code:
6
6
 
7
- ## Installation
8
-
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
7
+ ```ruby
8
+ oauth = Jbr::OAuth.create code: code, redirect_uri: jobber_callback_url
9
+ ```
10
10
 
11
- Install the gem and add to the application's Gemfile by executing:
11
+ Access OAuth attributes:
12
12
 
13
- ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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'
15
27
  ```
16
28
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
29
+ Fetches a quote from Jobber:
18
30
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
31
+ ```ruby
32
+ quote = oauth.quotes.find 'Z2lkOi8vS'
33
+ quote.id # => 'Z2lkOi8vS'
34
+ quote.request_id # => 'Z2lkOi8vSm9iYmVyL'
21
35
  ```
22
36
 
23
- ## Usage
24
-
25
- TODO: Write usage instructions here
37
+ Fetches a job from Jobber:
26
38
 
27
- ## Development
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
+ ```
28
46
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
47
+ Fetches a non-draft invoice from Jobber:
30
48
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
+ ```
32
57
 
33
- ## Contributing
34
58
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jbr.
59
+ Revoke authentication:
36
60
 
37
- ## License
61
+ ```ruby
62
+ oauth.delete
63
+ ```
38
64
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -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
data/lib/jbr/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jbr
4
- VERSION = "0.1.0"
4
+ VERSION = '1.0.2'
5
5
  end
data/lib/jbr.rb CHANGED
@@ -1,8 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "jbr/version"
3
+ require 'json'
4
+ require 'net/http'
4
5
 
5
- module Jbr
6
- class Error < StandardError; end
7
- # Your code goes here...
8
- end
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,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jbr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
- - claudiob
7
+ - Claudio Baccigalupo
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: The Jobber API Ruby client
12
+ description: Jobber API
13
13
  email:
14
14
  - claudiob@users.noreply.github.com
15
15
  executables: []
@@ -19,17 +19,24 @@ files:
19
19
  - CHANGELOG.md
20
20
  - LICENSE.txt
21
21
  - README.md
22
- - Rakefile
23
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
24
32
  - lib/jbr/version.rb
25
- - sig/jbr.rbs
26
- homepage: https://example.com
33
+ homepage: https://github.com/HouseAccountEng/jbr
27
34
  licenses:
28
35
  - MIT
29
36
  metadata:
30
- homepage_uri: https://example.com
31
- source_code_uri: https://example.com
32
- changelog_uri: https://example.com
37
+ homepage_uri: https://github.com/HouseAccountEng/jbr
38
+ source_code_uri: https://github.com/HouseAccountEng/jbr
39
+ changelog_uri: https://github.com/HouseAccountEng/jbr
33
40
  rdoc_options: []
34
41
  require_paths:
35
42
  - lib
@@ -46,5 +53,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
46
53
  requirements: []
47
54
  rubygems_version: 4.0.3
48
55
  specification_version: 4
49
- summary: The Jobber API Ruby client
56
+ summary: A Ruby client for the Jobber API.
50
57
  test_files: []
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- task default: :spec
data/sig/jbr.rbs DELETED
@@ -1,4 +0,0 @@
1
- module Jbr
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end