ledger_sync-xero 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -0,0 +1,8 @@
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
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ledger_sync'
4
+
5
+ module LedgerSync
6
+ module Xero
7
+ def self.root(*paths)
8
+ File.join(File.expand_path('../..', __dir__), *paths.map(&:to_s))
9
+ end
10
+ end
11
+ end
12
+
13
+ require_relative 'xero/config'
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client'
4
+
5
+ LedgerSync.register_ledger(:xero, base_module: LedgerSync::Xero, root_path: 'ledger_sync/xero') do |config|
6
+ config.name = 'Xero'
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Xero
5
+ class Contact
6
+ class Deserializer < Xero::Deserializer
7
+ attribute :ledger_id,
8
+ hash_attribute: 'ContactID'
9
+ attribute :Name
10
+ attribute :EmailAddress
11
+ end
12
+ end
13
+ end
14
+ 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