sunat_invoice 0.0.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.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'tributer'
3
+ require_relative 'utils'
4
+
5
+ module SunatInvoice
6
+ class Provider < Tributer
7
+ include Utils
8
+
9
+ attr_accessor :signature_id, :signature_location_id, :uri, :pk_file,
10
+ :cert_file
11
+
12
+ def address(xml)
13
+ xml['cbc'].ID @ubigeo
14
+ xml['cbc'].StreetName @street
15
+ xml['cbc'].CitySubdivisionName @zone
16
+ xml['cbc'].CityName @province
17
+ xml['cbc'].CountrySubentity @department
18
+ xml['cbc'].District @district
19
+ build_country(xml)
20
+ end
21
+
22
+ def build_country(xml)
23
+ xml['cac'].Country do
24
+ xml['cbc'].IdentificationCode country_code
25
+ end
26
+ end
27
+
28
+ def build_name(xml)
29
+ xml['cac'].PartyName do
30
+ xml['cbc'].Name name
31
+ end
32
+ end
33
+
34
+ def build_registration_name(xml)
35
+ xml['cac'].PartyLegalEntity do
36
+ xml['cbc'].RegistrationName name
37
+ end
38
+ end
39
+
40
+ def info(xml)
41
+ xml['cac'].AccountingSupplierParty do
42
+ xml['cbc'].CustomerAssignedAccountID ruc
43
+ xml['cbc'].AdditionalAccountID document_type
44
+ xml['cac'].Party do
45
+ build_name(xml)
46
+ xml['cac'].PostalAddress { address(xml) }
47
+ build_registration_name(xml)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model'
4
+ require_relative 'utils'
5
+ require 'xmldsig'
6
+
7
+ module SunatInvoice
8
+ class Signature < Model
9
+ include Utils
10
+
11
+ C14N_ALGORITHM = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'
12
+ DIGEST_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#sha1'
13
+ SIGNATURE_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
14
+ TRANSFORMATION_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#enveloped-signature'
15
+
16
+ attr_accessor :provider
17
+
18
+ def signer_data(xml)
19
+ xml['cac'].Signature do
20
+ xml['cbc'].ID provider.signature_id
21
+ xml['cac'].SignatoryParty do
22
+ xml['cac'].PartyIdentification do
23
+ xml['cbc'].ID provider.ruc
24
+ end
25
+ xml['cac'].PartyName do
26
+ xml['cbc'].Name provider.name
27
+ end
28
+ end
29
+ xml['cac'].DigitalSignatureAttachment do
30
+ xml['cac'].ExternalReference do
31
+ xml['cbc'].URI "##{provider.signature_location_id}"
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def sign(invoice_xml)
38
+ Xmldsig::SignedDocument.new(invoice_xml, id_attr: provider.signature_location_id).sign(private_key)
39
+ end
40
+
41
+ def signature_ext(xml)
42
+ ubl_ext(xml) do
43
+ xml['ds'].Signature(Id: provider.signature_location_id) do
44
+ signed_info xml
45
+ signature_value xml
46
+ end
47
+ end
48
+ end
49
+
50
+ def signed_info(xml)
51
+ xml['ds'].SignedInfo do
52
+ xml['ds'].CanonicalizationMethod Algorithm: C14N_ALGORITHM
53
+ xml['ds'].SignatureMethod Algorithm: SIGNATURE_ALGORITHM
54
+ xml['ds'].Reference URI: '' do
55
+ xml['ds'].Transforms do
56
+ xml['ds'].Transform Algorithm: TRANSFORMATION_ALGORITHM
57
+ end
58
+ xml['ds'].DigestMethod Algorithm: DIGEST_ALGORITHM
59
+ xml['ds'].DigestValue
60
+ end
61
+ end
62
+ end
63
+
64
+ def signature_value(xml)
65
+ xml['ds'].SignatureValue
66
+ xml['ds'].KeyInfo do
67
+ xml['ds'].X509Data do
68
+ xml['ds'].X509Certificate encoded_certificate
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def encoded_certificate
76
+ Base64.encode64(certificate.to_der).gsub(/\n/, '')
77
+ end
78
+
79
+ def private_key
80
+ OpenSSL::PKey::RSA.new(File.read(provider.pk_file))
81
+ end
82
+
83
+ def certificate
84
+ OpenSSL::X509::Certificate.new(File.read(provider.cert_file))
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'catalogs'
4
+ require_relative 'utils'
5
+
6
+ module SunatInvoice
7
+ class Tax < Model
8
+ include Utils
9
+
10
+ TAXES = {
11
+ igv: { id: '1000', name: 'IGV', tax_type_code: 'VAT' },
12
+ isc: { id: '2000', name: 'ISC', tax_type_code: 'EXC' },
13
+ other: { id: '9999', name: 'OTROS', tax_type_code: 'OTH' }
14
+ }.freeze
15
+
16
+ attr_accessor :amount, :tax_type, :tax_exemption_reason, :tier_range
17
+
18
+ def initialize(*args)
19
+ super(*args)
20
+ defaults_for_type(tax_type)
21
+ end
22
+
23
+ def defaults_for_type(type)
24
+ case type
25
+ when :igv
26
+ @tax_exemption_reason ||= Catalogs::CATALOG_07.first
27
+ when :isc
28
+ @tier_range ||= Catalogs::CATALOG_08.first
29
+ end
30
+ end
31
+
32
+ def xml(xml, currency)
33
+ xml['cac'].TaxTotal do
34
+ amount_xml(xml['cbc'], 'TaxAmount', amount, currency)
35
+ xml['cac'].TaxSubtotal do
36
+ amount_xml(xml['cbc'], 'TaxAmount', amount, currency)
37
+ tax_category(xml)
38
+ end
39
+ end
40
+ end
41
+
42
+ def tax_category(xml)
43
+ xml['cac'].TaxCategory do
44
+ xml['cbc'].TaxExemptionReasonCode(tax_exemption_reason) if tax_exemption_reason
45
+ xml['cbc'].TierRange(tier_range) if tier_range
46
+ tax_scheme(xml)
47
+ end
48
+ end
49
+
50
+ def tax_scheme(xml)
51
+ xml['cac'].TaxScheme do
52
+ xml['cbc'].ID tax_data(:id)
53
+ xml['cbc'].Name tax_data(:name)
54
+ xml['cbc'].TaxTypeCode tax_data(:tax_type_code)
55
+ end
56
+ end
57
+
58
+ def tax_data(attribute)
59
+ TAXES[tax_type][attribute]
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model'
4
+
5
+ module SunatInvoice
6
+ class Tributer < Model
7
+ attr_accessor :ruc, :name, :document_type, :ubigeo, :street, :zone,
8
+ :province, :department, :district, :country_code
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: false
2
+
3
+ module SunatInvoice
4
+ module Utils
5
+ @namespace_path = 'urn:oasis:names:specification:ubl:schema:xsd'
6
+ @sunat_namespace_path = 'urn:sunat:names:specification:ubl:peru:schema:xsd'
7
+
8
+ UBL_NAMESPACES = {
9
+ 'xmlns' => "#{@namespace_path}:Invoice-2",
10
+ 'xmlns:cac' => "#{@namespace_path}:CommonAggregateComponents-2",
11
+ 'xmlns:cbc' => "#{@namespace_path}:CommonBasicComponents-2",
12
+ 'xmlns:ccts' => 'urn:un:unece:uncefact:documentation:2',
13
+ 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#',
14
+ 'xmlns:ext' => "#{@namespace_path}:CommonExtensionComponents-2",
15
+ 'xmlns:qdt' => "#{@namespace_path}:QualifiedDatatypes-2",
16
+ 'xmlns:sac' => "#{@sunat_namespace_path}:SunatAggregateComponents-1",
17
+ 'xmlns:udt' => 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2',
18
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
19
+ }.freeze
20
+
21
+ def ubl_ext(xml, &block)
22
+ xml['ext'].UBLExtension do
23
+ xml['ext'].ExtensionContent(&block)
24
+ end
25
+ end
26
+
27
+ def amount_xml(xml, tag, price, currency)
28
+ xml.send(tag, price, currencyID: currency)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sunat_invoice/invoice'
4
+ require 'sunat_invoice/provider'
5
+ require 'sunat_invoice/customer'
6
+ require 'sunat_invoice/item'
7
+ require 'sunat_invoice/tax'
8
+ require 'sunat_invoice/configuration'
9
+ require 'sunat_invoice/client'
10
+ require 'sunat_invoice/clients/invoice_client'
11
+
12
+ module SunatInvoice
13
+ class << self
14
+ attr_accessor :configuration
15
+ end
16
+
17
+ def self.configure
18
+ self.configuration ||= Configuration.new
19
+ yield(configuration)
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'sunat_invoice'
5
+ s.version = '0.0.1'
6
+ s.summary = 'SOAP client to use SUNAT Electronic Invoice API'
7
+ s.description = s.summary
8
+ s.authors = ['César Carruitero']
9
+ s.email = ['cesar@mozilla.pe']
10
+ s.homepage = 'https://github.com/ccarruitero/sunat_invoice'
11
+ s.license = 'MPL-2.0'
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+
15
+ s.add_dependency 'savon', '~> 2.11.2'
16
+ s.add_dependency 'nokogiri', '~> 1.8'
17
+ s.add_dependency 'rubyzip', '~> 1.2.1'
18
+ s.add_dependency 'xmldsig', '~> 0.6.5'
19
+
20
+ s.add_development_dependency 'cutest', '~> 1.2'
21
+ s.add_development_dependency 'factory_bot', '~> 4.8'
22
+ s.add_development_dependency 'ffaker', '~> 2.7'
23
+ s.add_development_dependency 'pry', '~> 0.11'
24
+ s.add_development_dependency 'rubocop', '~> 0.51'
25
+ end
data/test/factories.rb ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: false
2
+
3
+ FactoryBot.define do
4
+ factory :tax, class: 'SunatInvoice::Tax' do
5
+ amount 20
6
+ tax_type :igv
7
+ end
8
+
9
+ factory :item, class: 'SunatInvoice::Item' do
10
+ quantity 10
11
+ unit_code 'NIU'
12
+ price 20
13
+ price_code '01'
14
+ description 'Grabadora Externo'
15
+ end
16
+
17
+ factory :invoice, class: 'SunatInvoice::Invoice' do
18
+ provider { build(:provider) }
19
+ customer { build(:customer) }
20
+
21
+ initialize_with { new(attributes) }
22
+ end
23
+
24
+ factory :provider, class: 'SunatInvoice::Provider' do
25
+ pk_file File.join(File.dirname(__FILE__), 'certs/pk_file')
26
+ cert_file File.join(File.dirname(__FILE__), 'certs/cert_file')
27
+ ruc FFaker::IdentificationMX.curp
28
+ name FFaker::Company.name
29
+ document_type 6
30
+ ubigeo '14'
31
+ street ''
32
+ zone ''
33
+ province ''
34
+ department ''
35
+ district ''
36
+ country_code ''
37
+
38
+ initialize_with { new(attributes) }
39
+ end
40
+
41
+ factory :customer, class: 'SunatInvoice::Customer' do
42
+ ruc FFaker::IdentificationMX.curp
43
+ name FFaker::Company.name
44
+ document_type 6
45
+
46
+ initialize_with { new(attributes) }
47
+ end
48
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ require 'cutest'
3
+ require 'pry'
4
+ require 'ffaker'
5
+ require 'factory_bot'
6
+ require_relative '../lib/sunat_invoice'
7
+ require_relative 'factories'
8
+
9
+ Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f }
10
+
11
+ SignatureHelper.generate_keys
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ scope 'SunatInvoice::InvoiceClient' do
6
+ setup do
7
+ provider = FactoryBot.build(:provider,
8
+ signature_id: 'signatureST',
9
+ signature_location_id: 'signQWI3',
10
+ ruc: '20100454523',
11
+ name: 'SOPORTE TECNOLOGICO EIRL')
12
+
13
+ customer = FactoryBot.build(:customer,
14
+ ruc: '20293028401',
15
+ name: 'SOME BUSINESS')
16
+ SunatInvoice.configure do |c|
17
+ c.account_ruc = provider.ruc
18
+ c.account_user = 'MODDATOS'
19
+ c.account_password = 'moddatos'
20
+ c.provider = provider
21
+ end
22
+ @client = SunatInvoice::InvoiceClient.new
23
+
24
+ tax = SunatInvoice::Tax.new(amount: 3.6, tax_type: :igv)
25
+ item = FactoryBot.build(:item, taxes: [tax])
26
+ @invoice = SunatInvoice::Invoice.new(provider: provider,
27
+ customer: customer,
28
+ document_number: 'FY02-234')
29
+ @invoice.items << item
30
+ end
31
+
32
+ test 'should use dev service when not define environment' do
33
+ assert_equal @client.wsdl, @client.dev_server
34
+ end
35
+
36
+
37
+ test '#send_invoice success' do
38
+ response = @client.send_invoice(@invoice.xml, @invoice.document_name)
39
+ assert_equal response.http.code, 200
40
+ end
41
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: false
2
+ require_relative 'helper'
3
+ include SunatInvoice
4
+
5
+ setup do
6
+ @provider = FactoryBot.build(:provider)
7
+ @invoice = FactoryBot.build(:invoice, provider: @provider)
8
+ tax = SunatInvoice::Tax.new(amount: 3.6, tax_type: :igv)
9
+ item_attr = { quantity: 10, price: 20, price_code: '01', taxes: [tax] }
10
+ @invoice.items << SunatInvoice::Item.new(item_attr)
11
+ @parsed_xml = Nokogiri::XML(@invoice.xml, &:noblanks)
12
+ end
13
+
14
+ test 'is not broken' do
15
+ assert !@invoice.nil?
16
+ end
17
+
18
+ test 'has namespaces' do
19
+ cbc = 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'
20
+ namespaces = @parsed_xml.css('Invoice').first.namespaces
21
+ assert_equal namespaces['xmlns:cbc'], cbc
22
+ end
23
+
24
+ test 'xml start with invoice tag' do
25
+ assert_equal @parsed_xml.root.name, 'Invoice'
26
+ end
27
+
28
+ test 'has a date' do
29
+ date = @parsed_xml.xpath('//cbc:IssueDate')
30
+ assert_equal date.first.content, DateTime.now.strftime('%Y-%m-%d')
31
+ end
32
+
33
+ test 'has a signature' do
34
+ # /ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature
35
+ signature = @parsed_xml.xpath('//ext:UBLExtensions/ext:UBLExtension')
36
+ assert_equal signature.xpath('//ext:ExtensionContent/ds:Signature').count, 1
37
+
38
+ # /cac:Signature
39
+ signature = @parsed_xml.xpath('//cac:Signature')
40
+ assert_equal signature.count, 1
41
+ end
42
+
43
+ test 'has a registration name' do
44
+ provider = @parsed_xml.xpath('//cac:AccountingSupplierParty/cac:Party')
45
+ name = provider.xpath('//cac:PartyLegalEntity/cbc:RegistrationName')
46
+ assert_equal name.first.content, @provider.name
47
+ end
48
+
49
+ test 'has a name' do
50
+ provider = @parsed_xml.xpath('//cac:AccountingSupplierParty/cac:Party')
51
+ name = provider.xpath('//cac:PartyName/cbc:Name')
52
+ assert_equal name.first.content, @provider.name
53
+ end
54
+
55
+ test 'has an address' do
56
+ provider = @parsed_xml.xpath('//cac:AccountingSupplierParty/cac:Party')
57
+ ubigeo = provider.xpath('//cac:PostalAddress/cbc:ID')
58
+ assert_equal ubigeo.first.content, @provider.ubigeo
59
+
60
+ street = provider.xpath('//cac:PostalAddress/cbc:StreetName')
61
+ assert_equal street.first.content, @provider.street
62
+
63
+ urbanizacion = provider.xpath('//cac:PostalAddress/cbc:CitySubdivisionName')
64
+ assert_equal urbanizacion.first.content, @provider.zone
65
+
66
+ provincia = provider.xpath('//cac:PostalAddress/cbc:CityName')
67
+ assert_equal provincia.first.content, @provider.province
68
+
69
+ # Departamento
70
+ departamento = provider.xpath('//cac:PostalAddress/cbc:CountrySubentity')
71
+ assert_equal departamento.first.content, @provider.department
72
+
73
+ # Distrito
74
+ distrito = provider.xpath('//cac:PostalAddress/cbc:District')
75
+ assert_equal distrito.first.content, @provider.district
76
+
77
+ country_code_path = '//cac:PostalAddress/cac:Country/cbc:IdentificationCode'
78
+ country_code = provider.xpath(country_code_path)
79
+ assert_equal country_code.first.content, @provider.country_code.to_s
80
+ end
81
+
82
+ test 'has a ruc' do
83
+ provider = @parsed_xml.xpath('//cac:AccountingSupplierParty')
84
+ ruc = provider.xpath('//cbc:CustomerAssignedAccountID')
85
+ assert_equal ruc.first.content, @provider.ruc
86
+
87
+ document_type = provider.xpath('//cbc:AdditionalAccountID')
88
+ assert_equal document_type.first.content, @provider.document_type.to_s
89
+ end
90
+
91
+ test 'has an invoice type' do
92
+ invoice_type = @parsed_xml.xpath('//cbc:InvoiceTypeCode')
93
+ assert_equal invoice_type.first.content, @invoice.document_type
94
+ end
95
+
96
+ test 'has a correlative' do
97
+ tags = @parsed_xml.xpath('//cbc:ID')
98
+ correlative = tags.select { |tax| tax.parent.name == 'Invoice' }.first
99
+ assert_equal correlative.content, @invoice.document_number
100
+ end
101
+
102
+ test 'has a customer' do
103
+ customer = @parsed_xml.xpath('//cac:AccountingCustomerParty')
104
+ assert_equal customer.count, 1
105
+ end
106
+
107
+ test 'has at least one item' do
108
+ item = @parsed_xml.xpath('//cac:InvoiceLine')
109
+ assert item.count.positive?
110
+ end
111
+
112
+ test 'has total by kind of sale' do
113
+ tag = '//ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sac:AdditionalInformation'
114
+ additional_info = @parsed_xml.xpath(tag)
115
+ assert additional_info.count.positive?
116
+ assert_equal additional_info.first.children.count, 1
117
+ amount_tag = '//sac:AdditionalMonetaryTotal/cbc:PayableAmount'
118
+ assert_equal additional_info.xpath(amount_tag).first.content.to_f, 200.to_f
119
+ end
120
+
121
+ test 'has total tag' do
122
+ total = @parsed_xml.xpath('//cac:LegalMonetaryTotal')
123
+ assert total.count.positive?
124
+ end
125
+
126
+ test '#calculate_tax_totals' do
127
+ tax = FactoryBot.build(:tax, amount: 15)
128
+ invoice = FactoryBot.build(:invoice)
129
+ 2.times do
130
+ invoice.items << FactoryBot.build(:item, taxes: [tax])
131
+ end
132
+ invoice.calculate_tax_totals
133
+ tax_totals = invoice.instance_variable_get('@tax_totals')
134
+ assert_equal tax_totals.count, 1
135
+ assert_equal tax_totals[:igv], 300
136
+ end
137
+
138
+ test '#get_total_igv_code' do
139
+ invoice = SunatInvoice::Invoice.new
140
+ assert_equal invoice.get_total_igv_code('11'), '1004'
141
+ assert_equal invoice.get_total_igv_code('15'), '1004'
142
+ assert_equal invoice.get_total_igv_code('20'), '1003'
143
+ assert_equal invoice.get_total_igv_code('32'), '1002'
144
+ assert invoice.get_total_igv_code('42').nil?
145
+ end
data/test/item_test.rb ADDED
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'helper'
3
+ include SunatInvoice
4
+
5
+ setup do
6
+ @item = SunatInvoice::Item.new(quantity: 200, price: 69, price_code: '01',
7
+ description: 'item description',
8
+ unit_code: 'NIU')
9
+ @item.taxes << SunatInvoice::Tax.new(amount: 12.42, tax_type: :igv)
10
+ invoice_setup
11
+ end
12
+
13
+ def invoice_setup
14
+ # add item to invoice in order to setup namespaces
15
+ invoice = FactoryBot.build(:invoice)
16
+ invoice.items << @item
17
+ invoice_xml = Nokogiri::XML(invoice.xml, &:noblanks)
18
+ @item_xml = invoice_xml.xpath('//cac:InvoiceLine')
19
+ end
20
+
21
+ test 'not broken' do
22
+ assert !@item.nil?
23
+ end
24
+
25
+ # xml
26
+ test 'has unit code and quantity' do
27
+ quantity = @item_xml.xpath('//cbc:InvoicedQuantity')
28
+ assert quantity.count.positive?
29
+ assert_equal @item.quantity.to_s, quantity.first.content
30
+
31
+ # /Invoice/cac:InvoiceLine/cbc:InvoicedQuantity/@unitCode
32
+ attributes = quantity.first.attributes
33
+ assert_equal attributes.keys, ['unitCode']
34
+ assert_equal attributes['unitCode'].value, @item.unit_code
35
+ end
36
+
37
+ test 'has a description' do
38
+ description = @item_xml.xpath('//cbc:Description')
39
+ assert_equal @item.description, description.first.content
40
+ end
41
+
42
+ test 'has unit prices' do
43
+ price = @item_xml.xpath('//cac:Price/cbc:PriceAmount')
44
+ assert_equal @item.price.to_s, price.first.content
45
+ # /Invoice/cac:InvoiceLine/cac:Price/cbc:PriceAmount/@currencyID
46
+ assert_equal price.first.attributes.keys, ['currencyID']
47
+ assert_equal price.first.attributes['currencyID'].value, 'PEN'
48
+
49
+ ref_pricing = @item_xml.xpath('//cac:PricingReference')
50
+ alt_condition_price = ref_pricing.xpath('//cac:AlternativeConditionPrice')
51
+ assert_equal 1, alt_condition_price.count
52
+
53
+ tag = '//cac:AlternativeConditionPrice/cbc:PriceAmount'
54
+ alt_price = ref_pricing.xpath(tag)
55
+ assert_equal @item.sale_price.to_s, alt_price.first.content
56
+ # //cac:AlternativeConditionPrice/cbc:PriceAmount/@currencyID
57
+
58
+ price_type = alt_condition_price.xpath('//cbc:PriceTypeCode')
59
+ assert_equal @item.price_code, price_type.first.content
60
+ end
61
+
62
+ # taxes
63
+ test 'has taxes' do
64
+ taxes = @item_xml.xpath('//cac:InvoiceLine/cac:TaxTotal')
65
+ assert_equal 1, taxes.count
66
+ end
67
+
68
+ test 'has amount in correct tag' do
69
+ tag_path = '//cac:InvoiceLine/cac:TaxTotal/cbc:TaxAmount'
70
+ amount = @item_xml.xpath(tag_path)
71
+ assert_equal amount.count, 1
72
+ assert_equal amount.first.content, '12.42'
73
+ # cac:TaxTotal/cbc:TaxAmount/@currencyID
74
+ end
75
+
76
+ test 'has correct values in TaxScheme' do
77
+ tag_path = '//cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme'
78
+ id = @item_xml.xpath("#{tag_path}/cbc:ID")
79
+ assert_equal id.first.content, '1000'
80
+
81
+ name = @item_xml.xpath("#{tag_path}/cbc:Name")
82
+ assert_equal name.first.content, 'IGV'
83
+
84
+ tax_code = @item_xml.xpath("#{tag_path}/cbc:TaxTypeCode")
85
+ assert_equal tax_code.first.content, 'VAT'
86
+ end
87
+
88
+ scope '#sale_taxes' do
89
+ setup do
90
+ tax = SunatInvoice::Tax.new(amount: 10.8, tax_type: :igv)
91
+ @item = SunatInvoice::Item.new(quantity: 2, price: 60, taxes: [tax])
92
+ end
93
+
94
+ test 'return a hash' do
95
+ assert_equal @item.sale_taxes.class, Hash
96
+ end
97
+
98
+ test 'has tax_type and total tax for item' do
99
+ assert_equal @item.sale_taxes.count, 1
100
+ assert_equal @item.sale_taxes.keys, [:igv]
101
+ assert_equal @item.sale_taxes.values, [21.6]
102
+ end
103
+
104
+ test 'has sum by tax_type' do
105
+ @item.taxes << SunatInvoice::Tax.new(amount: 5, tax_type: :isc)
106
+ assert_equal @item.sale_taxes.count, 2
107
+ assert_equal @item.sale_taxes.keys, [:igv, :isc]
108
+ assert_equal @item.sale_taxes.values, [21.6, 10]
109
+ end
110
+ end
@@ -0,0 +1,30 @@
1
+ module SignatureHelper
2
+ def self.generate_keys
3
+ pk = OpenSSL::PKey::RSA.new 2048
4
+ name = OpenSSL::X509::Name.parse('CN=example.com/C=EE')
5
+ cert = OpenSSL::X509::Certificate.new
6
+ cert.version = 2
7
+ cert.serial = 0
8
+ cert.not_before = Time.now
9
+ cert.not_after = cert.not_before + 1 * 365 * 24 * 60 * 60
10
+ cert.public_key = pk.public_key
11
+ cert.subject = name
12
+ cert.issuer = name
13
+ cert.sign pk, OpenSSL::Digest::SHA1.new
14
+
15
+ unless exists?('pk_file')
16
+ cert_dir = "#{File.dirname(__FILE__)}/../certs"
17
+ Dir.mkdir(cert_dir) unless Dir.exist?(cert_dir)
18
+ File.open(file('pk_file'), 'w') { |f| f.puts pk.to_pem }
19
+ File.open(file('cert_file'), 'w') { |f| f.puts cert.to_pem }
20
+ end
21
+ end
22
+
23
+ def self.file(name)
24
+ File.join(File.dirname(__FILE__), "../certs/#{name}")
25
+ end
26
+
27
+ def self.exists?(name)
28
+ File.exists?(file(name))
29
+ end
30
+ end