vertex_client 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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