vertex_client 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.env +2 -0
- data/.gitignore +12 -0
- data/.travis.yml +24 -0
- data/Brewfile +2 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +195 -0
- data/Rakefile +11 -0
- data/bin/bootstrap +23 -0
- data/bin/console +15 -0
- data/bin/setup +11 -0
- data/bin/test +12 -0
- data/bin/update +4 -0
- data/lib/generators/install/install_generator.rb +12 -0
- data/lib/generators/install/templates/initializer.rb.erb +27 -0
- data/lib/vertex_client.rb +89 -0
- data/lib/vertex_client/configuration.rb +35 -0
- data/lib/vertex_client/connection.rb +58 -0
- data/lib/vertex_client/payloads/base.rb +44 -0
- data/lib/vertex_client/payloads/distribute_tax.rb +20 -0
- data/lib/vertex_client/payloads/invoice.rb +29 -0
- data/lib/vertex_client/payloads/quotation.rb +69 -0
- data/lib/vertex_client/payloads/quotation_fallback.rb +14 -0
- data/lib/vertex_client/payloads/tax_area.rb +22 -0
- data/lib/vertex_client/railtie.rb +7 -0
- data/lib/vertex_client/rates.rb +55 -0
- data/lib/vertex_client/resources/base.rb +41 -0
- data/lib/vertex_client/resources/distribute_tax.rb +6 -0
- data/lib/vertex_client/resources/invoice.rb +12 -0
- data/lib/vertex_client/resources/quotation.rb +12 -0
- data/lib/vertex_client/resources/tax_area.rb +10 -0
- data/lib/vertex_client/responses/base.rb +17 -0
- data/lib/vertex_client/responses/distribute_tax.rb +17 -0
- data/lib/vertex_client/responses/invoice.rb +6 -0
- data/lib/vertex_client/responses/line_item.rb +15 -0
- data/lib/vertex_client/responses/quotation.rb +39 -0
- data/lib/vertex_client/responses/quotation_fallback.rb +42 -0
- data/lib/vertex_client/responses/tax_area.rb +9 -0
- data/lib/vertex_client/responses/tax_area_fallback.rb +9 -0
- data/lib/vertex_client/utils/adjustment_allocator.rb +61 -0
- data/lib/vertex_client/version.rb +3 -0
- data/vertex_client.gemspec +54 -0
- metadata +288 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rails'
|
2
|
+
require 'rails/generators'
|
3
|
+
module VertexClient
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
5
|
+
desc 'Installs the vertex_client gem with base configuration'
|
6
|
+
source_root File.expand_path('../templates', __FILE__)
|
7
|
+
|
8
|
+
def create_intitializer
|
9
|
+
template 'initializer.rb.erb', 'config/initializers/vertex_client.rb'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Uncomment the next line if you want to use circuitbox with VertexClient.
|
2
|
+
# require "circuitbox"
|
3
|
+
VertexClient.configure do |config|
|
4
|
+
config.trusted_id = ENV.fetch('VERTEX_TRUSTED_ID')
|
5
|
+
config.soap_api = ENV.fetch('VERTEX_SOAP_API')
|
6
|
+
|
7
|
+
# Circuitbox configuration.
|
8
|
+
# https://github.com/yammer/circuitbox#per-circuit-configuration
|
9
|
+
# config.circuit_config = {
|
10
|
+
# sleep_window: 300,
|
11
|
+
# time_window: 60,
|
12
|
+
# error_threshold: 50,
|
13
|
+
# logger: Rails.logger,
|
14
|
+
# exceptions: [
|
15
|
+
# VertexClient::ServerError
|
16
|
+
# ]
|
17
|
+
# }
|
18
|
+
|
19
|
+
# Optional circuitbox store configuration:
|
20
|
+
# config.circuit_config[:cache] can be set to any moneta-backed store:
|
21
|
+
# see: https://github.com/minad/moneta fore more information.
|
22
|
+
#
|
23
|
+
# Example - Using your Rails.cache's configured dalli store:
|
24
|
+
# backend = Rails.cache.instance_variable_get(:@data)
|
25
|
+
# config.circuit_config[:cache] = Moneta::Adapters::MemcachedDalli.new backend: backend
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'vertex_client/version'
|
2
|
+
require 'vertex_client/railtie' if defined?(Rails)
|
3
|
+
require 'active_support/all'
|
4
|
+
require 'savon'
|
5
|
+
|
6
|
+
module VertexClient
|
7
|
+
|
8
|
+
autoload :Configuration, 'vertex_client/configuration'
|
9
|
+
autoload :Connection, 'vertex_client/connection'
|
10
|
+
autoload :RATES, 'vertex_client/rates'
|
11
|
+
|
12
|
+
module Utils
|
13
|
+
autoload :AdjustmentAllocator, 'vertex_client/utils/adjustment_allocator'
|
14
|
+
end
|
15
|
+
|
16
|
+
module Payload
|
17
|
+
autoload :Base, 'vertex_client/payloads/base'
|
18
|
+
autoload :DistributeTax, 'vertex_client/payloads/distribute_tax'
|
19
|
+
autoload :Invoice, 'vertex_client/payloads/invoice'
|
20
|
+
autoload :Quotation, 'vertex_client/payloads/quotation'
|
21
|
+
autoload :QuotationFallback, 'vertex_client/payloads/quotation_fallback'
|
22
|
+
autoload :TaxArea, 'vertex_client/payloads/tax_area'
|
23
|
+
end
|
24
|
+
|
25
|
+
module Resource
|
26
|
+
autoload :Base, 'vertex_client/resources/base'
|
27
|
+
autoload :DistributeTax, 'vertex_client/resources/distribute_tax'
|
28
|
+
autoload :Invoice, 'vertex_client/resources/invoice'
|
29
|
+
autoload :Quotation, 'vertex_client/resources/quotation'
|
30
|
+
autoload :TaxArea, 'vertex_client/resources/tax_area'
|
31
|
+
end
|
32
|
+
|
33
|
+
module Response
|
34
|
+
autoload :Base, 'vertex_client/responses/base'
|
35
|
+
autoload :DistributeTax, 'vertex_client/responses/distribute_tax'
|
36
|
+
autoload :Invoice, 'vertex_client/responses/invoice'
|
37
|
+
autoload :LineItem, 'vertex_client/responses/line_item'
|
38
|
+
autoload :Quotation, 'vertex_client/responses/quotation'
|
39
|
+
autoload :QuotationFallback, 'vertex_client/responses/quotation_fallback'
|
40
|
+
autoload :TaxArea, 'vertex_client/responses/tax_area'
|
41
|
+
autoload :TaxAreaFallback, 'vertex_client/responses/tax_area_fallback'
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
|
46
|
+
attr_accessor :configuration
|
47
|
+
|
48
|
+
def configuration
|
49
|
+
@configuration ||= Configuration.new
|
50
|
+
end
|
51
|
+
|
52
|
+
def reconfigure!
|
53
|
+
@configuration = Configuration.new
|
54
|
+
yield(@configuration) if block_given?
|
55
|
+
end
|
56
|
+
|
57
|
+
def configure
|
58
|
+
yield(configuration)
|
59
|
+
end
|
60
|
+
|
61
|
+
def quotation(payload)
|
62
|
+
Resource::Quotation.new(payload).result
|
63
|
+
end
|
64
|
+
|
65
|
+
def invoice(payload)
|
66
|
+
Resource::Invoice.new(payload).result
|
67
|
+
end
|
68
|
+
|
69
|
+
def distribute_tax(payload)
|
70
|
+
Resource::DistributeTax.new(payload).result
|
71
|
+
end
|
72
|
+
|
73
|
+
def tax_area(payload)
|
74
|
+
Resource::TaxArea.new(payload).result
|
75
|
+
end
|
76
|
+
|
77
|
+
def circuit
|
78
|
+
return unless configuration.circuit_config && defined?(Circuitbox)
|
79
|
+
Circuitbox.circuit(
|
80
|
+
Configuration::CIRCUIT_NAME,
|
81
|
+
configuration.circuit_config
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class Error < StandardError; end
|
87
|
+
class ValidationError < Error; end
|
88
|
+
class ServerError < Error; end
|
89
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module VertexClient
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
CIRCUIT_NAME = 'vertex_client'.freeze
|
5
|
+
CIRCUIT_CONFIG = {
|
6
|
+
sleep_window: 300,
|
7
|
+
volume_threshold: 10,
|
8
|
+
error_threshold: 50,
|
9
|
+
time_window: 60,
|
10
|
+
logger: Logger.new(STDOUT),
|
11
|
+
exceptions: [
|
12
|
+
Savon::Error
|
13
|
+
]
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
attr_accessor :trusted_id, :soap_api, :circuit_config
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@trusted_id = ENV['VERTEX_TRUSTED_ID']
|
20
|
+
@soap_api = ENV['VERTEX_SOAP_API']
|
21
|
+
end
|
22
|
+
|
23
|
+
def circuit_config
|
24
|
+
CIRCUIT_CONFIG.merge(@circuit_config) if @circuit_config
|
25
|
+
end
|
26
|
+
|
27
|
+
def soap_api
|
28
|
+
@soap_api.gsub(/\/+$/ ,'') + '/'
|
29
|
+
end
|
30
|
+
|
31
|
+
def fallback_rates
|
32
|
+
RATES
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module VertexClient
|
2
|
+
class Connection
|
3
|
+
|
4
|
+
VERTEX_NAMESPACE = "urn:vertexinc:o-series:tps:7:0".freeze
|
5
|
+
ERROR_MESSAGE = 'The Vertex API returned an error or is unavailable'.freeze
|
6
|
+
|
7
|
+
def initialize(endpoint)
|
8
|
+
@endpoint = endpoint
|
9
|
+
end
|
10
|
+
|
11
|
+
def request(payload)
|
12
|
+
call_with_circuit_if_available do
|
13
|
+
client.call(
|
14
|
+
:vertex_envelope,
|
15
|
+
message: shell_with_auth.merge(payload)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def client
|
21
|
+
@client ||= Savon.client do |globals|
|
22
|
+
globals.endpoint clean_endpoint
|
23
|
+
globals.namespace VERTEX_NAMESPACE
|
24
|
+
globals.convert_request_keys_to :camelcase
|
25
|
+
globals.env_namespace :soapenv
|
26
|
+
globals.namespace_identifier :urn
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def config
|
31
|
+
@config ||= VertexClient.configuration
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def call_with_circuit_if_available
|
37
|
+
if VertexClient.circuit
|
38
|
+
VertexClient.circuit.run { yield }
|
39
|
+
else
|
40
|
+
begin
|
41
|
+
yield
|
42
|
+
rescue => _e
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def shell_with_auth
|
49
|
+
{
|
50
|
+
login: { trusted_id: @config.trusted_id }
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def clean_endpoint
|
55
|
+
URI.join(config.soap_api, @endpoint).to_s
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
module VertexClient
|
3
|
+
module Payload
|
4
|
+
class Base
|
5
|
+
|
6
|
+
attr_reader :params
|
7
|
+
|
8
|
+
def initialize(params)
|
9
|
+
@params = params.with_indifferent_access
|
10
|
+
validate!
|
11
|
+
end
|
12
|
+
|
13
|
+
def transform
|
14
|
+
{
|
15
|
+
request_key => body
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def request_key
|
22
|
+
:"#{self.class.name.demodulize.underscore}_request"
|
23
|
+
end
|
24
|
+
|
25
|
+
def transform_customer(customer)
|
26
|
+
remove_nils({
|
27
|
+
:@isTaxExempt => customer[:is_tax_exempt],
|
28
|
+
destination: remove_nils({
|
29
|
+
street_address_1: customer[:address_1],
|
30
|
+
street_address_2: customer[:address_2],
|
31
|
+
city: customer[:city],
|
32
|
+
main_division: customer[:state],
|
33
|
+
postal_code: customer[:postal_code],
|
34
|
+
country: customer[:country]
|
35
|
+
})
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
def remove_nils(hash)
|
40
|
+
hash.select {|_key, value| value.present? }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module VertexClient
|
2
|
+
module Payload
|
3
|
+
class DistributeTax < Invoice
|
4
|
+
def validate!
|
5
|
+
super
|
6
|
+
raise VertexClient::ValidationError.new('total_tax must be specified for all line items') unless all_line_items_have_tax?
|
7
|
+
end
|
8
|
+
|
9
|
+
def all_line_items_have_tax?
|
10
|
+
params[:line_items].all?{ |item| item[:total_tax].present? }
|
11
|
+
end
|
12
|
+
|
13
|
+
def transform_line_item(line_item, number, defaults)
|
14
|
+
remove_nils(super(line_item, number, defaults).merge(
|
15
|
+
input_total_tax: line_item[:total_tax]
|
16
|
+
))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module VertexClient
|
2
|
+
module Payload
|
3
|
+
class Invoice < Quotation
|
4
|
+
|
5
|
+
DOCUMENT_NUMBER_LIMIT = 40
|
6
|
+
|
7
|
+
def validate!
|
8
|
+
super
|
9
|
+
raise VertexClient::ValidationError.new('document_number is required for invoice') if document_number_missing?
|
10
|
+
raise VertexClient::ValidationError.new("document_number must be less than or equal to #{DOCUMENT_NUMBER_LIMIT} characters") if document_number_too_long?
|
11
|
+
end
|
12
|
+
|
13
|
+
def body
|
14
|
+
super.merge({
|
15
|
+
:'@documentNumber' => params[:document_number],
|
16
|
+
:'@documentDate' => params[:date]
|
17
|
+
})
|
18
|
+
end
|
19
|
+
|
20
|
+
def document_number_missing?
|
21
|
+
params[:document_number].to_s.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def document_number_too_long?
|
25
|
+
params[:document_number].to_s.length > DOCUMENT_NUMBER_LIMIT
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module VertexClient
|
2
|
+
module Payload
|
3
|
+
class Quotation < Base
|
4
|
+
|
5
|
+
SALE_TRANSACTION_TYPE = 'SALE'.freeze
|
6
|
+
|
7
|
+
def validate!
|
8
|
+
raise VertexClient::ValidationError.new('customer requires a state or postal_code') if customer_missing_location?
|
9
|
+
end
|
10
|
+
|
11
|
+
def body
|
12
|
+
{
|
13
|
+
:'@transactionType' => SALE_TRANSACTION_TYPE,
|
14
|
+
line_item: params[:line_items].map.with_index do |line_item, index|
|
15
|
+
transform_line_item(line_item, index, params)
|
16
|
+
end
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def customer_missing_location?
|
23
|
+
!customer_lines(params).all? { |customer| customer_destination_present?(customer) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def transform_customer(customer_params)
|
27
|
+
super(customer_params).tap do |customer|
|
28
|
+
if customer_params[:tax_area_id].present?
|
29
|
+
customer[:destination] = { :@taxAreaId => customer_params[:tax_area_id]}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def customer_lines(params)
|
35
|
+
[params[:customer], params[:line_items].map { |li| li[:customer]}].flatten.compact
|
36
|
+
end
|
37
|
+
|
38
|
+
def customer_destination_present?(customer)
|
39
|
+
customer[:state].present? && customer[:postal_code].present?
|
40
|
+
end
|
41
|
+
|
42
|
+
def transform_line_item(line_item, number, defaults)
|
43
|
+
remove_nils({
|
44
|
+
:'@lineItemNumber' => number+1,
|
45
|
+
:'@taxDate' => line_item[:date] || defaults[:date],
|
46
|
+
:'@locationCode' => line_item[:location_code] || defaults[:location_code],
|
47
|
+
customer: transform_customer(line_item[:customer] || defaults[:customer]),
|
48
|
+
seller: transform_seller(line_item[:seller] || defaults[:seller]),
|
49
|
+
product: transform_product(line_item),
|
50
|
+
quantity: line_item[:quantity],
|
51
|
+
extended_price: line_item[:price],
|
52
|
+
})
|
53
|
+
end
|
54
|
+
|
55
|
+
def transform_seller(seller)
|
56
|
+
{
|
57
|
+
company: seller[:company]
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def transform_product(line_item)
|
62
|
+
{
|
63
|
+
:'@productClass' => line_item[:product_class],
|
64
|
+
:content! => line_item[:product_code]
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module VertexClient
|
2
|
+
module Payload
|
3
|
+
class QuotationFallback < Quotation
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def transform_customer(customer_params)
|
8
|
+
customer_without_tax_area = customer_params.dup
|
9
|
+
customer_without_tax_area.delete(:tax_area_id) if customer_without_tax_area[:tax_area_id].present?
|
10
|
+
super(customer_without_tax_area)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module VertexClient
|
2
|
+
module Payload
|
3
|
+
class TaxArea < Base
|
4
|
+
|
5
|
+
def validate!
|
6
|
+
raise VertexClient::ValidationError.new('At least give a value') if values_empty?
|
7
|
+
end
|
8
|
+
|
9
|
+
def body
|
10
|
+
{
|
11
|
+
tax_area_lookup: {
|
12
|
+
postal_address: transform_customer(params)[:destination]
|
13
|
+
}
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def values_empty?
|
18
|
+
params.values.all?(&:empty?)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|