ledger_sync-xero 0.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 +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
|