vertex_client 0.6.1
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/.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
|