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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.env +2 -0
  3. data/.gitignore +12 -0
  4. data/.travis.yml +24 -0
  5. data/Brewfile +2 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +195 -0
  10. data/Rakefile +11 -0
  11. data/bin/bootstrap +23 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +11 -0
  14. data/bin/test +12 -0
  15. data/bin/update +4 -0
  16. data/lib/generators/install/install_generator.rb +12 -0
  17. data/lib/generators/install/templates/initializer.rb.erb +27 -0
  18. data/lib/vertex_client.rb +89 -0
  19. data/lib/vertex_client/configuration.rb +35 -0
  20. data/lib/vertex_client/connection.rb +58 -0
  21. data/lib/vertex_client/payloads/base.rb +44 -0
  22. data/lib/vertex_client/payloads/distribute_tax.rb +20 -0
  23. data/lib/vertex_client/payloads/invoice.rb +29 -0
  24. data/lib/vertex_client/payloads/quotation.rb +69 -0
  25. data/lib/vertex_client/payloads/quotation_fallback.rb +14 -0
  26. data/lib/vertex_client/payloads/tax_area.rb +22 -0
  27. data/lib/vertex_client/railtie.rb +7 -0
  28. data/lib/vertex_client/rates.rb +55 -0
  29. data/lib/vertex_client/resources/base.rb +41 -0
  30. data/lib/vertex_client/resources/distribute_tax.rb +6 -0
  31. data/lib/vertex_client/resources/invoice.rb +12 -0
  32. data/lib/vertex_client/resources/quotation.rb +12 -0
  33. data/lib/vertex_client/resources/tax_area.rb +10 -0
  34. data/lib/vertex_client/responses/base.rb +17 -0
  35. data/lib/vertex_client/responses/distribute_tax.rb +17 -0
  36. data/lib/vertex_client/responses/invoice.rb +6 -0
  37. data/lib/vertex_client/responses/line_item.rb +15 -0
  38. data/lib/vertex_client/responses/quotation.rb +39 -0
  39. data/lib/vertex_client/responses/quotation_fallback.rb +42 -0
  40. data/lib/vertex_client/responses/tax_area.rb +9 -0
  41. data/lib/vertex_client/responses/tax_area_fallback.rb +9 -0
  42. data/lib/vertex_client/utils/adjustment_allocator.rb +61 -0
  43. data/lib/vertex_client/version.rb +3 -0
  44. data/vertex_client.gemspec +54 -0
  45. 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
@@ -0,0 +1,7 @@
1
+ module VertexClient
2
+ class Railtie < Rails::Railtie
3
+ generators do
4
+ require 'generators/install/install_generator'
5
+ end
6
+ end
7
+ end