ledger_sync-xero 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.env.test +5 -0
- data/.gitignore +21 -0
- data/.overcommit.yml +29 -0
- data/.rubocop.yml +78 -0
- data/.rubocop_todo.yml +25 -0
- data/.travis.yml +26 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +234 -0
- data/README.md +23 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bin/xero_oauth_server.rb +110 -0
- data/ledger_sync-xero.gemspec +45 -0
- data/lib/ledger_sync/xero.rb +13 -0
- data/lib/ledger_sync/xero/client.rb +158 -0
- data/lib/ledger_sync/xero/config.rb +7 -0
- data/lib/ledger_sync/xero/contact/deserializer.rb +14 -0
- data/lib/ledger_sync/xero/contact/operations/create.rb +20 -0
- data/lib/ledger_sync/xero/contact/operations/find.rb +20 -0
- data/lib/ledger_sync/xero/contact/operations/update.rb +20 -0
- data/lib/ledger_sync/xero/contact/serializer.rb +14 -0
- data/lib/ledger_sync/xero/deserializer.rb +8 -0
- data/lib/ledger_sync/xero/oauth_client.rb +100 -0
- data/lib/ledger_sync/xero/operation.rb +57 -0
- data/lib/ledger_sync/xero/operation/create.rb +26 -0
- data/lib/ledger_sync/xero/operation/find.rb +23 -0
- data/lib/ledger_sync/xero/operation/update.rb +26 -0
- data/lib/ledger_sync/xero/request.rb +57 -0
- data/lib/ledger_sync/xero/resource.rb +8 -0
- data/lib/ledger_sync/xero/resources/contact.rb +10 -0
- data/lib/ledger_sync/xero/serializer.rb +8 -0
- data/lib/ledger_sync/xero/version.rb +19 -0
- metadata +301 -0
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# LedgerSync::Xero
|
2
|
+
|
3
|
+
[ledgersync.dev](www.ledgersync.dev)
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'ledger_sync-xero'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install ledger_sync-xero
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Please visit [ledgersync.dev](www.ledgersync.dev) for full documentation, guidelines, and contribution help.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'ledger_sync/xero'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Setup
|
4
|
+
#
|
5
|
+
# gem install bundler
|
6
|
+
# Ensure you have http://localhost:5678 (or PORT) as a Redirect URI in Xero.
|
7
|
+
|
8
|
+
require 'bundler/inline'
|
9
|
+
|
10
|
+
gemfile do
|
11
|
+
source 'https://rubygems.org'
|
12
|
+
gem 'dotenv'
|
13
|
+
# gem 'ledger_sync', git: 'https://www.github.com/LedgerSync/ledger_sync', branch: 'feature/dotenv'
|
14
|
+
gem 'ledger_sync', path: '../ledger_sync'
|
15
|
+
gem 'ledger_sync-xero', path: './'
|
16
|
+
gem 'rack'
|
17
|
+
gem 'pd_ruby'
|
18
|
+
gem 'byebug'
|
19
|
+
end
|
20
|
+
|
21
|
+
puts 'Gems installed and loaded!'
|
22
|
+
|
23
|
+
require 'pd_ruby'
|
24
|
+
require 'byebug'
|
25
|
+
require 'socket'
|
26
|
+
require 'dotenv'
|
27
|
+
require 'rack'
|
28
|
+
require 'ledger_sync/xero'
|
29
|
+
require 'rack/lobster'
|
30
|
+
Dotenv.load('.env.local')
|
31
|
+
|
32
|
+
port = ENV.fetch('PORT', 5678)
|
33
|
+
server = TCPServer.new(port)
|
34
|
+
|
35
|
+
base_url = "http://localhost:#{port}"
|
36
|
+
|
37
|
+
puts "Listening at #{base_url}"
|
38
|
+
|
39
|
+
client_id = ENV.fetch('XERO_CLIENT_ID')
|
40
|
+
|
41
|
+
raise 'XERO_CLIENT_ID not set in ../.env' if client_id.blank?
|
42
|
+
|
43
|
+
client = LedgerSync::Xero::Client.new_from_env
|
44
|
+
|
45
|
+
puts 'Go to the following URL:'
|
46
|
+
puts client.authorization_url(redirect_uri: base_url)
|
47
|
+
|
48
|
+
while (session = server.accept) # rubocop:disable Lint/UnreachableLoop
|
49
|
+
request = session.gets
|
50
|
+
|
51
|
+
puts request
|
52
|
+
|
53
|
+
# 1
|
54
|
+
_method, full_path = request.split(' ')
|
55
|
+
|
56
|
+
# 2
|
57
|
+
_path, query = full_path.split('?')
|
58
|
+
|
59
|
+
params = Hash[query.split('&').map { |e| e.split('=') }] if query.present?
|
60
|
+
|
61
|
+
client.set_credentials_from_oauth_code(
|
62
|
+
code: params.fetch('code'),
|
63
|
+
redirect_uri: base_url
|
64
|
+
)
|
65
|
+
|
66
|
+
puts "\n"
|
67
|
+
|
68
|
+
puts 'access_token:'
|
69
|
+
puts client.access_token
|
70
|
+
puts ''
|
71
|
+
puts 'client_id:'
|
72
|
+
puts client.client_id
|
73
|
+
puts ''
|
74
|
+
puts 'client_secret:'
|
75
|
+
puts client.client_secret
|
76
|
+
puts ''
|
77
|
+
puts 'refresh_token:'
|
78
|
+
puts client.refresh_token
|
79
|
+
puts ''
|
80
|
+
puts 'expires_at:'
|
81
|
+
puts Time&.at(client.oauth.expires_at.to_i)&.to_datetime
|
82
|
+
puts ''
|
83
|
+
puts 'tenants:'
|
84
|
+
client = LedgerSync::Xero::Client.new_from_env
|
85
|
+
client.tenants.each do |t|
|
86
|
+
puts "#{t['tenantName']} (#{t['tenantType']}) - #{t['tenantId']}"
|
87
|
+
end
|
88
|
+
puts ''
|
89
|
+
puts 'Done!'
|
90
|
+
|
91
|
+
status = 200
|
92
|
+
body = 'Done'
|
93
|
+
headers = {
|
94
|
+
'Content-Length' => body.size
|
95
|
+
}
|
96
|
+
|
97
|
+
session.print "HTTP/1.1 #{status}\r\n"
|
98
|
+
|
99
|
+
headers.each do |key, value|
|
100
|
+
session.print "#{key}: #{value}\r\n"
|
101
|
+
end
|
102
|
+
|
103
|
+
session.print "\r\n"
|
104
|
+
|
105
|
+
session.print body
|
106
|
+
|
107
|
+
session.close
|
108
|
+
|
109
|
+
break
|
110
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'ledger_sync/xero/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'ledger_sync-xero'
|
9
|
+
spec.version = LedgerSync::Xero::VERSION
|
10
|
+
spec.authors = ['Modern Treasury']
|
11
|
+
spec.email = ['ledgersync@moderntreasury.com']
|
12
|
+
|
13
|
+
spec.required_ruby_version = '>= 2.5.8'
|
14
|
+
|
15
|
+
spec.summary = 'Sync common objects to accounting software.'
|
16
|
+
spec.description = 'LedgerSync is a simple library that allows you to sync common objects to popular accounting '\
|
17
|
+
'software like QuickBooks Online, Xero, NetSuite, etc.'
|
18
|
+
spec.homepage = 'https://www.ledgersync.dev'
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
spec.bindir = 'exe'
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
|
29
|
+
spec.add_development_dependency('awesome_print', '>= 0')
|
30
|
+
spec.add_development_dependency('bump', '~> 0.9.0')
|
31
|
+
spec.add_development_dependency('bundler', '~> 2.1')
|
32
|
+
spec.add_development_dependency('byebug')
|
33
|
+
spec.add_development_dependency('climate_control')
|
34
|
+
spec.add_development_dependency('coveralls', '~> 0.8.23')
|
35
|
+
spec.add_development_dependency('factory_bot', '~> 6.1.0')
|
36
|
+
spec.add_development_dependency('overcommit', '~> 0.57.0')
|
37
|
+
spec.add_development_dependency('rake', '~> 13.0')
|
38
|
+
spec.add_development_dependency('rspec', '~> 3.2')
|
39
|
+
spec.add_development_dependency('rubocop', '>= 0')
|
40
|
+
spec.add_development_dependency('webmock', '>= 0')
|
41
|
+
spec.add_runtime_dependency('dotenv')
|
42
|
+
spec.add_runtime_dependency('ledger_sync')
|
43
|
+
spec.add_runtime_dependency('nokogiri', '>= 0')
|
44
|
+
spec.add_runtime_dependency('oauth2', '>= 0')
|
45
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'oauth_client'
|
4
|
+
|
5
|
+
module LedgerSync
|
6
|
+
module Xero
|
7
|
+
class Client
|
8
|
+
include Ledgers::Client::Mixin
|
9
|
+
|
10
|
+
ROOT_URI = 'https://api.xero.com/api.xro/2.0'
|
11
|
+
OAUTH_HEADERS = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }.freeze
|
12
|
+
|
13
|
+
attr_reader :access_token,
|
14
|
+
:client_id,
|
15
|
+
:client_secret,
|
16
|
+
:expires_at,
|
17
|
+
:previous_refresh_tokens,
|
18
|
+
:refresh_token,
|
19
|
+
:refresh_token_expires_at,
|
20
|
+
:tenant_id,
|
21
|
+
:update_dotenv
|
22
|
+
|
23
|
+
def initialize(args = {})
|
24
|
+
@access_token = args.fetch(:access_token)
|
25
|
+
@client_id = args.fetch(:client_id)
|
26
|
+
@client_secret = args.fetch(:client_secret)
|
27
|
+
@refresh_token = args.fetch(:refresh_token)
|
28
|
+
@tenant_id = args.fetch(:tenant_id)
|
29
|
+
@update_dotenv = args.fetch(:update_dotenv, true)
|
30
|
+
|
31
|
+
@previous_access_tokens = []
|
32
|
+
@previous_refresh_tokens = []
|
33
|
+
|
34
|
+
update_secrets_in_dotenv if update_dotenv
|
35
|
+
end
|
36
|
+
|
37
|
+
def authorization_url(redirect_uri:)
|
38
|
+
oauth_client.authorization_url(redirect_uri: redirect_uri)
|
39
|
+
end
|
40
|
+
|
41
|
+
def find(path:)
|
42
|
+
url = "#{ROOT_URI}/#{path.capitalize}"
|
43
|
+
|
44
|
+
request(
|
45
|
+
headers: oauth_headers,
|
46
|
+
method: :get,
|
47
|
+
url: url
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def post(path:, payload:)
|
52
|
+
url = "#{ROOT_URI}/#{path.capitalize}"
|
53
|
+
|
54
|
+
request(
|
55
|
+
headers: oauth_headers,
|
56
|
+
method: :post,
|
57
|
+
body: {
|
58
|
+
path.capitalize => payload
|
59
|
+
},
|
60
|
+
url: url
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def oauth_headers
|
65
|
+
OAUTH_HEADERS.dup.merge('Xero-tenant-id' => @tenant_id)
|
66
|
+
end
|
67
|
+
|
68
|
+
def oauth
|
69
|
+
OAuth2::AccessToken.new(
|
70
|
+
oauth_client.client,
|
71
|
+
access_token,
|
72
|
+
refresh_token: refresh_token
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def oauth_client
|
77
|
+
@oauth_client ||= LedgerSync::Xero::OAuthClient.new(
|
78
|
+
client_id: client_id,
|
79
|
+
client_secret: client_secret
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def refresh!
|
84
|
+
set_credentials_from_oauth_token(
|
85
|
+
token: Request.new(
|
86
|
+
client: self
|
87
|
+
).refresh!
|
88
|
+
)
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
def tenants
|
93
|
+
response = oauth.get(
|
94
|
+
'/connections',
|
95
|
+
body: nil,
|
96
|
+
headers: LedgerSync::Xero::Client::OAUTH_HEADERS.dup
|
97
|
+
)
|
98
|
+
JSON.parse(response.body)
|
99
|
+
end
|
100
|
+
|
101
|
+
def request(method:, url:, body: nil, headers: {})
|
102
|
+
Request.new(
|
103
|
+
client: self,
|
104
|
+
body: body,
|
105
|
+
headers: headers,
|
106
|
+
method: method,
|
107
|
+
url: url
|
108
|
+
).perform
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.new_from_env(**override)
|
112
|
+
new(
|
113
|
+
{
|
114
|
+
access_token: ENV.fetch('XERO_ACCESS_TOKEN'),
|
115
|
+
client_id: ENV.fetch('XERO_CLIENT_ID'),
|
116
|
+
client_secret: ENV.fetch('XERO_CLIENT_SECRET'),
|
117
|
+
refresh_token: ENV.fetch('XERO_REFRESH_TOKEN'),
|
118
|
+
tenant_id: ENV.fetch('XERO_TENANT_ID')
|
119
|
+
}.merge(override)
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
def set_credentials_from_oauth_code(code:, redirect_uri:)
|
124
|
+
oauth_token = oauth_client.get_token(
|
125
|
+
code: code,
|
126
|
+
redirect_uri: redirect_uri
|
127
|
+
)
|
128
|
+
|
129
|
+
set_credentials_from_oauth_token(
|
130
|
+
token: oauth_token
|
131
|
+
)
|
132
|
+
|
133
|
+
oauth_token
|
134
|
+
end
|
135
|
+
|
136
|
+
def set_credentials_from_oauth_token(token:) # rubocop:disable Metrics/CyclomaticComplexity,Naming/AccessorMethodName,Metrics/PerceivedComplexity
|
137
|
+
@previous_access_tokens << access_token if access_token.present?
|
138
|
+
@access_token = token.token
|
139
|
+
|
140
|
+
@expires_at = Time&.at(token.expires_at.to_i)&.to_datetime
|
141
|
+
unless token.params['x_refresh_token_expires_in'].nil?
|
142
|
+
@refresh_token_expires_at = Time&.at(
|
143
|
+
Time.now.to_i + token.params['x_refresh_token_expires_in']
|
144
|
+
)&.to_datetime
|
145
|
+
end
|
146
|
+
|
147
|
+
@previous_refresh_tokens << refresh_token if refresh_token.present?
|
148
|
+
@refresh_token = token.refresh_token
|
149
|
+
ensure
|
150
|
+
update_secrets_in_dotenv if update_dotenv
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.ledger_attributes_to_save
|
154
|
+
%i[access_token expires_at refresh_token refresh_token_expires_at]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LedgerSync
|
4
|
+
module Xero
|
5
|
+
class Contact
|
6
|
+
module Operations
|
7
|
+
class Create < Xero::Operation::Create
|
8
|
+
class Contract < LedgerSync::Ledgers::Contract
|
9
|
+
params do
|
10
|
+
required(:external_id).filled(:string)
|
11
|
+
required(:ledger_id).value(:nil)
|
12
|
+
required(:Name).maybe(:string)
|
13
|
+
required(:EmailAddress).maybe(:string)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LedgerSync
|
4
|
+
module Xero
|
5
|
+
class Contact
|
6
|
+
module Operations
|
7
|
+
class Find < Xero::Operation::Find
|
8
|
+
class Contract < LedgerSync::Ledgers::Contract
|
9
|
+
params do
|
10
|
+
optional(:external_id).filled(:string)
|
11
|
+
required(:ledger_id).filled(:string)
|
12
|
+
optional(:Name).maybe(:string)
|
13
|
+
optional(:EmailAddress).maybe(:string)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|